diff --git a/README.md b/README.md
index ab42a5599..026f3b4d9 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
 
 ## ⭐ Features
 
-* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
+* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
 * Fancy, Reactive, Fast UI/UX.
 * Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
 * 20 second intervals.
diff --git a/db/patch-add-docker-columns.sql b/db/patch-add-docker-columns.sql
new file mode 100644
index 000000000..4cea448d7
--- /dev/null
+++ b/db/patch-add-docker-columns.sql
@@ -0,0 +1,18 @@
+-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
+BEGIN TRANSACTION;
+
+CREATE TABLE docker_host (
+	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+	user_id INT NOT NULL,
+	docker_daemon VARCHAR(255),
+	docker_type VARCHAR(255),
+	name VARCHAR(255)
+);
+
+ALTER TABLE monitor
+	ADD docker_host INTEGER REFERENCES docker_host(id);
+
+ALTER TABLE monitor
+	ADD docker_container VARCHAR(255);
+
+COMMIT;
diff --git a/db/patch-add-radius-monitor.sql b/db/patch-add-radius-monitor.sql
new file mode 100644
index 000000000..1fd5b44f4
--- /dev/null
+++ b/db/patch-add-radius-monitor.sql
@@ -0,0 +1,18 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE monitor
+    ADD radius_username VARCHAR(255);
+
+ALTER TABLE monitor
+    ADD radius_password VARCHAR(255);
+
+ALTER TABLE monitor
+    ADD radius_calling_station_id VARCHAR(50);
+
+ALTER TABLE monitor
+    ADD radius_called_station_id VARCHAR(50);
+
+ALTER TABLE monitor
+    ADD radius_secret VARCHAR(255);
+
+COMMIT
diff --git a/db/patch-monitor-add-resend-interval.sql b/db/patch-monitor-add-resend-interval.sql
new file mode 100644
index 000000000..8e28bf693
--- /dev/null
+++ b/db/patch-monitor-add-resend-interval.sql
@@ -0,0 +1,10 @@
+-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
+BEGIN TRANSACTION;
+
+ALTER TABLE monitor
+    ADD resend_interval INTEGER default 0 not null;
+
+ALTER TABLE heartbeat
+    ADD down_count INTEGER default 0 not null;
+
+COMMIT;
diff --git a/docker/alpine-base.dockerfile b/docker/alpine-base.dockerfile
index cde65bb64..1d74de05d 100644
--- a/docker/alpine-base.dockerfile
+++ b/docker/alpine-base.dockerfile
@@ -4,5 +4,5 @@ WORKDIR /app
 
 # Install apprise, iputils for non-root ping, setpriv
 RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
-    pip3 --no-cache-dir install apprise==0.9.9 && \
+    pip3 --no-cache-dir install apprise==1.0.0 && \
     rm -rf /root/.cache
diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile
index f90968a8b..20bef3dd4 100644
--- a/docker/debian-base.dockerfile
+++ b/docker/debian-base.dockerfile
@@ -11,7 +11,7 @@ WORKDIR /app
 RUN apt update && \
     apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
         sqlite3 iputils-ping util-linux dumb-init && \
-    pip3 --no-cache-dir install apprise==0.9.9 && \
+    pip3 --no-cache-dir install apprise==1.0.0 && \
     rm -rf /var/lib/apt/lists/* && \
     apt --yes autoremove
 
diff --git a/package-lock.json b/package-lock.json
index 778e6bc38..0cf62fa7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -39,6 +39,7 @@
                 "mqtt": "^4.2.8",
                 "mssql": "^8.1.0",
                 "node-cloudflared-tunnel": "~1.0.9",
+                "node-radius-client": "^1.0.0",
                 "nodemailer": "~6.6.5",
                 "notp": "~2.0.3",
                 "password-hash": "~1.2.2",
@@ -8215,6 +8216,12 @@
                 "readable-stream": "^3.6.0"
             }
         },
+        "node_modules/hoek": {
+            "version": "6.1.3",
+            "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz",
+            "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==",
+            "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues."
+        },
         "node_modules/homedir-polyfill": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -8915,6 +8922,17 @@
             "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
             "devOptional": true
         },
+        "node_modules/isemail": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
+            "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
+            "dependencies": {
+                "punycode": "2.x.x"
+            },
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
         "node_modules/isexe": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -12151,6 +12169,32 @@
             "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
             "dev": true
         },
+        "node_modules/node-radius-client": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz",
+            "integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==",
+            "dependencies": {
+                "joi": "^14.3.1",
+                "node-radius-utils": "^1.2.0",
+                "radius": "^1.1.4"
+            }
+        },
+        "node_modules/node-radius-client/node_modules/joi": {
+            "version": "14.3.1",
+            "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz",
+            "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==",
+            "deprecated": "This module has moved and is now available at @hapi/joi. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.",
+            "dependencies": {
+                "hoek": "6.x.x",
+                "isemail": "3.x.x",
+                "topo": "3.x.x"
+            }
+        },
+        "node_modules/node-radius-utils": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz",
+            "integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw=="
+        },
         "node_modules/node-releases": {
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
@@ -13429,6 +13473,14 @@
                 "node": ">=8"
             }
         },
+        "node_modules/radius": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz",
+            "integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw==",
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
         "node_modules/range-parser": {
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -15261,6 +15313,15 @@
                 "node": ">=0.6"
             }
         },
+        "node_modules/topo": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
+            "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
+            "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.",
+            "dependencies": {
+                "hoek": "6.x.x"
+            }
+        },
         "node_modules/toposort": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
@@ -22641,6 +22702,11 @@
                 "readable-stream": "^3.6.0"
             }
         },
+        "hoek": {
+            "version": "6.1.3",
+            "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz",
+            "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ=="
+        },
         "homedir-polyfill": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -23123,6 +23189,14 @@
             "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
             "devOptional": true
         },
+        "isemail": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
+            "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
+            "requires": {
+                "punycode": "2.x.x"
+            }
+        },
         "isexe": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -25618,6 +25692,33 @@
             "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
             "dev": true
         },
+        "node-radius-client": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz",
+            "integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==",
+            "requires": {
+                "joi": "^14.3.1",
+                "node-radius-utils": "^1.2.0",
+                "radius": "^1.1.4"
+            },
+            "dependencies": {
+                "joi": {
+                    "version": "14.3.1",
+                    "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz",
+                    "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==",
+                    "requires": {
+                        "hoek": "6.x.x",
+                        "isemail": "3.x.x",
+                        "topo": "3.x.x"
+                    }
+                }
+            }
+        },
+        "node-radius-utils": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz",
+            "integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw=="
+        },
         "node-releases": {
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
@@ -26532,6 +26633,11 @@
             "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
             "dev": true
         },
+        "radius": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz",
+            "integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw=="
+        },
         "range-parser": {
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -27967,6 +28073,14 @@
             "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
             "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
         },
+        "topo": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
+            "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
+            "requires": {
+                "hoek": "6.x.x"
+            }
+        },
         "toposort": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
diff --git a/package.json b/package.json
index ea6c5a791..981ca1912 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "uptime-kuma",
-    "version": "1.17.1",
+    "version": "1.18.0-beta.0",
     "license": "MIT",
     "repository": {
         "type": "git",
@@ -91,6 +91,7 @@
         "mqtt": "^4.2.8",
         "mssql": "^8.1.0",
         "node-cloudflared-tunnel": "~1.0.9",
+        "node-radius-client": "^1.0.0",
         "nodemailer": "~6.6.5",
         "notp": "~2.0.3",
         "password-hash": "~1.2.2",
diff --git a/server/client.js b/server/client.js
index 279acd3a6..a0c52e1e4 100644
--- a/server/client.js
+++ b/server/client.js
@@ -125,10 +125,35 @@ async function sendInfo(socket) {
     });
 }
 
+/**
+ * Send list of docker hosts to client
+ * @param {Socket} socket Socket.io socket instance
+ * @returns {Promise<Bean[]>}
+ */
+async function sendDockerHostList(socket) {
+    const timeLogger = new TimeLogger();
+
+    let result = [];
+    let list = await R.find("docker_host", " user_id = ? ", [
+        socket.userID,
+    ]);
+
+    for (let bean of list) {
+        result.push(bean.toJSON());
+    }
+
+    io.to(socket.userID).emit("dockerHostList", result);
+
+    timeLogger.print("Send Docker Host List");
+
+    return list;
+}
+
 module.exports = {
     sendNotificationList,
     sendImportantHeartbeatList,
     sendHeartbeatList,
     sendProxyList,
     sendInfo,
+    sendDockerHostList
 };
diff --git a/server/database.js b/server/database.js
index 00fd48d9f..b1a23a475 100644
--- a/server/database.js
+++ b/server/database.js
@@ -53,6 +53,7 @@ class Database {
         "patch-2fa-invalidate-used-token.sql": true,
         "patch-notification_sent_history.sql": true,
         "patch-monitor-basic-auth.sql": true,
+        "patch-add-docker-columns.sql": true,
         "patch-status-page.sql": true,
         "patch-proxy.sql": true,
         "patch-monitor-expiry-notification.sql": true,
@@ -61,6 +62,8 @@ class Database {
         "patch-add-clickable-status-page-link.sql": true,
         "patch-add-sqlserver-monitor.sql": true,
         "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
+        "patch-add-radius-monitor.sql": true,
+        "patch-monitor-add-resend-interval.sql": true,
     };
 
     /**
@@ -147,6 +150,9 @@ class Database {
         await R.exec("PRAGMA cache_size = -12000");
         await R.exec("PRAGMA auto_vacuum = FULL");
 
+        // Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
+        await R.exec("PRAGMA busy_timeout = 5000");
+
         // This ensures that an operating system crash or power failure will not corrupt the database.
         // FULL synchronous is very safe, but it is also slower.
         // Read more: https://sqlite.org/pragma.html#pragma_synchronous
diff --git a/server/docker.js b/server/docker.js
new file mode 100644
index 000000000..177fa6cb6
--- /dev/null
+++ b/server/docker.js
@@ -0,0 +1,106 @@
+const axios = require("axios");
+const { R } = require("redbean-node");
+const version = require("../package.json").version;
+const https = require("https");
+
+class DockerHost {
+    /**
+     * Save a docker host
+     * @param {Object} dockerHost Docker host to save
+     * @param {?number} dockerHostID ID of the docker host to update
+     * @param {number} userID ID of the user who adds the docker host
+     * @returns {Promise<Bean>}
+     */
+    static async save(dockerHost, dockerHostID, userID) {
+        let bean;
+
+        if (dockerHostID) {
+            bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
+
+            if (!bean) {
+                throw new Error("docker host not found");
+            }
+
+        } else {
+            bean = R.dispense("docker_host");
+        }
+
+        bean.user_id = userID;
+        bean.docker_daemon = dockerHost.dockerDaemon;
+        bean.docker_type = dockerHost.dockerType;
+        bean.name = dockerHost.name;
+
+        await R.store(bean);
+
+        return bean;
+    }
+
+    /**
+     * Delete a Docker host
+     * @param {number} dockerHostID ID of the Docker host to delete
+     * @param {number} userID ID of the user who created the Docker host
+     * @returns {Promise<void>}
+     */
+    static async delete(dockerHostID, userID) {
+        let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
+
+        if (!bean) {
+            throw new Error("docker host not found");
+        }
+
+        // Delete removed proxy from monitors if exists
+        await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
+
+        await R.trash(bean);
+    }
+
+    /**
+     * Fetches the amount of containers on the Docker host
+     * @param {Object} dockerHost Docker host to check for
+     * @returns {number} Total amount of containers on the host
+     */
+    static async testDockerHost(dockerHost) {
+        const options = {
+            url: "/containers/json?all=true",
+            headers: {
+                "Accept": "*/*",
+                "User-Agent": "Uptime-Kuma/" + version
+            },
+            httpsAgent: new https.Agent({
+                maxCachedSessions: 0,      // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
+                rejectUnauthorized: false,
+            }),
+        };
+
+        if (dockerHost.dockerType === "socket") {
+            options.socketPath = dockerHost.dockerDaemon;
+        } else if (dockerHost.dockerType === "tcp") {
+            options.baseURL = dockerHost.dockerDaemon;
+        }
+
+        let res = await axios.request(options);
+
+        if (Array.isArray(res.data)) {
+
+            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?");
+                }
+
+            } else {
+                return res.data.length;
+            }
+
+        } else {
+            throw new Error("Invalid Docker response, is it Docker really a daemon?");
+        }
+
+    }
+}
+
+module.exports = {
+    DockerHost,
+};
diff --git a/server/model/docker_host.js b/server/model/docker_host.js
new file mode 100644
index 000000000..205982922
--- /dev/null
+++ b/server/model/docker_host.js
@@ -0,0 +1,19 @@
+const { BeanModel } = require("redbean-node/dist/bean-model");
+
+class DockerHost extends BeanModel {
+    /**
+     * Returns an object that ready to parse to JSON
+     * @returns {Object}
+     */
+    toJSON() {
+        return {
+            id: this.id,
+            userID: this.user_id,
+            dockerDaemon: this.docker_daemon,
+            dockerType: this.docker_type,
+            name: this.name,
+        };
+    }
+}
+
+module.exports = DockerHost;
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 81149b52a..f96b4df06 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -7,7 +7,7 @@ dayjs.extend(timezone);
 const axios = require("axios");
 const { Prometheus } = require("../prometheus");
 const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
-const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
+const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
 const { R } = require("redbean-node");
 const { BeanModel } = require("redbean-node/dist/bean-model");
 const { Notification } = require("../notification");
@@ -79,6 +79,7 @@ class Monitor extends BeanModel {
             type: this.type,
             interval: this.interval,
             retryInterval: this.retryInterval,
+            resendInterval: this.resendInterval,
             keyword: this.keyword,
             expiryNotification: this.isEnabledExpiryNotification(),
             ignoreTls: this.getIgnoreTls(),
@@ -88,6 +89,9 @@ class Monitor extends BeanModel {
             dns_resolve_type: this.dns_resolve_type,
             dns_resolve_server: this.dns_resolve_server,
             dns_last_result: this.dns_last_result,
+            pushToken: this.pushToken,
+            docker_container: this.docker_container,
+            docker_host: this.docker_host,
             proxyId: this.proxy_id,
             notificationIDList,
             tags: tags,
@@ -100,6 +104,11 @@ class Monitor extends BeanModel {
             authMethod: this.authMethod,
             authWorkstation: this.authWorkstation,
             authDomain: this.authDomain,
+            radiusUsername: this.radiusUsername,
+            radiusPassword: this.radiusPassword,
+            radiusCalledStationId: this.radiusCalledStationId,
+            radiusCallingStationId: this.radiusCallingStationId,
+            radiusSecret: this.radiusSecret,
         };
 
         if (includeSensitiveData) {
@@ -206,6 +215,7 @@ class Monitor extends BeanModel {
             bean.monitor_id = this.id;
             bean.time = R.isoDateTimeMillis(dayjs.utc());
             bean.status = DOWN;
+            bean.downCount = previousBeat?.downCount || 0;
 
             if (this.isUpsideDown()) {
                 bean.status = flipStatus(bean.status);
@@ -468,6 +478,35 @@ class Monitor extends BeanModel {
                     } else {
                         throw new Error("Server not found on Steam");
                     }
+                } else if (this.type === "docker") {
+                    log.debug(`[${this.name}] Prepare Options for Axios`);
+
+                    const dockerHost = await R.load("docker_host", this.docker_host);
+
+                    const options = {
+                        url: `/containers/${this.docker_container}/json`,
+                        headers: {
+                            "Accept": "*/*",
+                            "User-Agent": "Uptime-Kuma/" + version,
+                        },
+                        httpsAgent: new https.Agent({
+                            maxCachedSessions: 0,      // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
+                            rejectUnauthorized: ! this.getIgnoreTls(),
+                        }),
+                    };
+
+                    if (dockerHost._dockerType === "socket") {
+                        options.socketPath = dockerHost._dockerDaemon;
+                    } else if (dockerHost._dockerType === "tcp") {
+                        options.baseURL = dockerHost._dockerDaemon;
+                    }
+
+                    log.debug(`[${this.name}] Axios Request`);
+                    let res = await axios.request(options);
+                    if (res.data.State.Running) {
+                        bean.status = UP;
+                        bean.msg = "";
+                    }
                 } else if (this.type === "mqtt") {
                     bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
                         port: this.port,
@@ -492,6 +531,30 @@ class Monitor extends BeanModel {
                     bean.msg = "";
                     bean.status = UP;
                     bean.ping = dayjs().valueOf() - startTime;
+                } else if (this.type === "radius") {
+                    let startTime = dayjs().valueOf();
+                    try {
+                        const resp = await radius(
+                            this.hostname,
+                            this.radiusUsername,
+                            this.radiusPassword,
+                            this.radiusCalledStationId,
+                            this.radiusCallingStationId,
+                            this.radiusSecret
+                        );
+                        if (resp.code) {
+                            bean.msg = resp.code;
+                        }
+                        bean.status = UP;
+                    } catch (error) {
+                        bean.status = DOWN;
+                        if (error.response?.code) {
+                            bean.msg = error.response.code;
+                        } else {
+                            bean.msg = error.message;
+                        }
+                    }
+                    bean.ping = dayjs().valueOf() - startTime;
                 } else {
                     bean.msg = "Unknown Monitor Type";
                     bean.status = PENDING;
@@ -533,12 +596,27 @@ class Monitor extends BeanModel {
                 log.debug("monitor", `[${this.name}] sendNotification`);
                 await Monitor.sendNotification(isFirstBeat, this, bean);
 
+                // Reset down count
+                bean.downCount = 0;
+
                 // Clear Status Page Cache
                 log.debug("monitor", `[${this.name}] apicache clear`);
                 apicache.clear();
 
             } else {
                 bean.important = false;
+
+                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;
+                    }
+                }
             }
 
             if (bean.status === UP) {
@@ -549,7 +627,7 @@ class Monitor extends BeanModel {
                 }
                 log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
             } else {
-                log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
+                log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
             }
 
             log.debug("monitor", `[${this.name}] Send to socket`);
diff --git a/server/notification-providers/bark.js b/server/notification-providers/bark.js
index 092511d87..3258e7c52 100644
--- a/server/notification-providers/bark.js
+++ b/server/notification-providers/bark.js
@@ -12,9 +12,7 @@ const { default: axios } = require("axios");
 
 // bark is an APN bridge that sends notifications to Apple devices.
 
-const barkNotificationGroup = "UptimeKuma";
 const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
-const barkNotificationSound = "telegraph";
 const successMessage = "Successes!";
 
 class Bark extends NotificationProvider {
@@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
      * @param {string} postUrl URL to append parameters to
      * @returns {string}
      */
-    appendAdditionalParameters(postUrl) {
-        // grouping all our notifications
-        postUrl += "?group=" + barkNotificationGroup;
+    appendAdditionalParameters(notification, postUrl) {
         // set icon to uptime kuma icon, 11kb should be fine
         postUrl += "&icon=" + barkNotificationAvatar;
+        // grouping all our notifications
+        if (notification.barkGroup != null) {
+            postUrl += "&group=" + notification.barkGroup;
+        } else {
+            // default name
+            postUrl += "&group=" + "UptimeKuma";
+        }
         // picked a sound, this should follow system's mute status when arrival
-        postUrl += "&sound=" + barkNotificationSound;
+        if (notification.barkSound != null) {
+            postUrl += "&sound=" + notification.barkSound;
+        } else {
+            // default sound
+            postUrl += "&sound=" + "telegraph";
+        }
         return postUrl;
     }
 
diff --git a/server/notification-providers/home-assistant.js b/server/notification-providers/home-assistant.js
new file mode 100644
index 000000000..285989eeb
--- /dev/null
+++ b/server/notification-providers/home-assistant.js
@@ -0,0 +1,38 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+const defaultNotificationService = "notify";
+
+class HomeAssistant extends NotificationProvider {
+    name = "HomeAssistant";
+
+    async send(notification, message, monitor = null, heartbeat = null) {
+        const notificationService = notification?.notificationService || defaultNotificationService;
+
+        try {
+            await axios.post(
+                `${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
+                {
+                    title: "Uptime Kuma",
+                    message,
+                    ...(notificationService !== "persistent_notification" && { data: {
+                        name: monitor?.name,
+                        status: heartbeat?.status,
+                    } }),
+                },
+                {
+                    headers: {
+                        Authorization: `Bearer ${notification.longLivedAccessToken}`,
+                        "Content-Type": "application/json",
+                    },
+                }
+            );
+
+            return "Sent Successfully.";
+        } catch (error) {
+            this.throwGeneralAxiosError(error);
+        }
+    }
+}
+
+module.exports = HomeAssistant;
diff --git a/server/notification.js b/server/notification.js
index ad1c8705a..8093572a1 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -12,6 +12,7 @@ const Feishu = require("./notification-providers/feishu");
 const GoogleChat = require("./notification-providers/google-chat");
 const Gorush = require("./notification-providers/gorush");
 const Gotify = require("./notification-providers/gotify");
+const HomeAssistant = require("./notification-providers/home-assistant");
 const Line = require("./notification-providers/line");
 const LineNotify = require("./notification-providers/linenotify");
 const LunaSea = require("./notification-providers/lunasea");
@@ -61,6 +62,7 @@ class Notification {
             new GoogleChat(),
             new Gorush(),
             new Gotify(),
+            new HomeAssistant(),
             new Line(),
             new LineNotify(),
             new LunaSea(),
diff --git a/server/server.js b/server/server.js
index 61bd9d93a..818bd7d12 100644
--- a/server/server.js
+++ b/server/server.js
@@ -118,13 +118,14 @@ if (config.demoMode) {
 }
 
 // Must be after io instantiation
-const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
+const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
 const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
 const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
 const TwoFA = require("./2fa");
 const StatusPage = require("./model/status_page");
 const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
 const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
+const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
 
 app.use(express.json());
 
@@ -668,6 +669,7 @@ let needSetup = false;
                 bean.basic_auth_pass = monitor.basic_auth_pass;
                 bean.interval = monitor.interval;
                 bean.retryInterval = monitor.retryInterval;
+                bean.resendInterval = monitor.resendInterval;
                 bean.hostname = monitor.hostname;
                 bean.maxretries = monitor.maxretries;
                 bean.port = parseInt(monitor.port);
@@ -680,6 +682,8 @@ let needSetup = false;
                 bean.dns_resolve_type = monitor.dns_resolve_type;
                 bean.dns_resolve_server = monitor.dns_resolve_server;
                 bean.pushToken = monitor.pushToken;
+                bean.docker_container = monitor.docker_container;
+                bean.docker_host = monitor.docker_host;
                 bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
                 bean.mqttUsername = monitor.mqttUsername;
                 bean.mqttPassword = monitor.mqttPassword;
@@ -690,6 +694,11 @@ let needSetup = false;
                 bean.authMethod = monitor.authMethod;
                 bean.authWorkstation = monitor.authWorkstation;
                 bean.authDomain = monitor.authDomain;
+                bean.radiusUsername = monitor.radiusUsername;
+                bean.radiusPassword = monitor.radiusPassword;
+                bean.radiusCalledStationId = monitor.radiusCalledStationId;
+                bean.radiusCallingStationId = monitor.radiusCallingStationId;
+                bean.radiusSecret = monitor.radiusSecret;
 
                 await R.store(bean);
 
@@ -1270,6 +1279,7 @@ let needSetup = false;
                                 authDomain: monitorListData[i].authDomain,
                                 interval: monitorListData[i].interval,
                                 retryInterval: retryInterval,
+                                resendInterval: monitorListData[i].resendInterval || 0,
                                 hostname: monitorListData[i].hostname,
                                 maxretries: monitorListData[i].maxretries,
                                 port: monitorListData[i].port,
@@ -1438,6 +1448,7 @@ let needSetup = false;
         cloudflaredSocketHandler(socket);
         databaseSocketHandler(socket);
         proxySocketHandler(socket);
+        dockerSocketHandler(socket);
 
         log.debug("server", "added all socket handlers");
 
@@ -1538,6 +1549,7 @@ async function afterLogin(socket, user) {
     let monitorList = await server.sendMonitorList(socket);
     sendNotificationList(socket);
     sendProxyList(socket);
+    sendDockerHostList(socket);
 
     await sleep(500);
 
diff --git a/server/socket-handlers/docker-socket-handler.js b/server/socket-handlers/docker-socket-handler.js
new file mode 100644
index 000000000..5a53494db
--- /dev/null
+++ b/server/socket-handlers/docker-socket-handler.js
@@ -0,0 +1,79 @@
+const { sendDockerHostList } = require("../client");
+const { checkLogin } = require("../util-server");
+const { DockerHost } = require("../docker");
+const { log } = require("../../src/util");
+
+/**
+ * Handlers for docker hosts
+ * @param {Socket} socket Socket.io instance
+ */
+module.exports.dockerSocketHandler = (socket) => {
+    socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
+        try {
+            checkLogin(socket);
+
+            let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
+            await sendDockerHostList(socket);
+
+            callback({
+                ok: true,
+                msg: "Saved",
+                id: dockerHostBean.id,
+            });
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("deleteDockerHost", async (dockerHostID, callback) => {
+        try {
+            checkLogin(socket);
+
+            await DockerHost.delete(dockerHostID, socket.userID);
+            await sendDockerHostList(socket);
+
+            callback({
+                ok: true,
+                msg: "Deleted",
+            });
+
+        } catch (e) {
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+
+    socket.on("testDockerHost", async (dockerHost, callback) => {
+        try {
+            checkLogin(socket);
+
+            let amount = await DockerHost.testDockerHost(dockerHost);
+            let msg;
+
+            if (amount > 1) {
+                msg = "Connected Successfully. Amount of containers: " + amount;
+            } else {
+                msg = "Connected Successfully, but there are no containers?";
+            }
+
+            callback({
+                ok: true,
+                msg,
+            });
+
+        } catch (e) {
+            log.error("docker", e);
+
+            callback({
+                ok: false,
+                msg: e.message,
+            });
+        }
+    });
+};
diff --git a/server/util-server.js b/server/util-server.js
index df711cf05..067da6fd5 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -15,6 +15,12 @@ const { Client } = require("pg");
 const postgresConParse = require("pg-connection-string").parse;
 const { NtlmClient } = require("axios-ntlm");
 const { Settings } = require("./settings");
+const radiusClient = require("node-radius-client");
+const {
+    dictionaries: {
+        rfc2865: { file, attributes },
+    },
+} = require("node-radius-utils");
 
 // From ping-lite
 exports.WIN = /^win/.test(process.platform);
@@ -285,6 +291,30 @@ exports.postgresQuery = function (connectionString, query) {
     });
 };
 
+exports.radius = function (
+    hostname,
+    username,
+    password,
+    calledStationId,
+    callingStationId,
+    secret,
+) {
+    const client = new radiusClient({
+        host: hostname,
+        dictionaries: [ file ],
+    });
+
+    return client.accessRequest({
+        secret: secret,
+        attributes: [
+            [ attributes.USER_NAME, username ],
+            [ attributes.USER_PASSWORD, password ],
+            [ attributes.CALLING_STATION_ID, callingStationId ],
+            [ attributes.CALLED_STATION_ID, calledStationId ],
+        ],
+    });
+};
+
 /**
  * Retrieve value of setting based on key
  * @param {string} key Key of setting to retrieve
diff --git a/src/components/DockerHostDialog.vue b/src/components/DockerHostDialog.vue
new file mode 100644
index 000000000..92a8ce455
--- /dev/null
+++ b/src/components/DockerHostDialog.vue
@@ -0,0 +1,177 @@
+<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("Setup Docker Host") }}
+                        </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="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
+                            <input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
+                            <select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
+                                <option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
+                            </select>
+                        </div>
+
+                        <div class="mb-3">
+                            <label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
+                            <input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
+
+                            <div class="form-text">
+                                {{ $t("Examples") }}:
+                                <ul>
+                                    <li>/var/run/docker.sock</li>
+                                    <li>tcp://localhost:2375</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("deleteDockerHostMsg") }}
+    </Confirm>
+</template>
+
+<script lang="ts">
+import { Modal } from "bootstrap";
+import Confirm from "./Confirm.vue";
+import { useToast } from "vue-toastification";
+const toast = useToast();
+
+export default {
+    components: {
+        Confirm,
+    },
+    props: {},
+    emits: [ "added" ],
+    data() {
+        return {
+            model: null,
+            processing: false,
+            id: null,
+            connectionTypes: [ "socket", "tcp" ],
+            dockerHost: {
+                name: "",
+                dockerDaemon: "",
+                dockerType: "",
+                // Do not set default value here, please scroll to show()
+            }
+        };
+    },
+
+    mounted() {
+        this.modal = new Modal(this.$refs.modal);
+    },
+    methods: {
+
+        deleteConfirm() {
+            this.modal.hide();
+            this.$refs.confirmDelete.show();
+        },
+
+        show(dockerHostID) {
+            if (dockerHostID) {
+                let found = false;
+
+                this.id = dockerHostID;
+
+                for (let n of this.$root.dockerHostList) {
+                    if (n.id === dockerHostID) {
+                        this.dockerHost = n;
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    toast.error("Docker Host not found!");
+                }
+
+            } else {
+                this.id = null;
+                this.dockerHost = {
+                    name: "",
+                    dockerType: "socket",
+                    dockerDaemon: "/var/run/docker.sock",
+                };
+            }
+
+            this.modal.show();
+        },
+
+        submit() {
+            this.processing = true;
+            this.$root.getSocket().emit("addDockerHost", this.dockerHost, 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() {
+            this.processing = true;
+            this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
+                this.$root.toastRes(res);
+                this.processing = false;
+            });
+        },
+
+        deleteDockerHost() {
+            this.processing = true;
+            this.$root.getSocket().emit("deleteDockerHost", 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/notifications/Bark.vue b/src/components/notifications/Bark.vue
index 014450dec..6cac73d36 100644
--- a/src/components/notifications/Bark.vue
+++ b/src/components/notifications/Bark.vue
@@ -2,9 +2,6 @@
     <div class="mb-3">
         <label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
         <input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
-        <div class="form-text">
-            <p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
-        </div>
         <i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
             <a
                 href="https://github.com/Finb/Bark"
@@ -12,4 +9,45 @@
             >{{ $t("here") }}</a>
         </i18n-t>
     </div>
+    <div class="mb-3">
+        <label for="Bark Group" class="form-label">{{ $t("Bark Group") }}</label>
+        <input id="Bark Group" v-model="$parent.notification.barkGroup" type="text" class="form-control" required>
+    </div>
+    <div class="mb-3">
+        <label for="Bark Sound" class="form-label">{{ $t("Bark Sound") }}</label>
+        <select id="Bark Sound" v-model="$parent.notification.barkSound" class="form-select" required>
+            <option value="alarm">alarm</option>
+            <option value="anticipate">anticipate</option>
+            <option value="bell">bell</option>
+            <option value="birdsong">birdsong</option>
+            <option value="bloom">bloom</option>
+            <option value="calypso">calypso</option>
+            <option value="chime">chime</option>
+            <option value="choo">choo</option>
+            <option value="descent">descent</option>
+            <option value="electronic">electronic</option>
+            <option value="fanfare">fanfare</option>
+            <option value="glass">glass</option>
+            <option value="gotosleep">gotosleep</option>
+            <option value="healthnotification">healthnotification</option>
+            <option value="horn">horn</option>
+            <option value="ladder">ladder</option>
+            <option value="mailsent">mailsent</option>
+            <option value="minuet">minuet</option>
+            <option value="multiwayinvitation">multiwayinvitation</option>
+            <option value="newmail">newmail</option>
+            <option value="newsflash">newsflash</option>
+            <option value="noir">noir</option>
+            <option value="paymentsuccess">paymentsuccess</option>
+            <option value="shake">shake</option>
+            <option value="sherwoodforest">sherwoodforest</option>
+            <option value="silence">silence</option>
+            <option value="spell">spell</option>
+            <option value="suspense">suspense</option>
+            <option value="telegraph">telegraph</option>
+            <option value="tiptoes">tiptoes</option>
+            <option value="typewriters">typewriters</option>
+            <option value="update">update</option>
+        </select>
+    </div>
 </template>
diff --git a/src/components/notifications/HomeAssistant.vue b/src/components/notifications/HomeAssistant.vue
new file mode 100644
index 000000000..67e370a15
--- /dev/null
+++ b/src/components/notifications/HomeAssistant.vue
@@ -0,0 +1,40 @@
+<template>
+    <div class="mb-3">
+        <label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
+        <input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
+    </div>
+
+    <div class="mb-3">
+        <label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
+        <input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
+
+        <div class="form-text">
+            <p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
+        </div>
+    </div>
+
+    <div class="mb-3">
+        <label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
+        <input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
+
+        <div class="form-text">
+            <p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
+            <p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
+            <p>
+                {{ $t("Trigger type:") }} <code>Event</code><br />
+                {{ $t("Event type:") }} <code>call_service</code><br />
+                {{ $t("Event data:") }}
+            </p>
+            <pre>domain: notify
+service: mobile_app_my_phone # change to your device name
+service_data:
+  title: Uptime Kuma
+  data:
+    status: 0 # 0=down 1=up
+    # name: Optional Uptime Kuma Monitor Name to filter by</pre>
+            <p>
+                {{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
+            </p>
+        </div>
+    </div>
+</template>
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index c1b7da4aa..ff523052e 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -10,6 +10,7 @@ import Feishu from "./Feishu.vue";
 import GoogleChat from "./GoogleChat.vue";
 import Gorush from "./Gorush.vue";
 import Gotify from "./Gotify.vue";
+import HomeAssistant from "./HomeAssistant.vue";
 import Line from "./Line.vue";
 import LineNotify from "./LineNotify.vue";
 import LunaSea from "./LunaSea.vue";
@@ -54,6 +55,7 @@ const NotificationFormList = {
     "GoogleChat": GoogleChat,
     "gorush": Gorush,
     "gotify": Gotify,
+    "HomeAssistant": HomeAssistant,
     "line": Line,
     "LineNotify": LineNotify,
     "lunasea": LunaSea,
diff --git a/src/components/settings/Docker.vue b/src/components/settings/Docker.vue
new file mode 100644
index 000000000..c411c307f
--- /dev/null
+++ b/src/components/settings/Docker.vue
@@ -0,0 +1,48 @@
+<template>
+    <div>
+        <div class="dockerHost-list my-4">
+            <p v-if="$root.dockerHostList.length === 0">
+                {{ $t("Not available, please setup.") }}
+            </p>
+
+            <ul class="list-group mb-3" style="border-radius: 1rem;">
+                <li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
+                    {{ dockerHost.name }}<br>
+                    <a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
+                </li>
+            </ul>
+
+            <button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
+                {{ $t("Setup Docker Host") }}
+            </button>
+        </div>
+
+        <DockerHostDialog ref="dockerHostDialog" />
+    </div>
+</template>
+
+<script>
+import DockerHostDialog from "../../components/DockerHostDialog.vue";
+
+export default {
+    components: {
+        DockerHostDialog,
+    },
+
+    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/languages/de-DE.js b/src/languages/de-DE.js
index 3df13b945..ef47909c7 100644
--- a/src/languages/de-DE.js
+++ b/src/languages/de-DE.js
@@ -165,7 +165,10 @@ export default {
     Pink: "Pink",
     "Search...": "Suchen...",
     "Heartbeat Retry Interval": "Überprüfungsintervall",
+    "Resend Notification if Down X times consequently": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
     retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
+    resendEveryXTimes: "Erneut versenden alle {0} mal",
+    resendDisabled: "Erneut versenden deaktiviert",
     "Import Backup": "Backup importieren",
     "Export Backup": "Backup exportieren",
     "Avg. Ping": "Durchschn. Ping",
diff --git a/src/languages/en.js b/src/languages/en.js
index 352a63f6f..3bb025858 100644
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -2,6 +2,8 @@ export default {
     languageName: "English",
     checkEverySecond: "Check every {0} seconds",
     retryCheckEverySecond: "Retry every {0} seconds",
+    resendEveryXTimes: "Resend every {0} times",
+    resendDisabled: "Resend disabled",
     retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
     ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
     upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
@@ -72,6 +74,7 @@ export default {
     "Heartbeat Interval": "Heartbeat Interval",
     Retries: "Retries",
     "Heartbeat Retry Interval": "Heartbeat Retry Interval",
+    "Resend Notification if Down X times consequently": "Resend Notification if Down X times consequently",
     Advanced: "Advanced",
     "Upside Down Mode": "Upside Down Mode",
     "Max. Redirects": "Max. Redirects",
@@ -408,6 +411,8 @@ export default {
     SignName: "SignName",
     "Sms template must contain parameters: ": "Sms template must contain parameters: ",
     "Bark Endpoint": "Bark Endpoint",
+    "Bark Group": "Bark Group",
+    "Bark Sound": "Bark Sound",
     WebHookUrl: "WebHookUrl",
     SecretKey: "SecretKey",
     "For safety, must use secret key": "For safety, must use secret key",
@@ -467,6 +472,7 @@ export default {
     "Domain Name Expiry Notification": "Domain Name Expiry Notification",
     Proxy: "Proxy",
     "Date Created": "Date Created",
+    HomeAssistant: "Home Assistant",
     onebotHttpAddress: "OneBot HTTP Address",
     onebotMessageType: "OneBot Message Type",
     onebotGroupMessage: "Group",
@@ -479,6 +485,12 @@ export default {
     "Domain Names": "Domain Names",
     signedInDisp: "Signed in as {0}",
     signedInDispDisabled: "Auth Disabled.",
+    RadiusSecret: "Radius Secret",
+    RadiusSecretDescription: "Shared Secret between client and server",
+    RadiusCalledStationId: "Called Station Id",
+    RadiusCalledStationIdDescription: "Identifier of the called device",
+    RadiusCallingStationId: "Calling Station Id",
+    RadiusCallingStationIdDescription: "Identifier of the calling device",
     "Certificate Expiry Notification": "Certificate Expiry Notification",
     "API Username": "API Username",
     "API Key": "API Key",
@@ -487,7 +499,7 @@ export default {
     "Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
     "Octopush API Version": "Octopush API Version",
     "Legacy Octopush-DM": "Legacy Octopush-DM",
-    "endpoint": "endpoint",
+    endpoint: "endpoint",
     octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
     octopushLogin: "\"Login\" from HTTP API credentials in control panel",
     promosmsLogin: "API Login Name",
@@ -531,9 +543,19 @@ export default {
     "Coming Soon": "Coming Soon",
     wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
     "Connection String": "Connection String",
-    "Query": "Query",
+    Query: "Query",
     settingsCertificateExpiry: "TLS Certificate Expiry",
     certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
+    "Setup Docker Host": "Setup Docker Host",
+    "Connection Type": "Connection Type",
+    "Docker Daemon": "Docker Daemon",
+    deleteDockerHostMsg: "Are you sure want to delete this docker host for all monitors?",
+    socket: "Socket",
+    tcp: "TCP / HTTP",
+    "Docker Container": "Docker Container",
+    "Container Name / ID": "Container Name / ID",
+    "Docker Host": "Docker Host",
+    "Docker Hosts": "Docker Hosts",
     "ntfy Topic": "ntfy Topic",
     "Domain": "Domain",
     "Workstation": "Workstation",
diff --git a/src/languages/zh-CN.js b/src/languages/zh-CN.js
index 67077f389..8dbe05f0f 100644
--- a/src/languages/zh-CN.js
+++ b/src/languages/zh-CN.js
@@ -404,6 +404,8 @@ export default {
     TemplateCode: "TemplateCode",
     SignName: "SignName",
     "Bark Endpoint": "Bark 接入点",
+    "Bark Group": "Bark 群组",
+    "Bark Sound": "Bark 铃声",
     "Device Token": "Apple Device Token",
     Platform: "平台",
     iOS: "iOS",
diff --git a/src/languages/zh-TW.js b/src/languages/zh-TW.js
index be87c540a..3405c02ab 100644
--- a/src/languages/zh-TW.js
+++ b/src/languages/zh-TW.js
@@ -408,6 +408,8 @@ export default {
     SignName: "SignName",
     "Sms template must contain parameters: ": "Sms 範本必須包含參數:",
     "Bark Endpoint": "Bark 端點",
+    "Bark Group": "Bark 群組",
+    "Bark Sound": "Bark 鈴聲",
     WebHookUrl: "WebHookUrl",
     SecretKey: "SecretKey",
     "For safety, must use secret key": "為了安全起見,必須使用秘密金鑰",
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index ed1620bfe..52dd38919 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -39,6 +39,7 @@ export default {
             uptimeList: { },
             tlsInfoList: {},
             notificationList: [],
+            dockerHostList: [],
             statusPageListLoaded: false,
             statusPageList: [],
             proxyList: [],
@@ -147,6 +148,10 @@ export default {
                 });
             });
 
+            socket.on("dockerHostList", (data) => {
+                this.dockerHostList = 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 3b2603187..99cbeb95f 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -27,6 +27,9 @@
                                         <option value="dns">
                                             DNS
                                         </option>
+                                        <option value="docker">
+                                            {{ $t("Docker Container") }}
+                                        </option>
                                     </optgroup>
 
                                     <optgroup label="Passive Monitor Type">
@@ -48,6 +51,9 @@
                                         <option value="postgres">
                                             PostgreSQL
                                         </option>
+                                        <option value="radius">
+                                            Radius
+                                        </option>
                                     </optgroup>
                                 </select>
                             </div>
@@ -84,8 +90,8 @@
                             </div>
 
                             <!-- Hostname -->
-                            <!-- TCP Port / Ping / DNS / Steam / MQTT only -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
+                            <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
+                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
                                 <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
                                 <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
                             </div>
@@ -141,6 +147,34 @@
                                 </div>
                             </template>
 
+                            <!-- Docker Container Name / ID -->
+                            <!-- For Docker Type -->
+                            <div v-if="monitor.type === 'docker'" class="my-3">
+                                <label for="docker_container" class="form-label">{{ $t("Container Name / ID") }}</label>
+                                <input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required>
+                            </div>
+
+                            <!-- Docker Host -->
+                            <!-- For Docker Type -->
+                            <div v-if="monitor.type === 'docker'" class="my-3">
+                                <h2 class="mb-2">{{ $t("Docker Host") }}</h2>
+                                <p v-if="$root.dockerHostList.length === 0">
+                                    {{ $t("Not available, please setup.") }}
+                                </p>
+
+                                <div v-else class="mb-3">
+                                    <label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
+                                    <select id="docket-host" v-model="monitor.docker_host" class="form-select">
+                                        <option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
+                                    </select>
+                                    <a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
+                                </div>
+
+                                <button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
+                                    {{ $t("Setup Docker Host") }}
+                                </button>
+                            </div>
+
                             <!-- MQTT -->
                             <!-- For MQTT Type -->
                             <template v-if="monitor.type === 'mqtt'">
@@ -171,6 +205,36 @@
                                 </div>
                             </template>
 
+                            <template v-if="monitor.type === 'radius'">
+                                <div class="my-3">
+                                    <label for="radius_username" class="form-label">Radius {{ $t("Username") }}</label>
+                                    <input id="radius_username" v-model="monitor.radiusUsername" type="text" class="form-control" required />
+                                </div>
+
+                                <div class="my-3">
+                                    <label for="radius_password" class="form-label">Radius {{ $t("Password") }}</label>
+                                    <input id="radius_password" v-model="monitor.radiusPassword" type="password" class="form-control" required />
+                                </div>
+
+                                <div class="my-3">
+                                    <label for="radius_secret" class="form-label">{{ $t("RadiusSecret") }}</label>
+                                    <input id="radius_secret" v-model="monitor.radiusSecret" type="password" class="form-control" required />
+                                    <div class="form-text"> {{ $t( "RadiusSecretDescription") }} </div>
+                                </div>
+
+                                <div class="my-3">
+                                    <label for="radius_called_station_id" class="form-label">{{ $t("RadiusCalledStationId") }}</label>
+                                    <input id="radius_called_station_id" v-model="monitor.radiusCalledStationId" type="text" class="form-control" required />
+                                    <div class="form-text"> {{ $t( "RadiusCalledStationIdDescription") }} </div>
+                                </div>
+
+                                <div class="my-3">
+                                    <label for="radius_calling_station_id" class="form-label">{{ $t("RadiusCallingStationId") }}</label>
+                                    <input id="radius_calling_station_id" v-model="monitor.radiusCallingStationId" type="text" class="form-control" required />
+                                    <div class="form-text"> {{ $t( "RadiusCallingStationIdDescription") }} </div>
+                                </div>
+                            </template>
+
                             <!-- SQL Server and PostgreSQL -->
                             <template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres'">
                                 <div class="my-3">
@@ -211,6 +275,15 @@
                                 <input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required min="20" step="1">
                             </div>
 
+                            <div class="my-3">
+                                <label for="resend-interval" class="form-label">
+                                    {{ $t("Resend Notification if Down X times consequently") }}
+                                    <span v-if="monitor.resendInterval > 0">({{ $t("resendEveryXTimes", [ monitor.resendInterval ]) }})</span>
+                                    <span v-else>({{ $t("resendDisabled") }})</span>
+                                </label>
+                                <input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
+                            </div>
+
                             <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
 
                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
@@ -424,6 +497,7 @@
             </form>
 
             <NotificationDialog ref="notificationDialog" @added="addedNotification" />
+            <DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
             <ProxyDialog ref="proxyDialog" @added="addedProxy" />
         </div>
     </transition>
@@ -434,6 +508,7 @@ import VueMultiselect from "vue-multiselect";
 import { useToast } from "vue-toastification";
 import CopyableInput from "../components/CopyableInput.vue";
 import NotificationDialog from "../components/NotificationDialog.vue";
+import DockerHostDialog from "../components/DockerHostDialog.vue";
 import ProxyDialog from "../components/ProxyDialog.vue";
 import TagsManager from "../components/TagsManager.vue";
 import { genSecret, isDev } from "../util.ts";
@@ -445,6 +520,7 @@ export default {
         ProxyDialog,
         CopyableInput,
         NotificationDialog,
+        DockerHostDialog,
         TagsManager,
         VueMultiselect,
     },
@@ -593,6 +669,7 @@ export default {
                     method: "GET",
                     interval: 60,
                     retryInterval: this.interval,
+                    resendInterval: 0,
                     maxretries: 0,
                     notificationIDList: {},
                     ignoreTls: false,
@@ -602,6 +679,8 @@ export default {
                     accepted_statuscodes: [ "200-299" ],
                     dns_resolve_type: "A",
                     dns_resolve_server: "1.1.1.1",
+                    docker_container: "",
+                    docker_host: null,
                     proxyId: null,
                     mqttUsername: "",
                     mqttPassword: "",
@@ -729,6 +808,12 @@ export default {
         addedProxy(id) {
             this.monitor.proxyId = id;
         },
+
+        // Added a Docker Host Event
+        // Enable it if the Docker Host is added in EditMonitor.vue
+        addedDockerHost(id) {
+            this.monitor.docker_host = id;
+        }
     },
 };
 </script>
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
index e10137898..efd26ce37 100644
--- a/src/pages/Settings.vue
+++ b/src/pages/Settings.vue
@@ -89,6 +89,9 @@ export default {
                 "monitor-history": {
                     title: this.$t("Monitor History"),
                 },
+                "docker-hosts": {
+                    title: this.$t("Docker Hosts"),
+                },
                 security: {
                     title: this.$t("Security"),
                 },
diff --git a/src/router.js b/src/router.js
index 726194776..7d29a1882 100644
--- a/src/router.js
+++ b/src/router.js
@@ -25,6 +25,7 @@ const Security = () => import("./components/settings/Security.vue");
 import Proxies from "./components/settings/Proxies.vue";
 import Backup from "./components/settings/Backup.vue";
 import About from "./components/settings/About.vue";
+import DockerHosts from "./components/settings/Docker.vue";
 
 const routes = [
     {
@@ -95,6 +96,10 @@ const routes = [
                                 path: "monitor-history",
                                 component: MonitorHistory,
                             },
+                            {
+                                path: "docker-hosts",
+                                component: DockerHosts,
+                            },
                             {
                                 path: "security",
                                 component: Security,