diff --git a/README.md b/README.md
index 1502f392a..c79eefcaa 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,12 @@ I recommend using Google, GitHub Issues, or Uptime Kuma's subreddit for finding
 My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam)
 You can mention me if you ask a question on the subreddit.
 
-## Contribute
+## Contributions
+
+### Create Pull Requests
+
+We DO NOT accept all types of pull requests and do not want to waste your time. Please be sure that you have read and follow pull request rules:
+[CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)
 
 ### Test Pull Requests
 
@@ -183,6 +188,4 @@ If you want to translate Uptime Kuma into your language, please visit [Weblate R
 Feel free to correct the grammar in the documentation or code.
 My mother language is not English and my grammar is not that great.
 
-### Create Pull Requests
 
-If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
diff --git a/db/knex_migrations/2023-09-29-0000-heartbeat-retires.js b/db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
new file mode 100644
index 000000000..a6b9c7bb9
--- /dev/null
+++ b/db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
@@ -0,0 +1,15 @@
+exports.up = function (knex) {
+    // Add new column heartbeat.retries
+    return knex.schema
+        .alterTable("heartbeat", function (table) {
+            table.integer("retries").notNullable().defaultTo(0);
+        });
+
+};
+
+exports.down = function (knex) {
+    return knex.schema
+        .alterTable("heartbeat", function (table) {
+            table.dropColumn("retries");
+        });
+};
diff --git a/db/knex_migrations/2023-10-16-0000-create-remote-browsers.js b/db/knex_migrations/2023-10-16-0000-create-remote-browsers.js
new file mode 100644
index 000000000..c720d3f4a
--- /dev/null
+++ b/db/knex_migrations/2023-10-16-0000-create-remote-browsers.js
@@ -0,0 +1,21 @@
+exports.up = function (knex) {
+    return knex.schema
+        .createTable("remote_browser", function (table) {
+            table.increments("id");
+            table.string("name", 255).notNullable();
+            table.string("url", 255).notNullable();
+            table.integer("user_id").unsigned();
+        }).alterTable("monitor", function (table) {
+            // Add new column monitor.remote_browser
+            table.integer("remote_browser").nullable().defaultTo(null).unsigned()
+                .index()
+                .references("id")
+                .inTable("remote_browser");
+        });
+};
+
+exports.down = function (knex) {
+    return knex.schema.dropTable("remote_browser").alterTable("monitor", function (table) {
+        table.dropColumn("remote_browser");
+    });
+};
diff --git a/extra/fs-rmSync.js b/extra/fs-rmSync.js
index 0fdbab936..a42e30a68 100644
--- a/extra/fs-rmSync.js
+++ b/extra/fs-rmSync.js
@@ -5,7 +5,7 @@ const fs = require("fs");
  * or the `recursive` property removing completely in the future Node.js version.
  * See the link below.
  * @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
- * @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
+ * @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
  * @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
  * @param {fs.PathLike} path Valid types for path values in "fs".
  * @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
diff --git a/package-lock.json b/package-lock.json
index c86453e52..96311c9b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
                 "form-data": "~4.0.0",
                 "gamedig": "^4.2.0",
                 "html-escaper": "^3.0.3",
+                "http-cookie-agent": "~5.0.4",
                 "http-graceful-shutdown": "~3.1.7",
                 "http-proxy-agent": "~5.0.0",
                 "https-proxy-agent": "~5.0.1",
@@ -77,6 +78,7 @@
                 "tar": "~6.1.11",
                 "tcp-ping": "~0.1.1",
                 "thirty-two": "~1.0.2",
+                "tough-cookie": "~4.1.3",
                 "ws": "^8.13.0"
             },
             "devDependencies": {
@@ -9078,6 +9080,44 @@
             "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
             "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
         },
+        "node_modules/http-cookie-agent": {
+            "version": "5.0.4",
+            "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.4.tgz",
+            "integrity": "sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==",
+            "dependencies": {
+                "agent-base": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=14.18.0 <15.0.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/3846masa"
+            },
+            "peerDependencies": {
+                "deasync": "^0.1.26",
+                "tough-cookie": "^4.0.0",
+                "undici": "^5.11.0"
+            },
+            "peerDependenciesMeta": {
+                "deasync": {
+                    "optional": true
+                },
+                "undici": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/http-cookie-agent/node_modules/agent-base": {
+            "version": "7.1.0",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+            "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+            "dependencies": {
+                "debug": "^4.3.4"
+            },
+            "engines": {
+                "node": ">= 14"
+            }
+        },
         "node_modules/http-errors": {
             "version": "1.8.1",
             "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
@@ -13009,8 +13049,7 @@
         "node_modules/psl": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
-            "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
-            "dev": true
+            "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
         },
         "node_modules/pump": {
             "version": "3.0.0",
@@ -13164,8 +13203,7 @@
         "node_modules/querystringify": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
-            "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
-            "dev": true
+            "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
         },
         "node_modules/queue-microtask": {
             "version": "1.2.3",
@@ -13649,8 +13687,7 @@
         "node_modules/requires-port": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
-            "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
-            "dev": true
+            "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
         },
         "node_modules/resolve": {
             "version": "1.22.8",
@@ -15329,7 +15366,6 @@
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
             "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
-            "dev": true,
             "dependencies": {
                 "psl": "^1.1.33",
                 "punycode": "^2.1.1",
@@ -15344,7 +15380,6 @@
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
             "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
-            "dev": true,
             "engines": {
                 "node": ">= 4.0.0"
             }
@@ -15679,7 +15714,6 @@
             "version": "1.5.10",
             "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
             "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
-            "dev": true,
             "dependencies": {
                 "querystringify": "^2.1.1",
                 "requires-port": "^1.0.0"
diff --git a/package.json b/package.json
index a7f79c472..d278bb211 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
         "express-static-gzip": "~2.1.7",
         "form-data": "~4.0.0",
         "gamedig": "^4.2.0",
+        "http-cookie-agent": "~5.0.4",
         "html-escaper": "^3.0.3",
         "http-graceful-shutdown": "~3.1.7",
         "http-proxy-agent": "~5.0.0",
@@ -141,6 +142,7 @@
         "tar": "~6.1.11",
         "tcp-ping": "~0.1.1",
         "thirty-two": "~1.0.2",
+        "tough-cookie": "~4.1.3",
         "ws": "^8.13.0"
     },
     "devDependencies": {
diff --git a/server/client.js b/server/client.js
index d03065f36..260e77a73 100644
--- a/server/client.js
+++ b/server/client.js
@@ -185,6 +185,30 @@ async function sendDockerHostList(socket) {
     return list;
 }
 
+/**
+ * Send list of docker hosts to client
+ * @param {Socket} socket Socket.io socket instance
+ * @returns {Promise<Bean[]>} List of docker hosts
+ */
+async function sendRemoteBrowserList(socket) {
+    const timeLogger = new TimeLogger();
+
+    let result = [];
+    let list = await R.find("remote_browser", " user_id = ? ", [
+        socket.userID,
+    ]);
+
+    for (let bean of list) {
+        result.push(bean.toJSON());
+    }
+
+    io.to(socket.userID).emit("remoteBrowserList", result);
+
+    timeLogger.print("Send Remote Browser List");
+
+    return list;
+}
+
 module.exports = {
     sendNotificationList,
     sendImportantHeartbeatList,
@@ -192,5 +216,6 @@ module.exports = {
     sendProxyList,
     sendAPIKeyList,
     sendInfo,
-    sendDockerHostList
+    sendDockerHostList,
+    sendRemoteBrowserList,
 };
diff --git a/server/docker.js b/server/docker.js
index a96324a9f..bec0e0b12 100644
--- a/server/docker.js
+++ b/server/docker.js
@@ -1,10 +1,10 @@
 const axios = require("axios");
 const { R } = require("redbean-node");
-const version = require("../package.json").version;
 const https = require("https");
 const fs = require("fs");
 const path = require("path");
 const Database = require("./database");
+const { axiosAbortSignal } = require("./util-server");
 
 class DockerHost {
 
@@ -70,9 +70,11 @@ class DockerHost {
     static async testDockerHost(dockerHost) {
         const options = {
             url: "/containers/json?all=true",
+            timeout: 5000,
             headers: {
                 "Accept": "*/*",
             },
+            signal: axiosAbortSignal(6000),
         };
 
         if (dockerHost.dockerType === "socket") {
@@ -82,26 +84,33 @@ class DockerHost {
             options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
         }
 
-        let res = await axios.request(options);
+        try {
+            let res = await axios.request(options);
 
-        if (Array.isArray(res.data)) {
+            if (Array.isArray(res.data)) {
 
-            if (res.data.length > 1) {
+                if (res.data.length > 1) {
+
+                    if ("ImageID" in res.data[0]) {
+                        return res.data.length;
+                    } else {
+                        throw new Error("Invalid Docker response, is it Docker really a daemon?");
+                    }
 
-                if ("ImageID" in res.data[0]) {
-                    return res.data.length;
                 } else {
-                    throw new Error("Invalid Docker response, is it Docker really a daemon?");
+                    return res.data.length;
                 }
 
             } else {
-                return res.data.length;
+                throw new Error("Invalid Docker response, is it Docker really a daemon?");
+            }
+        } catch (e) {
+            if (e.code === "ECONNABORTED" || e.name === "CanceledError") {
+                throw new Error("Connection to Docker daemon timed out.");
+            } else {
+                throw e;
             }
-
-        } else {
-            throw new Error("Invalid Docker response, is it Docker really a daemon?");
         }
-
     }
 
     /**
diff --git a/server/model/heartbeat.js b/server/model/heartbeat.js
index cc86ef634..9e972a376 100644
--- a/server/model/heartbeat.js
+++ b/server/model/heartbeat.js
@@ -29,13 +29,14 @@ class Heartbeat extends BeanModel {
      */
     toJSON() {
         return {
-            monitorID: this.monitor_id,
-            status: this.status,
-            time: this.time,
-            msg: this.msg,
-            ping: this.ping,
-            important: this.important,
-            duration: this.duration,
+            monitorID: this._monitorId,
+            status: this._status,
+            time: this._time,
+            msg: this._msg,
+            ping: this._ping,
+            important: this._important,
+            duration: this._duration,
+            retries: this._retries,
         };
     }
 
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 4c1dbc453..3cf72d235 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -1,4 +1,3 @@
-const https = require("https");
 const dayjs = require("dayjs");
 const axios = require("axios");
 const { Prometheus } = require("../prometheus");
@@ -23,6 +22,8 @@ const jsonata = require("jsonata");
 const jwt = require("jsonwebtoken");
 const crypto = require("crypto");
 const { UptimeCalculator } = require("../uptime-calculator");
+const { CookieJar } = require("tough-cookie");
+const { HttpsCookieAgent } = require("http-cookie-agent/http");
 
 const rootCertificates = rootCertificatesFingerprints();
 
@@ -153,6 +154,7 @@ class Monitor extends BeanModel {
             kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
             kafkaProducerMessage: this.kafkaProducerMessage,
             screenshot,
+            remote_browser: this.remote_browser,
         };
 
         if (includeSensitiveData) {
@@ -351,6 +353,9 @@ class Monitor extends BeanModel {
                 previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
                     this.id,
                 ]);
+                if (previousBeat) {
+                    retries = previousBeat.retries;
+                }
             }
 
             const isFirstBeat = !previousBeat;
@@ -504,7 +509,12 @@ class Monitor extends BeanModel {
                     }
 
                     if (!options.httpsAgent) {
-                        options.httpsAgent = new https.Agent(httpsAgentOptions);
+                        let jar = new CookieJar();
+                        let httpsCookieAgentOptions = {
+                            ...httpsAgentOptions,
+                            cookies: { jar }
+                        };
+                        options.httpsAgent = new HttpsCookieAgent(httpsCookieAgentOptions);
                     }
 
                     if (this.auth_method === "mtls") {
@@ -622,6 +632,7 @@ class Monitor extends BeanModel {
                         // If the previous beat was down or pending we use the regular
                         // beatInterval/retryInterval in the setTimeout further below
                         if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
+                            bean.duration = Math.round(msSinceLastBeat / 1000);
                             throw new Error("No heartbeat in the time window");
                         } else {
                             let timeout = beatInterval * 1000 - msSinceLastBeat;
@@ -637,6 +648,7 @@ class Monitor extends BeanModel {
                             return;
                         }
                     } else {
+                        bean.duration = beatInterval;
                         throw new Error("No heartbeat in the time window");
                     }
 
@@ -909,9 +921,14 @@ class Monitor extends BeanModel {
                 } else if ((this.maxretries > 0) && (retries < this.maxretries)) {
                     retries++;
                     bean.status = PENDING;
+                } else {
+                    // Continue counting retries during DOWN
+                    retries++;
                 }
             }
 
+            bean.retries = retries;
+
             log.debug("monitor", `[${this.name}] Check isImportant`);
             let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
 
@@ -1425,10 +1442,7 @@ class Monitor extends BeanModel {
      * @returns {Promise<LooseObject<any>>} Previous heartbeat
      */
     static async getPreviousHeartbeat(monitorID) {
-        return await R.getRow(`
-            SELECT ping, status, time FROM heartbeat
-            WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
-        `, [
+        return await R.findOne("heartbeat", " id = (select MAX(id) from heartbeat where monitor_id = ?)", [
             monitorID
         ]);
     }
diff --git a/server/model/remote_browser.js b/server/model/remote_browser.js
new file mode 100644
index 000000000..49299ad4f
--- /dev/null
+++ b/server/model/remote_browser.js
@@ -0,0 +1,17 @@
+const { BeanModel } = require("redbean-node/dist/bean-model");
+
+class RemoteBrowser extends BeanModel {
+    /**
+     * Returns an object that ready to parse to JSON
+     * @returns {object} Object ready to parse
+     */
+    toJSON() {
+        return {
+            id: this.id,
+            url: this.url,
+            name: this.name,
+        };
+    }
+}
+
+module.exports = RemoteBrowser;
diff --git a/server/monitor-types/real-browser-monitor-type.js b/server/monitor-types/real-browser-monitor-type.js
index bda1b57c6..c03d36ca6 100644
--- a/server/monitor-types/real-browser-monitor-type.js
+++ b/server/monitor-types/real-browser-monitor-type.js
@@ -8,6 +8,7 @@ const path = require("path");
 const Database = require("../database");
 const jwt = require("jsonwebtoken");
 const config = require("../config");
+const { RemoteBrowser } = require("../remote-browser");
 
 let browser = null;
 
@@ -86,6 +87,19 @@ async function getBrowser() {
     return browser;
 }
 
+/**
+ * Get the current instance of the browser. If there isn't one, create it
+ * @param {integer} remoteBrowserID Path to executable
+ * @param {integer} userId User ID
+ * @returns {Promise<Browser>} The browser
+ */
+async function getRemoteBrowser(remoteBrowserID, userId) {
+    let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
+    log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
+    browser = chromium.connect(remoteBrowser.url);
+    return browser;
+}
+
 /**
  * Prepare the chrome executable path
  * @param {string} executablePath Path to chrome executable
@@ -192,11 +206,21 @@ async function testChrome(executablePath) {
         throw new Error(e.message);
     }
 }
-
+// test remote browser
 /**
- * TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
- *
+ * @param {string} remoteBrowserURL Remote Browser URL
+ * @returns {Promise<boolean>} Returns if connection worked
  */
+async function testRemoteBrowser(remoteBrowserURL) {
+    try {
+        const browser = await chromium.connect(remoteBrowserURL);
+        browser.version();
+        await browser.close();
+        return true;
+    } catch (e) {
+        throw new Error(e.message);
+    }
+}
 class RealBrowserMonitorType extends MonitorType {
 
     name = "real-browser";
@@ -205,7 +229,7 @@ class RealBrowserMonitorType extends MonitorType {
      * @inheritdoc
      */
     async check(monitor, heartbeat, server) {
-        const browser = await getBrowser();
+        const browser = monitor.remote_browser ? await getRemoteBrowser(monitor.remote_browser, monitor.user_id) : await getBrowser();
         const context = await browser.newContext();
         const page = await context.newPage();
 
@@ -238,4 +262,5 @@ module.exports = {
     RealBrowserMonitorType,
     testChrome,
     resetChrome,
+    testRemoteBrowser,
 };
diff --git a/server/notification-providers/mattermost.js b/server/notification-providers/mattermost.js
index 9cbb51bbf..5a6a8e2c5 100644
--- a/server/notification-providers/mattermost.js
+++ b/server/notification-providers/mattermost.js
@@ -78,12 +78,12 @@ class Mattermost extends NotificationProvider {
                     {
                         fallback:
                             "Your " +
-                            monitorJSON.name +
+                            monitorJSON.pathName +
                             " service went " +
                             statusText,
                         color: color,
                         title:
-                            monitorJSON.name +
+                            monitorJSON.pathName +
                             " service went " +
                             statusText,
                         title_link: monitorJSON.url,
diff --git a/server/proxy.js b/server/proxy.js
index a0b4378d1..3f3771ab9 100644
--- a/server/proxy.js
+++ b/server/proxy.js
@@ -4,6 +4,8 @@ const HttpsProxyAgent = require("https-proxy-agent");
 const SocksProxyAgent = require("socks-proxy-agent");
 const { debug } = require("../src/util");
 const { UptimeKumaServer } = require("./uptime-kuma-server");
+const { CookieJar } = require("tough-cookie");
+const { createCookieAgent } = require("http-cookie-agent/http");
 
 class Proxy {
 
@@ -95,10 +97,13 @@ class Proxy {
         let httpAgent;
         let httpsAgent;
 
+        let jar = new CookieJar();
+
         const proxyOptions = {
             protocol: proxy.protocol,
             host: proxy.host,
             port: proxy.port,
+            cookies: { jar },
         };
 
         if (proxy.auth) {
@@ -112,12 +117,17 @@ class Proxy {
         switch (proxy.protocol) {
             case "http":
             case "https":
-                httpAgent = new HttpProxyAgent({
+                // eslint-disable-next-line no-case-declarations
+                const HttpCookieProxyAgent = createCookieAgent(HttpProxyAgent);
+                // eslint-disable-next-line no-case-declarations
+                const HttpsCookieProxyAgent = createCookieAgent(HttpsProxyAgent);
+
+                httpAgent = new HttpCookieProxyAgent({
                     ...httpAgentOptions || {},
-                    ...proxyOptions
+                    ...proxyOptions,
                 });
 
-                httpsAgent = new HttpsProxyAgent({
+                httpsAgent = new HttpsCookieProxyAgent({
                     ...httpsAgentOptions || {},
                     ...proxyOptions,
                 });
@@ -126,7 +136,9 @@ class Proxy {
             case "socks5":
             case "socks5h":
             case "socks4":
-                agent = new SocksProxyAgent({
+                // eslint-disable-next-line no-case-declarations
+                const SocksCookieProxyAgent = createCookieAgent(SocksProxyAgent);
+                agent = new SocksCookieProxyAgent({
                     ...httpAgentOptions,
                     ...httpsAgentOptions,
                     ...proxyOptions,
diff --git a/server/remote-browser.js b/server/remote-browser.js
new file mode 100644
index 000000000..0d17f1a56
--- /dev/null
+++ b/server/remote-browser.js
@@ -0,0 +1,84 @@
+const { R } = require("redbean-node");
+const { testRemoteBrowser } = require("./monitor-types/real-browser-monitor-type.js");
+class RemoteBrowser {
+
+    /**
+     * Gets remote browser from ID
+     * @param {number} remoteBrowserID ID of the remote browser
+     * @param {number} userID ID of the user who created the remote browser
+     * @returns {Promise<Bean>} Remote Browser
+     */
+    static async get(remoteBrowserID, userID) {
+        let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
+
+        if (!bean) {
+            throw new Error("Remote browser not found");
+        }
+
+        return bean;
+    }
+
+    /**
+     * Save a Remote Browser
+     * @param {object} remoteBrowser Remote Browser to save
+     * @param {?number} remoteBrowserID ID of the Remote Browser to update
+     * @param {number} userID ID of the user who adds the Remote Browser
+     * @returns {Promise<Bean>} Updated Remote Browser
+     */
+    static async save(remoteBrowser, remoteBrowserID, userID) {
+        let bean;
+
+        if (remoteBrowserID) {
+            bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
+
+            if (!bean) {
+                throw new Error("Remote browser not found");
+            }
+
+        } else {
+            bean = R.dispense("remote_browser");
+        }
+
+        bean.user_id = userID;
+        bean.name = remoteBrowser.name;
+        bean.url = remoteBrowser.url;
+
+        await R.store(bean);
+
+        return bean;
+    }
+
+    /**
+     * Delete a Remote Browser
+     * @param {number} remoteBrowserID ID of the Remote Browser to delete
+     * @param {number} userID ID of the user who created the Remote Browser
+     * @returns {Promise<void>}
+     */
+    static async delete(remoteBrowserID, userID) {
+        let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
+
+        if (!bean) {
+            throw new Error("Remote Browser not found");
+        }
+
+        // Delete removed remote browser from monitors if exists
+        await R.exec("UPDATE monitor SET remote_browser = null WHERE remote_browser = ?", [ remoteBrowserID ]);
+
+        await R.trash(bean);
+    }
+
+    /**
+     * Tests the connection to Remote Browser
+     * @param {object} remoteBrowser Docker host to check for
+     * @returns {boolean} Returns if connection worked
+     */
+    static async test(remoteBrowser) {
+        const testResult = await testRemoteBrowser(remoteBrowser.id, remoteBrowser.user_id);
+        return testResult;
+    }
+
+}
+
+module.exports = {
+    RemoteBrowser,
+};
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 0549518f3..7b14a6dac 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -64,38 +64,57 @@ router.get("/api/push/:pushToken", async (request, response) => {
 
         const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
 
-        if (monitor.isUpsideDown()) {
-            status = flipStatus(status);
-        }
-
         let isFirstBeat = true;
-        let previousStatus = status;
-        let duration = 0;
 
         let bean = R.dispense("heartbeat");
         bean.time = R.isoDateTimeMillis(dayjs.utc());
+        bean.monitor_id = monitor.id;
+        bean.ping = ping;
+        bean.msg = msg;
+        bean.downCount = previousHeartbeat?.downCount || 0;
 
         if (previousHeartbeat) {
             isFirstBeat = false;
-            previousStatus = previousHeartbeat.status;
-            duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
+            bean.duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
         }
 
         if (await Monitor.isUnderMaintenance(monitor.id)) {
             msg = "Monitor under maintenance";
-            status = MAINTENANCE;
+            bean.status = MAINTENANCE;
+        } else {
+            determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
         }
 
-        log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
-        log.debug("router", "PreviousStatus: " + previousStatus);
-        log.debug("router", "Current Status: " + status);
+        // Calculate uptime
+        let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitor.id);
+        let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
+        bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
 
-        bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
-        bean.monitor_id = monitor.id;
-        bean.status = status;
-        bean.msg = msg;
-        bean.ping = ping;
-        bean.duration = duration;
+        log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
+        log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
+        log.debug("router", "Current Status: " + bean.status);
+
+        bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status);
+
+        if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) {
+            // Reset down count
+            bean.downCount = 0;
+
+            log.debug("monitor", `[${this.name}] sendNotification`);
+            await Monitor.sendNotification(isFirstBeat, monitor, bean);
+        } else {
+            if (bean.status === DOWN && this.resendInterval > 0) {
+                ++bean.downCount;
+                if (bean.downCount >= this.resendInterval) {
+                    // Send notification again, because we are still DOWN
+                    log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
+                    await Monitor.sendNotification(isFirstBeat, this, bean);
+
+                    // Reset down count
+                    bean.downCount = 0;
+                }
+            }
+        }
 
         await R.store(bean);
 
@@ -107,11 +126,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
         response.json({
             ok: true,
         });
-
-        if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
-            await Monitor.sendNotification(isFirstBeat, monitor, bean);
-        }
-
     } catch (e) {
         response.status(404).json({
             ok: false,
@@ -562,4 +576,58 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
     }
 });
 
+/**
+ * Determines the status of the next beat in the push route handling.
+ * @param {string} status - The reported new status.
+ * @param {object} previousHeartbeat - The previous heartbeat object.
+ * @param {number} maxretries - The maximum number of retries allowed.
+ * @param {boolean} isUpsideDown - Indicates if the monitor is upside down.
+ * @param {object} bean - The new heartbeat object.
+ * @returns {void}
+ */
+function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) {
+    if (isUpsideDown) {
+        status = flipStatus(status);
+    }
+
+    if (previousHeartbeat) {
+        if (previousHeartbeat.status === UP && status === DOWN) {
+            // Going Down
+            if ((maxretries > 0) && (previousHeartbeat.retries < maxretries)) {
+                // Retries available
+                bean.retries = previousHeartbeat.retries + 1;
+                bean.status = PENDING;
+            } else {
+                // No more retries
+                bean.retries = 0;
+                bean.status = DOWN;
+            }
+        } else if (previousHeartbeat.status === PENDING && status === DOWN && previousHeartbeat.retries < maxretries) {
+            // Retries available
+            bean.retries = previousHeartbeat.retries + 1;
+            bean.status = PENDING;
+        } else {
+            // No more retries or not pending
+            if (status === DOWN) {
+                bean.retries = previousHeartbeat.retries + 1;
+                bean.status = status;
+            } else {
+                bean.retries = 0;
+                bean.status = status;
+            }
+        }
+    } else {
+        // First beat?
+        if (status === DOWN && maxretries > 0) {
+            // Retries available
+            bean.retries = 1;
+            bean.status = PENDING;
+        } else {
+            // Retires not enabled
+            bean.retries = 0;
+            bean.status = status;
+        }
+    }
+}
+
 module.exports = router;
diff --git a/server/server.js b/server/server.js
index cde0f3814..0be9a9722 100644
--- a/server/server.js
+++ b/server/server.js
@@ -131,9 +131,10 @@ const testMode = !!args["test"] || false;
 const e2eTestMode = !!args["e2e"] || false;
 
 // Must be after io instantiation
-const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
+const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
 const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
 const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
+const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
 const TwoFA = require("./2fa");
 const StatusPage = require("./model/status_page");
 const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
@@ -827,6 +828,7 @@ let needSetup = false;
                 bean.kafkaProducerAllowAutoTopicCreation =
                     monitor.kafkaProducerAllowAutoTopicCreation;
                 bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
+                bean.remote_browser = monitor.remote_browser;
 
                 bean.validate();
 
@@ -1508,6 +1510,7 @@ let needSetup = false;
         dockerSocketHandler(socket);
         maintenanceSocketHandler(socket);
         apiKeySocketHandler(socket);
+        remoteBrowserSocketHandler(socket);
         generalSocketHandler(socket, server);
 
         log.debug("server", "added all socket handlers");
@@ -1616,6 +1619,7 @@ async function afterLogin(socket, user) {
     sendProxyList(socket);
     sendDockerHostList(socket);
     sendAPIKeyList(socket);
+    sendRemoteBrowserList(socket);
 
     await sleep(500);
 
diff --git a/server/socket-handlers/remote-browser-socket-handler.js b/server/socket-handlers/remote-browser-socket-handler.js
new file mode 100644
index 000000000..ae53030ec
--- /dev/null
+++ b/server/socket-handlers/remote-browser-socket-handler.js
@@ -0,0 +1,82 @@
+const { sendRemoteBrowserList } = require("../client");
+const { checkLogin } = require("../util-server");
+const { RemoteBrowser } = require("../remote-browser");
+
+const { log } = require("../../src/util");
+const { testRemoteBrowser } = require("../monitor-types/real-browser-monitor-type");
+
+/**
+ * Handlers for docker hosts
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.remoteBrowserSocketHandler = (socket) => {
+    socket.on("addRemoteBrowser", async (remoteBrowser, remoteBrowserID, callback) => {
+        try {
+            checkLogin(socket);
+
+            let remoteBrowserBean = await RemoteBrowser.save(remoteBrowser, remoteBrowserID, socket.userID);
+            await sendRemoteBrowserList(socket);
+
+            callback({
+                ok: true,
+                msg: "Saved.",
+                msgi18n: true,
+                id: remoteBrowserBean.id,
+            });
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("deleteRemoteBrowser", async (dockerHostID, callback) => {
+        try {
+            checkLogin(socket);
+
+            await RemoteBrowser.delete(dockerHostID, socket.userID);
+            await sendRemoteBrowserList(socket);
+
+            callback({
+                ok: true,
+                msg: "successDeleted",
+                msgi18n: true,
+            });
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("testRemoteBrowser", async (remoteBrowser, callback) => {
+        try {
+            checkLogin(socket);
+            let check = await testRemoteBrowser(remoteBrowser.url);
+            log.info("remoteBrowser", "Tested remote browser: " + check);
+            let msg;
+
+            if (check) {
+                msg = "Connected Successfully.";
+            }
+
+            callback({
+                ok: true,
+                msg,
+            });
+
+        } catch (e) {
+            log.error("remoteBrowser", e);
+
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+};
diff --git a/src/components/RemoteBrowserDialog.vue b/src/components/RemoteBrowserDialog.vue
new file mode 100644
index 000000000..941ab8f7d
--- /dev/null
+++ b/src/components/RemoteBrowserDialog.vue
@@ -0,0 +1,185 @@
+<template>
+    <form @submit.prevent="submit">
+        <div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
+            <div class="modal-dialog">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 id="exampleModalLabel" class="modal-title">
+                            {{ $t("Add a Remote Browser") }}
+                        </h5>
+                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
+                    </div>
+                    <div class="modal-body">
+                        <div class="mb-3">
+                            <label for="remote-browser-name" class="form-label">{{ $t("Friendly Name") }}</label>
+                            <input id="remote-browser-name" v-model="remoteBrowser.name" type="text" class="form-control" required>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="remote-browser-url" class="form-label">{{ $t("URL") }}</label>
+                            <input id="remote-browser-url" v-model="remoteBrowser.url" type="text" class="form-control" required>
+
+                            <div class="form-text mt-3">
+                                {{ $t("Examples") }}:
+                                <ul>
+                                    <li>ws://chrome.browserless.io/playwright?token=YOUR-API-TOKEN</li>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="modal-footer">
+                        <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
+                            {{ $t("Delete") }}
+                        </button>
+                        <button type="button" class="btn btn-warning" :disabled="processing" @click="test">
+                            {{ $t("Test") }}
+                        </button>
+                        <button type="submit" class="btn btn-primary" :disabled="processing">
+                            <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
+                            {{ $t("Save") }}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+
+    <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
+        {{ $t("deleteRemoteBrowserMessage") }}
+    </Confirm>
+</template>
+
+<script>
+import { Modal } from "bootstrap";
+import Confirm from "./Confirm.vue";
+
+export default {
+    components: {
+        Confirm,
+    },
+    props: {},
+    emits: [ "added" ],
+    data() {
+        return {
+            modal: null,
+            processing: false,
+            id: null,
+            remoteBrowser: {
+                name: "",
+                url: "",
+                // Do not set default value here, please scroll to show()
+            }
+        };
+    },
+
+    mounted() {
+        this.modal = new Modal(this.$refs.modal);
+    },
+    methods: {
+
+        /**
+         * Confirm deletion of docker host
+         * @returns {void}
+         */
+        deleteConfirm() {
+            this.modal.hide();
+            this.$refs.confirmDelete.show();
+        },
+
+        /**
+         * Show specified docker host
+         * @param {number} remoteBrowserID ID of host to show
+         * @returns {void}
+         */
+        show(remoteBrowserID) {
+            if (remoteBrowserID) {
+                let found = false;
+
+                this.id = remoteBrowserID;
+
+                for (let n of this.$root.remoteBrowserList) {
+                    if (n.id === remoteBrowserID) {
+                        this.remoteBrowser = n;
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    this.$root.toastError(this.$t("Remote Browser not found!"));
+                }
+
+            } else {
+                this.id = null;
+                this.remoteBrowser = {
+                    name: "",
+                    url: "",
+                };
+            }
+
+            this.modal.show();
+        },
+
+        /**
+         * Add docker host
+         * @returns {void}
+         */
+        submit() {
+            this.processing = true;
+            this.$root.getSocket().emit("addRemoteBrowser", this.remoteBrowser, this.id, (res) => {
+                this.$root.toastRes(res);
+                this.processing = false;
+
+                if (res.ok) {
+                    this.modal.hide();
+
+                    // Emit added event, doesn't emit edit.
+                    if (! this.id) {
+                        this.$emit("added", res.id);
+                    }
+
+                }
+            });
+        },
+
+        /**
+         * Test the docker host
+         * @returns {void}
+         */
+        test() {
+            this.processing = true;
+            this.$root.getSocket().emit("testRemoteBrowser", this.remoteBrowser, (res) => {
+                this.$root.toastRes(res);
+                this.processing = false;
+            });
+        },
+
+        /**
+         * Delete this docker host
+         * @returns {void}
+         */
+        deleteDockerHost() {
+            this.processing = true;
+            this.$root.getSocket().emit("deleteRemoteBrowser", this.id, (res) => {
+                this.$root.toastRes(res);
+                this.processing = false;
+
+                if (res.ok) {
+                    this.modal.hide();
+                }
+            });
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/vars.scss";
+
+.dark {
+    .modal-dialog .form-text, .modal-dialog p {
+        color: $dark-font-color;
+    }
+}
+</style>
diff --git a/src/components/settings/RemoteBrowsers.vue b/src/components/settings/RemoteBrowsers.vue
new file mode 100644
index 000000000..b449ac63a
--- /dev/null
+++ b/src/components/settings/RemoteBrowsers.vue
@@ -0,0 +1,53 @@
+<template>
+    <div>
+        <div class="dockerHost-list my-4">
+            <p v-if="$root.remoteBrowserList.length === 0">
+                {{ $t("Not available, please setup.") }}
+            </p>
+
+            <ul class="list-group mb-3" style="border-radius: 1rem;">
+                <li v-for="(remoteBrowser, index) in $root.remoteBrowserList" :key="index" class="list-group-item">
+                    {{ remoteBrowser.name }}<br>
+                    <a href="#" @click="$refs.remoteBrowserDialog.show(remoteBrowser.id)">{{ $t("Edit") }}</a>
+                </li>
+            </ul>
+
+            <button class="btn btn-primary me-2" type="button" @click="$refs.remoteBrowserDialog.show()">
+                <font-awesome-icon icon="plus" /> {{ $t("Add Remote Browser") }}
+            </button>
+        </div>
+
+        <div class="my-4 pt-4">
+            <h5 class="my-4 settings-subheading">{{ $t("What is a Remote Browser?") }}</h5>
+            <p>{{ $t("remoteBrowsersDescription") }} <a href="https://hub.docker.com/r/browserless/chrome">{{ $t("self-hosted container") }}</a></p>
+        </div>
+
+        <RemoteBrowserDialog ref="remoteBrowserDialog" />
+    </div>
+</template>
+
+<script>
+import RemoteBrowserDialog from "../../components/RemoteBrowserDialog.vue";
+
+export default {
+    components: {
+        RemoteBrowserDialog,
+    },
+
+    data() {
+        return {};
+    },
+
+    computed: {
+        settings() {
+            return this.$parent.$parent.$parent.settings;
+        },
+        saveSettings() {
+            return this.$parent.$parent.$parent.saveSettings;
+        },
+        settingsLoaded() {
+            return this.$parent.$parent.$parent.settingsLoaded;
+        },
+    }
+};
+</script>
diff --git a/src/lang/en.json b/src/lang/en.json
index a25d9196d..bcf9e220d 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -865,6 +865,15 @@
     "successEnabled": "Enabled Successfully.",
     "tagNotFound": "Tag not found.",
     "foundChromiumVersion": "Found Chromium/Chrome. Version: {0}",
+    "Remote Browsers": "Remote Browsers",
+    "Remote Browser": "Remote Browser",
+    "Add a Remote Browser": "Add a Remote Browser",
+    "Remote Browser not found!": "Remote Browser not found!",
+    "remoteBrowsersDescription": "Remote Browsers are an alternative to running Chromium locally. Setup with a service like browserless.io or connect to your own",
+    "self-hosted container": "self-hosted container",
+    "remoteBrowserToggle": "By default Chromium runs inside the Uptime Kuma container. You can use a remote browser by toggling this switch.",
+    "useRemoteBrowser": "Use a Remote Browser",
+    "deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
     "GrafanaOncallUrl": "Grafana Oncall URL",
     "Browser Screenshot": "Browser Screenshot"
 }
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index bbb06658e..a6338742e 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -46,6 +46,7 @@ export default {
             tlsInfoList: {},
             notificationList: [],
             dockerHostList: [],
+            remoteBrowserList: [],
             statusPageListLoaded: false,
             statusPageList: [],
             proxyList: [],
@@ -174,6 +175,10 @@ export default {
                 this.dockerHostList = data;
             });
 
+            socket.on("remoteBrowserList", (data) => {
+                this.remoteBrowserList = data;
+            });
+
             socket.on("heartbeat", (data) => {
                 if (! (data.monitorID in this.heartbeatList)) {
                     this.heartbeatList[data.monitorID] = [];
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 415d43f01..490e55ab1 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -144,6 +144,30 @@
                                 </div>
                             </div>
 
+                            <!-- Remote Browser -->
+                            <div v-if="monitor.type === 'real-browser'" class="my-3">
+                                <!-- Toggle -->
+                                <div class="my-3 form-check">
+                                    <input id="toggle" v-model="remoteBrowsersToggle" class="form-check-input" type="checkbox">
+                                    <label class="form-check-label" for="toggle">
+                                        {{ $t("useRemoteBrowser") }}
+                                    </label>
+                                    <div class="form-text">
+                                        {{ $t("remoteBrowserToggle") }}
+                                    </div>
+                                </div>
+
+                                <div v-if="remoteBrowsersToggle">
+                                    <label for="remote-browser" class="form-label">{{ $t("Remote Browser") }}</label>
+                                    <ActionSelect
+                                        v-model="monitor.remote_browser"
+                                        :options="remoteBrowsersOptions"
+                                        icon="plus"
+                                        :action="() => $refs.remoteBrowserDialog.show()"
+                                    />
+                                </div>
+                            </div>
+
                             <!-- Json Query -->
                             <div v-if="monitor.type === 'json-query'" class="my-3">
                                 <label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
@@ -838,6 +862,7 @@
             <DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
             <ProxyDialog ref="proxyDialog" @added="addedProxy" />
             <CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
+            <RemoteBrowserDialog ref="remoteBrowserDialog" @added="addedRemoteBrowser" />
         </div>
     </transition>
 </template>
@@ -850,6 +875,7 @@ import CopyableInput from "../components/CopyableInput.vue";
 import CreateGroupDialog from "../components/CreateGroupDialog.vue";
 import NotificationDialog from "../components/NotificationDialog.vue";
 import DockerHostDialog from "../components/DockerHostDialog.vue";
+import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
 import ProxyDialog from "../components/ProxyDialog.vue";
 import TagsManager from "../components/TagsManager.vue";
 import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
@@ -898,6 +924,7 @@ const monitorDefaults = {
     kafkaProducerSsl: false,
     kafkaProducerAllowAutoTopicCreation: false,
     gamedigGivenPortOnly: true,
+    remote_browser: null
 };
 
 export default {
@@ -909,6 +936,7 @@ export default {
         CreateGroupDialog,
         NotificationDialog,
         DockerHostDialog,
+        RemoteBrowserDialog,
         TagsManager,
         VueMultiselect,
     },
@@ -936,6 +964,7 @@ export default {
                 "mongodb": "mongodb://username:password@host:port/database",
             },
             draftGroupName: null,
+            remoteBrowsersEnabled: false,
         };
     },
 
@@ -959,7 +988,31 @@ export default {
             }
             return this.$t(name);
         },
-
+        remoteBrowsersOptions() {
+            return this.$root.remoteBrowserList.map(browser => {
+                return {
+                    label: browser.name,
+                    value: browser.id,
+                };
+            });
+        },
+        remoteBrowsersToggle: {
+            get() {
+                return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
+            },
+            set(value) {
+                if (value) {
+                    this.remoteBrowsersEnabled = true;
+                    if (this.monitor.remote_browser == null && this.$root.remoteBrowserList.length > 0) {
+                        // set a default remote browser if there is one. Otherwise, the user will have to select one manually.
+                        this.monitor.remote_browser = this.$root.remoteBrowserList[0].id;
+                    }
+                } else {
+                    this.remoteBrowsersEnabled = false;
+                    this.monitor.remote_browser = null;
+                }
+            }
+        },
         isAdd() {
             return this.$route.path === "/add";
         },
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index 4598dfb61..3da1ed9a0 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -104,6 +104,9 @@ export default {
                 "docker-hosts": {
                     title: this.$t("Docker Hosts"),
                 },
+                "remote-browsers": {
+                    title: this.$t("Remote Browsers"),
+                },
                 security: {
                     title: this.$t("Security"),
                 },
diff --git a/src/router.js b/src/router.js
index 0ceb139f9..36cdeadae 100644
--- a/src/router.js
+++ b/src/router.js
@@ -31,6 +31,7 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
 const Security = () => import("./components/settings/Security.vue");
 import Proxies from "./components/settings/Proxies.vue";
 import About from "./components/settings/About.vue";
+import RemoteBrowsers from "./components/settings/RemoteBrowsers.vue";
 
 const routes = [
     {
@@ -113,6 +114,10 @@ const routes = [
                                 path: "docker-hosts",
                                 component: DockerHosts,
                             },
+                            {
+                                path: "remote-browsers",
+                                component: RemoteBrowsers,
+                            },
                             {
                                 path: "security",
                                 component: Security,