diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 438a2575b..b3aa6467c 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -634,7 +634,7 @@
"option": {
"dockerHost": {
"label": "Docker host",
- "placeholder": "unix:///var/run/docker.sock or ssh://user@host"
+ "placeholder": "unix:///var/run/docker.sock"
},
"host": {
"label": "Host",
@@ -716,7 +716,7 @@
"http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard. Set an optional proxy server to connect through.",
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
- "docker": "Enter the Docker host to monitor: a local socket (unix:///var/run/docker.sock or /var/run/docker.sock) or a remote host over SSH (ssh://user@host, with an optional :port). Every container on the host is monitored.",
+ "docker": "Enter the Docker socket to monitor, as unix:///var/run/docker.sock or an absolute path such as /var/run/docker.sock. Every container on the host is monitored.",
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type. Some games require enabling server queries in their config file — check your game server's documentation. See the supported games list for protocol details.",
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts
index c11df8f80..ddea832ef 100644
--- a/server/src/api/validation/monitorValidation.ts
+++ b/server/src/api/validation/monitorValidation.ts
@@ -120,22 +120,15 @@ const refineProxySelection = (body: { proxyMode?: string; proxyId?: string }, ct
}
};
-const dockerUrlRegex = /^(?:ssh:\/\/[^@\s]+@[^\s/:@]+(?::\d{1,5})?\/?|unix:\/\/\/\S+|\/\S+)$/;
+const dockerUrlRegex = /^(?:unix:\/\/\/\S+|\/\S+)$/;
-const refineDockerUrl = (data: { type?: string; url?: string; sshPrivateKey?: string }, ctx: z.RefinementCtx) => {
+const refineDockerUrl = (data: { type?: string; url?: string }, ctx: z.RefinementCtx) => {
if (data.type !== "docker" || data.url === undefined) return;
if (!dockerUrlRegex.test(data.url)) {
ctx.addIssue({
code: "custom",
path: ["url"],
- message: "Docker host must be ssh://user@host[:port], unix:///path, or an absolute socket path",
- });
- }
- if (data.url.startsWith("ssh://") && !data.sshPrivateKey) {
- ctx.addIssue({
- code: "custom",
- path: ["sshPrivateKey"],
- message: "SSH Docker hosts require a private key",
+ message: "Docker host must be unix:///path or an absolute socket path",
});
}
};
@@ -164,7 +157,6 @@ export const createMonitorBodyValidation = z
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).default([]),
secret: z.string().optional(),
- sshPrivateKey: z.string().optional(),
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
@@ -201,7 +193,6 @@ export const editMonitorBodyValidation = z
tags: z.array(z.string()).optional(),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
- sshPrivateKey: z.string().optional(),
ignoreTlsErrors: z.boolean().optional(),
proxyMode: z.enum(ProxyModes).optional(),
proxyId: proxyIdValidation,
@@ -283,7 +274,6 @@ const importedMonitorSchema = z
tags: z.array(z.string()).default([]),
customUpCodes: z.array(httpStatusCode).default([]),
secret: z.string().optional(),
- sshPrivateKey: z.string().optional(),
cpuAlertThreshold: z.number().default(100),
cpuAlertCounter: z.number().default(5),
memoryAlertThreshold: z.number().default(100),
@@ -379,7 +369,6 @@ export const monitorResponseSchema = z
tags: z.array(z.string()),
customUpCodes: z.array(httpStatusCode).optional(),
secret: z.string().optional(),
- sshPrivateKey: z.string().optional(),
cpuAlertThreshold: z.number(),
memoryAlertThreshold: z.number(),
diskAlertThreshold: z.number(),
diff --git a/server/src/db/migration/0012_migrateDockerMonitorUrls.ts b/server/src/db/migration/0012_migrateDockerMonitorUrls.ts
index 75f3d5acf..d4d3bd143 100644
--- a/server/src/db/migration/0012_migrateDockerMonitorUrls.ts
+++ b/server/src/db/migration/0012_migrateDockerMonitorUrls.ts
@@ -7,8 +7,8 @@ import type { ILogger } from "@/utils/logger.js";
* The old docker provider interpreted monitor.url as a container name/ID and only
* ever talked to the local socket, so every pre-existing docker monitor's host is
* definitionally unix:///var/run/docker.sock. The container name is preserved in
- * the description. Urls already in host form (unix://, ssh://, or an absolute
- * socket path) are left untouched, which makes the migration idempotent.
+ * the description. Urls already in host form (unix:// or an absolute socket
+ * path) are left untouched, which makes the migration idempotent.
*/
export async function migrateDockerMonitorUrls(logger: ILogger): Promise {
const SERVICE_NAME = "Migration:MigrateDockerMonitorUrls";
@@ -16,7 +16,7 @@ export async function migrateDockerMonitorUrls(logger: ILogger): Promise {
try {
logger.info({ service: SERVICE_NAME, message: "Starting docker monitor url migration" });
- const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^((unix|ssh):\/\/|\/)/ } }, [
+ const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^(unix:\/\/|\/)/ } }, [
{
$set: {
description: {
diff --git a/server/src/domain/monitors/monitor.model.ts b/server/src/domain/monitors/monitor.model.ts
index f45b613a2..2517f5d8e 100644
--- a/server/src/domain/monitors/monitor.model.ts
+++ b/server/src/domain/monitors/monitor.model.ts
@@ -235,9 +235,6 @@ const MonitorSchema = new Schema(
secret: {
type: String,
},
- sshPrivateKey: {
- type: String,
- },
cpuAlertThreshold: {
type: Number,
default: 100,
diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts
index d31d830ac..db6b79853 100644
--- a/server/src/domain/monitors/monitor.repository.mongo.ts
+++ b/server/src/domain/monitors/monitor.repository.mongo.ts
@@ -436,7 +436,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
tags: tagIds,
customUpCodes: doc.customUpCodes ?? [],
secret: doc.secret ?? undefined,
- sshPrivateKey: doc.sshPrivateKey ?? undefined,
cpuAlertThreshold: doc.cpuAlertThreshold,
cpuAlertCounter: doc.cpuAlertCounter,
memoryAlertThreshold: doc.memoryAlertThreshold,
diff --git a/server/src/domain/monitors/monitor.type.ts b/server/src/domain/monitors/monitor.type.ts
index 587112c24..12fcd1036 100644
--- a/server/src/domain/monitors/monitor.type.ts
+++ b/server/src/domain/monitors/monitor.type.ts
@@ -92,7 +92,6 @@ export interface Monitor {
tags: string[];
customUpCodes: HttpStatusCode[];
secret?: string;
- sshPrivateKey?: string;
cpuAlertThreshold: number;
cpuAlertCounter: number;
memoryAlertThreshold: number;
diff --git a/server/src/service/network/DockerProvider.ts b/server/src/service/network/DockerProvider.ts
index 38bb59fef..776da3f92 100644
--- a/server/src/service/network/DockerProvider.ts
+++ b/server/src/service/network/DockerProvider.ts
@@ -63,31 +63,7 @@ export class DockerProvider implements IStatusProvider {
return { socketPath };
}
if (url.startsWith("/")) return { socketPath: url };
- const match = url.match(/^ssh:\/\/(?:([^@\s]+)@)?([^\s/:@]+)(?::(\d{1,5}))?\/?$/);
- if (!match) throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" });
- const [, username, host, port] = match;
- if (!username)
- throw new AppError({
- message: "SSH Docker host URL requires a user: ssh://user@host",
- status: 422,
- service: SERVICE_NAME,
- method: "toDockerOptions",
- });
-
- if (!monitor.sshPrivateKey)
- throw new AppError({
- message: "SSH Docker host requires a private key",
- status: 422,
- service: SERVICE_NAME,
- method: "toDockerOptions",
- });
- return {
- protocol: "ssh",
- host,
- port: port ? Number(port) : 22,
- username,
- sshOptions: { privateKey: monitor.sshPrivateKey },
- };
+ throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" });
};
private async mapWithConcurrency(items: T[], limit: number, fn: (item: T) => Promise): Promise {
diff --git a/server/test/unit/providers/network/dockerProvider.test.ts b/server/test/unit/providers/network/dockerProvider.test.ts
index ba9d90568..4c27985ef 100644
--- a/server/test/unit/providers/network/dockerProvider.test.ts
+++ b/server/test/unit/providers/network/dockerProvider.test.ts
@@ -9,8 +9,6 @@ import { DOCKER_LOG_TAIL_LINES } from "../../../../src/domain/docker/docker.type
// ── Fixtures ─────────────────────────────────────────────────────────────────
-const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----";
-
const makeMonitor = (overrides?: Partial): Monitor =>
({
id: "mon-1",
@@ -116,28 +114,6 @@ describe("DockerProvider", () => {
expect(DockerLib).toHaveBeenCalledWith({ socketPath: "/var/run/docker.sock" });
});
-
- it("parses ssh urls with a private key, defaulting to port 22", async () => {
- const { provider, DockerLib } = setup();
-
- await provider.handle(makeMonitor({ url: "ssh://deploy@prod-swarm-01.internal", sshPrivateKey: PRIVATE_KEY }));
-
- expect(DockerLib).toHaveBeenCalledWith({
- protocol: "ssh",
- host: "prod-swarm-01.internal",
- port: 22,
- username: "deploy",
- sshOptions: { privateKey: PRIVATE_KEY },
- });
- });
-
- it("parses an explicit ssh port", async () => {
- const { provider, DockerLib } = setup();
-
- await provider.handle(makeMonitor({ url: "ssh://deploy@host:2222", sshPrivateKey: PRIVATE_KEY }));
-
- expect(DockerLib).toHaveBeenCalledWith(expect.objectContaining({ port: 2222 }));
- });
});
// ── Fail-loudly url validation ───────────────────────────────────────
@@ -151,7 +127,7 @@ describe("DockerProvider", () => {
["a container name (old semantics)", "my-container", "Invalid Docker host URL"],
["tcp engine urls (unsupported)", "tcp://host:2375", "Invalid Docker host URL"],
["https engine urls (unsupported)", "https://host", "Invalid Docker host URL"],
- ["ssh without a user", "ssh://host", "SSH Docker host URL requires a user: ssh://user@host"],
+ ["ssh engine urls (unsupported)", "ssh://deploy@host", "Invalid Docker host URL"],
];
it.each(invalidUrls)("throws AppError for %s and never constructs a client", async (_label, url, message) => {
@@ -161,15 +137,6 @@ describe("DockerProvider", () => {
expect(DockerLib).not.toHaveBeenCalled();
});
- it("throws AppError for an ssh url with no private key", async () => {
- const { provider, DockerLib } = setup();
-
- await expect(provider.handle(makeMonitor({ url: "ssh://deploy@host", sshPrivateKey: undefined }))).rejects.toThrow(
- "SSH Docker host requires a private key"
- );
- expect(DockerLib).not.toHaveBeenCalled();
- });
-
it("preserves the toDockerOptions AppError through handle's catch", async () => {
const { provider } = setup();
diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts
index bae34f830..1cd224ab5 100644
--- a/server/test/unit/validation/monitorValidation.test.ts
+++ b/server/test/unit/validation/monitorValidation.test.ts
@@ -608,7 +608,6 @@ describe("monitorValidation — Docker host url", () => {
type: "docker" as const,
url: "unix:///var/run/docker.sock",
};
- const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----";
describe("createMonitorBodyValidation", () => {
it("accepts the default unix socket url", () => {
@@ -621,32 +620,15 @@ describe("monitorValidation — Docker host url", () => {
expect(parsed.url).toBe("/run/docker.sock");
});
- it("accepts an ssh url with a private key and retains the key", () => {
- for (const url of ["ssh://deploy@host", "ssh://deploy@host:2222"]) {
- const parsed = createMonitorBodyValidation.parse({ ...baseDockerBody, url, sshPrivateKey: PRIVATE_KEY });
- expect(parsed.sshPrivateKey).toBe(PRIVATE_KEY);
- }
- });
-
it("rejects container names and unsupported engine urls", () => {
- for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "unix://relative/path", ""]) {
+ for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "ssh://deploy@host", "unix://relative/path", ""]) {
expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: badUrl })).toThrow();
}
});
- it("rejects an ssh url without a user", () => {
- expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: "ssh://host", sshPrivateKey: PRIVATE_KEY })).toThrow();
- });
-
- it("rejects an ssh url without a private key, attributing the issue to sshPrivateKey", () => {
- const result = createMonitorBodyValidation.safeParse({ ...baseDockerBody, url: "ssh://deploy@host" });
- expect(result.success).toBe(false);
- expect(result.error?.issues.some((issue) => issue.path.includes("sshPrivateKey"))).toBe(true);
- });
-
it("does not apply docker url rules to other monitor types", () => {
const parsed = createMonitorBodyValidation.parse({ name: "HTTP check", type: "http", url: "https://example.com" });
- expect(parsed.sshPrivateKey).toBeUndefined();
+ expect(parsed.url).toBe("https://example.com");
});
});
@@ -660,7 +642,7 @@ describe("monitorValidation — Docker host url", () => {
expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "my-container" })).toThrow();
});
- it("rejects a docker edit switching to ssh without a key", () => {
+ it("rejects a docker edit with an ssh url", () => {
expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "ssh://deploy@host" })).toThrow();
});
});