From 3848fa9c0d69ec7abbced1535de7fad4c1bad82d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:36:16 -0600 Subject: [PATCH] fix: deploy monitoring as a swarm service instead of a standalone container (#5055) Monitoring was the only component still using docker.createContainer directly instead of docker.createService like every other Dokploy component (postgres, traefik, forward-auth, etc). This meant it never benefited from Swarm's own reconciliation, relying only on Docker's restart policy with no self-healing if the daemon didn't come back cleanly after a reboot. Also removed the try/catch that silently swallowed setup errors, which made the UI report success even when the container/service failed to start. --- packages/server/src/setup/monitoring-setup.ts | 257 ++++++++++-------- 1 file changed, 147 insertions(+), 110 deletions(-) diff --git a/packages/server/src/setup/monitoring-setup.ts b/packages/server/src/setup/monitoring-setup.ts index 287894c99..b2d3f6f4d 100644 --- a/packages/server/src/setup/monitoring-setup.ts +++ b/packages/server/src/setup/monitoring-setup.ts @@ -1,16 +1,13 @@ import { findServerById } from "@dokploy/server/services/server"; import { getWebServerSettings } from "@dokploy/server/services/web-server-settings"; -import type { ContainerCreateOptions } from "dockerode"; +import type { CreateServiceOptions } from "dockerode"; import { IS_CLOUD } from "../constants"; import { getDokployImageTag } from "../services/settings"; import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { getRemoteDocker } from "../utils/servers/remote-docker"; -export const setupMonitoring = async (serverId: string) => { - const server = await findServerById(serverId); - - const containerName = "dokploy-monitoring"; +const getMonitoringImage = () => { let imageName = "dokploy/monitoring:latest"; if ( @@ -21,134 +18,174 @@ export const setupMonitoring = async (serverId: string) => { imageName = "dokploy/monitoring:canary"; } - const settings: ContainerCreateOptions = { - name: containerName, - Env: [`METRICS_CONFIG=${JSON.stringify(server?.metricsConfig)}`], - Image: imageName, - HostConfig: { - // Memory: 100 * 1024 * 1024, // 100MB en bytes - // PidMode: "host", - // CapAdd: ["NET_ADMIN", "SYS_ADMIN"], - // Privileged: true, - RestartPolicy: { - Name: "always", + return imageName; +}; + +const deployMonitoringService = async ( + docker: Awaited>, + serviceName: string, + settings: CreateServiceOptions, +) => { + try { + const service = docker.getService(serviceName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + TaskTemplate: { + ...settings.TaskTemplate, + ForceUpdate: (inspect.Spec.TaskTemplate.ForceUpdate ?? 0) + 1, }, - PortBindings: { - [`${server.metricsConfig.server.port}/tcp`]: [ + }); + console.log("Monitoring Updated ✅"); + } catch (error: any) { + if (error?.statusCode && error.statusCode !== 404) { + throw error; + } + await docker.createService(settings); + console.log("Monitoring Started ✅"); + } +}; + +export const setupMonitoring = async (serverId: string) => { + const server = await findServerById(serverId); + + const serviceName = "dokploy-monitoring"; + const imageName = getMonitoringImage(); + + const settings: CreateServiceOptions = { + Name: serviceName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Env: [`METRICS_CONFIG=${JSON.stringify(server?.metricsConfig)}`], + Mounts: [ { - HostPort: server.metricsConfig.server.port.toString(), + Type: "bind", + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/sys", + Target: "/host/sys", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/etc/os-release", + Target: "/etc/os-release", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/proc", + Target: "/host/proc", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/etc/dokploy/monitoring/monitoring.db", + Target: "/app/monitoring.db", }, ], }, - Binds: [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "/sys:/host/sys:ro", - "/etc/os-release:/etc/os-release:ro", - "/proc:/host/proc:ro", - "/etc/dokploy/monitoring/monitoring.db:/app/monitoring.db", - ], - NetworkMode: "host", + Networks: [{ Target: "host" }], + Placement: { + Constraints: ["node.role==manager"], + }, }, - ExposedPorts: { - [`${server.metricsConfig.server.port}/tcp`]: {}, + Mode: { + Replicated: { + Replicas: 1, + }, }, }; + const docker = await getRemoteDocker(serverId); - try { - await execAsyncRemote( - serverId, - "mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db", - ); - if (serverId) { - await pullRemoteImage(imageName, serverId); - } - // Check if container exists - const container = docker.getContainer(containerName); - try { - await container.inspect(); - await container.remove({ force: true }); - console.log("Removed existing container"); - } catch { - // Container doesn't exist, continue - } - - await docker.createContainer(settings); - const newContainer = docker.getContainer(containerName); - await newContainer.start(); - - console.log("Monitoring Started "); - } catch (error) { - console.log("Monitoring Not Found: Starting ", error); - } + await execAsyncRemote( + serverId, + "mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db", + ); + await pullRemoteImage(imageName, serverId); + await deployMonitoringService(docker, serviceName, settings); }; export const setupWebMonitoring = async () => { const webServerSettings = await getWebServerSettings(); - const containerName = "dokploy-monitoring"; - let imageName = "dokploy/monitoring:latest"; + const serviceName = "dokploy-monitoring"; + const imageName = getMonitoringImage(); + const port = webServerSettings?.metricsConfig?.server?.port; - if ( - (getDokployImageTag() !== "latest" || - process.env.NODE_ENV === "development") && - !IS_CLOUD - ) { - imageName = "dokploy/monitoring:canary"; - } - - const settings: ContainerCreateOptions = { - name: containerName, - Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`], - Image: imageName, - HostConfig: { - // Memory: 100 * 1024 * 1024, // 100MB en bytes - // PidMode: "host", - // CapAdd: ["NET_ADMIN", "SYS_ADMIN"], - // Privileged: true, - RestartPolicy: { - Name: "always", - }, - PortBindings: { - [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [ + const settings: CreateServiceOptions = { + Name: serviceName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Env: [ + `METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`, + ], + Mounts: [ { - HostPort: webServerSettings?.metricsConfig?.server?.port.toString(), + Type: "bind", + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/sys", + Target: "/host/sys", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/etc/os-release", + Target: "/etc/os-release", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/proc", + Target: "/host/proc", + ReadOnly: true, + }, + { + Type: "bind", + Source: "/etc/dokploy/monitoring/monitoring.db", + Target: "/app/monitoring.db", }, ], }, - Binds: [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "/sys:/host/sys:ro", - "/etc/os-release:/etc/os-release:ro", - "/proc:/host/proc:ro", - "/etc/dokploy/monitoring/monitoring.db:/app/monitoring.db", - ], - // NetworkMode: "host", + Placement: { + Constraints: ["node.role==manager"], + }, }, - ExposedPorts: { - [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {}, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + EndpointSpec: { + Ports: [ + { + TargetPort: port, + PublishedPort: port, + Protocol: "tcp", + PublishMode: "host", + }, + ], }, }; + const docker = await getRemoteDocker(); - try { - await execAsync( - "mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db", - ); - await pullImage(imageName); - const container = docker.getContainer(containerName); - try { - await container.inspect(); - await container.remove({ force: true }); - console.log("Removed existing container"); - } catch {} - - await docker.createContainer(settings); - const newContainer = docker.getContainer(containerName); - await newContainer.start(); - - console.log("Monitoring Started "); - } catch (error) { - console.log("Monitoring Not Found: Starting ", error); - } + await execAsync( + "mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db", + ); + await pullImage(imageName); + await deployMonitoringService(docker, serviceName, settings); };