diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index 0968931d7..b43686bde 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -67,7 +67,11 @@ export const ShowTraefikActions = ({ serverId }: Props) => { > {t("settings.server.webServer.reload")} - + e.preventDefault()} className="cursor-pointer" @@ -108,15 +112,6 @@ export const ShowTraefikActions = ({ serverId }: Props) => { {haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard - {/* - - e.preventDefault()} - > - Enter the terminal - - */} e.preventDefault()} diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx index bedecf517..43b1838de 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx @@ -36,13 +36,20 @@ interface Props { appName: string; children?: React.ReactNode; serverId?: string; + type?: "standalone" | "swarm"; } -export const ShowModalLogs = ({ appName, children, serverId }: Props) => { +export const ShowModalLogs = ({ + appName, + children, + serverId, + type = "swarm", +}: Props) => { const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery( { appName, serverId, + type, }, { enabled: !!appName, diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index f6972e16b..3de59058c 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -65,10 +65,15 @@ export const dockerRouter = createTRPCRouter({ z.object({ appName: z.string().min(1), serverId: z.string().optional(), + type: z.enum(["standalone", "swarm"]), }), ) .query(async ({ input }) => { - return await getContainersByAppLabel(input.appName, input.serverId); + return await getContainersByAppLabel( + input.appName, + input.type, + input.serverId, + ); }), getStackContainersByAppName: protectedProcedure diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index ac05d0fc9..461d3e1f2 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -41,10 +41,6 @@ import { recreateDirectory, sendDockerCleanupNotifications, spawnAsync, - startService, - startServiceRemote, - stopService, - stopServiceRemote, updateLetsEncryptEmail, updateServerById, updateServerTraefik, @@ -88,11 +84,9 @@ export const settingsRouter = createTRPCRouter({ .mutation(async ({ input }) => { try { if (input?.serverId) { - await stopServiceRemote(input.serverId, "dokploy-traefik"); - await startServiceRemote(input.serverId, "dokploy-traefik"); + await execAsync("docker restart dokploy-traefik"); } else if (!IS_CLOUD) { - await stopService("dokploy-traefik"); - await startService("dokploy-traefik"); + await execAsync("docker restart dokploy-traefik"); } } catch (err) { console.error(err); @@ -106,6 +100,7 @@ export const settingsRouter = createTRPCRouter({ await initializeTraefik({ enableDashboard: input.enableDashboard, serverId: input.serverId, + force: true, }); return true; }), @@ -513,16 +508,18 @@ export const settingsRouter = createTRPCRouter({ .input(apiServerSchema) .query(async ({ input }) => { const command = - "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik"; + "docker container inspect dokploy-traefik --format '{{json .Config.Env}}'"; + let result = ""; if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - return result.stdout.trim(); - } - if (!IS_CLOUD) { - const result = await execAsync(command); - return result.stdout.trim(); + const execResult = await execAsyncRemote(input.serverId, command); + result = execResult.stdout; + } else { + const execResult = await execAsync(command); + result = execResult.stdout; } + const envVars = JSON.parse(result.trim()); + return envVars.join("\n"); }), writeTraefikEnv: adminProcedure @@ -532,6 +529,7 @@ export const settingsRouter = createTRPCRouter({ await initializeTraefik({ env: envs, serverId: input.serverId, + force: true, }); return true; @@ -539,27 +537,22 @@ export const settingsRouter = createTRPCRouter({ haveTraefikDashboardPortEnabled: adminProcedure .input(apiServerSchema) .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`; let stdout = ""; if (input?.serverId) { const result = await execAsyncRemote(input.serverId, command); stdout = result.stdout; } else if (!IS_CLOUD) { - const result = await execAsync( - "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik", - ); + const result = await execAsync(command); stdout = result.stdout; } - const parsed: any[] = JSON.parse(stdout.trim()); - for (const port of parsed) { - if (port.PublishedPort === 8080) { - return true; - } - } - - return false; + const ports = JSON.parse(stdout.trim()); + return Object.entries(ports).some(([containerPort, bindings]) => { + const [port] = containerPort.split("/"); + return port === "8080" && bindings && (bindings as any[]).length > 0; + }); }), readStatsLogs: adminProcedure @@ -772,6 +765,7 @@ export const settingsRouter = createTRPCRouter({ await initializeTraefik({ serverId: input.serverId, additionalPorts: input.additionalPorts, + force: true, }); return true; } catch (error) { @@ -788,7 +782,7 @@ export const settingsRouter = createTRPCRouter({ getTraefikPorts: adminProcedure .input(apiServerSchema) .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`; try { let stdout = ""; @@ -800,21 +794,38 @@ export const settingsRouter = createTRPCRouter({ stdout = result.stdout; } - const ports: { - Protocol: string; - TargetPort: number; - PublishedPort: number; - PublishMode: string; - }[] = JSON.parse(stdout.trim()); + const portsMap = JSON.parse(stdout.trim()); + const additionalPorts: Array<{ + targetPort: number; + publishedPort: number; + publishMode: "host" | "ingress"; + }> = []; - // Filter out the default ports (80, 443, and optionally 8080) - const additionalPorts = ports - .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) - .map((port) => ({ - targetPort: port.TargetPort, - publishedPort: port.PublishedPort, - publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", - })); + // Convert the Docker container port format to our expected format + for (const [containerPort, bindings] of Object.entries(portsMap)) { + if (!bindings) continue; + + const [port = ""] = containerPort.split("/"); + if (!port) continue; + + const targetPortNum = Number.parseInt(port, 10); + if (Number.isNaN(targetPortNum)) continue; + + // Skip default ports + if ([80, 443, 8080].includes(targetPortNum)) continue; + + for (const binding of bindings as Array<{ HostPort: string }>) { + if (!binding.HostPort) continue; + const publishedPort = Number.parseInt(binding.HostPort, 10); + if (Number.isNaN(publishedPort)) continue; + + additionalPorts.push({ + targetPort: targetPortNum, + publishedPort, + publishMode: "host", // Docker standalone uses host mode by default + }); + } + } return additionalPorts; } catch (error) { diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index d2f4de53b..69ae446a9 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -287,13 +287,19 @@ export const getServiceContainersByAppName = async ( export const getContainersByAppLabel = async ( appName: string, + type: "standalone" | "swarm", serverId?: string, ) => { try { let stdout = ""; let stderr = ""; - const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`; + const command = + type === "swarm" + ? `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'` + : type === "standalone" + ? `docker ps --filter "name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'` + : `docker ps --filter "label=com.docker.compose.project=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`; if (serverId) { const result = await execAsyncRemote(serverId, command); stdout = result.stdout; diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 4f2335b2e..e129dce10 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -9,6 +9,7 @@ import { TRAEFIK_PORT, TRAEFIK_SSL_PORT, TRAEFIK_VERSION, + TRAEFIK_HTTP3_PORT, getDefaultMiddlewares, getDefaultServerTraefikConfig, } from "@dokploy/server/setup/traefik-setup"; @@ -542,22 +543,28 @@ export const installRClone = () => ` export const createTraefikInstance = () => { const command = ` # Check if dokpyloy-traefik exists - if docker service ls | grep -q 'dokploy-traefik'; then + if docker service inspect dokploy-traefik > /dev/null 2>&1; then + echo "Migrating Traefik to Standalone..." + docker service rm dokploy-traefik + sleep 7 + echo "Traefik migrated to Standalone ✅" + fi + + if docker inspect dokploy-traefik > /dev/null 2>&1; then echo "Traefik already exists ✅" else - # Create the dokploy-traefik service + # Create the dokploy-traefik container TRAEFIK_VERSION=${TRAEFIK_VERSION} - docker service create \ + docker run -d \ --name dokploy-traefik \ - --replicas 1 \ - --constraint 'node.role==manager' \ --network dokploy-network \ - --mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \ - --mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \ - --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ - --label traefik.enable=true \ - --publish mode=host,target=${TRAEFIK_SSL_PORT},published=${TRAEFIK_SSL_PORT} \ - --publish mode=host,target=${TRAEFIK_PORT},published=${TRAEFIK_PORT} \ + --restart unless-stopped \ + -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \ + -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \ + -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \ + -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \ traefik:v$TRAEFIK_VERSION echo "Traefik version $TRAEFIK_VERSION installed ✅" fi diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index e8d019424..7f2b707d0 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -1,9 +1,8 @@ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; -import type { ContainerTaskSpec, CreateServiceOptions } from "dockerode"; +import type { ContainerCreateOptions } from "dockerode"; import { dump } from "js-yaml"; import { paths } from "../constants"; -import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { getRemoteDocker } from "../utils/servers/remote-docker"; import type { FileConfig } from "../utils/traefik/file-types"; import type { MainTraefikConfig } from "../utils/traefik/types"; @@ -12,6 +11,8 @@ export const TRAEFIK_SSL_PORT = Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443; export const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; +export const TRAEFIK_HTTP3_PORT = + Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2"; interface TraefikOptions { @@ -23,6 +24,7 @@ interface TraefikOptions { publishedPort: number; publishMode?: "ingress" | "host"; }[]; + force?: boolean; } export const initializeTraefik = async ({ @@ -30,113 +32,86 @@ export const initializeTraefik = async ({ env, serverId, additionalPorts = [], + force = false, }: TraefikOptions = {}) => { const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); const imageName = `traefik:v${TRAEFIK_VERSION}`; const containerName = "dokploy-traefik"; - const settings: CreateServiceOptions = { - Name: containerName, - TaskTemplate: { - ContainerSpec: { - Image: imageName, - Env: env, - Mounts: [ - { - Type: "bind", - Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`, - Target: "/etc/traefik/traefik.yml", - }, - { - Type: "bind", - Source: DYNAMIC_TRAEFIK_PATH, - Target: "/etc/dokploy/traefik/dynamic", - }, - { - Type: "bind", - Source: "/var/run/docker.sock", - Target: "/var/run/docker.sock", - }, - ], - }, - Networks: [{ Target: "dokploy-network" }], - Placement: { - Constraints: ["node.role==manager"], - }, - }, - Mode: { - Replicated: { - Replicas: 1, - }, - }, - EndpointSpec: { - Ports: [ - { - TargetPort: 443, - PublishedPort: TRAEFIK_SSL_PORT, - PublishMode: "host", - }, - { - TargetPort: 80, - PublishedPort: TRAEFIK_PORT, - PublishMode: "host", - }, - ...(enableDashboard - ? [ - { - TargetPort: 8080, - PublishedPort: 8080, - PublishMode: "host" as const, - }, - ] - : []), - ...additionalPorts.map((port) => ({ - TargetPort: port.targetPort, - PublishedPort: port.publishedPort, - PublishMode: port.publishMode || ("host" as const), - })), - ], - }, + + const exposedPorts: Record = { + [`${TRAEFIK_PORT}/tcp`]: {}, + [`${TRAEFIK_SSL_PORT}/tcp`]: {}, + [`${TRAEFIK_HTTP3_PORT}/udp`]: {}, }; + + const portBindings: Record> = { + [`${TRAEFIK_PORT}/tcp`]: [{ HostPort: TRAEFIK_PORT.toString() }], + [`${TRAEFIK_SSL_PORT}/tcp`]: [{ HostPort: TRAEFIK_SSL_PORT.toString() }], + [`${TRAEFIK_HTTP3_PORT}/udp`]: [ + { HostPort: TRAEFIK_HTTP3_PORT.toString() }, + ], + }; + + if (enableDashboard) { + exposedPorts["8080/tcp"] = {}; + portBindings["8080/tcp"] = [{ HostPort: "8080" }]; + } + + for (const port of additionalPorts) { + const portKey = `${port.targetPort}/tcp`; + exposedPorts[portKey] = {}; + portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }]; + } + + const settings: ContainerCreateOptions = { + name: containerName, + Image: imageName, + NetworkingConfig: { + EndpointsConfig: { + "dokploy-network": {}, + }, + }, + ExposedPorts: exposedPorts, + HostConfig: { + RestartPolicy: { + Name: "always", + }, + Binds: [ + `${MAIN_TRAEFIK_PATH}/traefik.yml:/etc/traefik/traefik.yml`, + `${DYNAMIC_TRAEFIK_PATH}:/etc/dokploy/traefik/dynamic`, + "/var/run/docker.sock:/var/run/docker.sock", + ], + PortBindings: portBindings, + }, + Env: env, + }; + const docker = await getRemoteDocker(serverId); try { - if (serverId) { - await pullRemoteImage(imageName, serverId); - } else { - await pullImage(imageName); - } - - const service = docker.getService(containerName); - const inspect = await service.inspect(); - - const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || []; - const updatedEnv = !env ? existingEnv : env; - - const updatedSettings = { - ...settings, - TaskTemplate: { - ...settings.TaskTemplate, - ContainerSpec: { - ...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec, - Env: updatedEnv, - }, - }, - }; - await service.update({ - version: Number.parseInt(inspect.Version.Index), - ...updatedSettings, - }); - - console.log("Traefik Started ✅"); - } catch (_) { try { - await docker.createService(settings); - } catch (error: any) { - if (error?.statusCode !== 409) { - throw error; + const service = docker.getService("dokploy-traefik"); + await service?.remove({ force: true }); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } catch (_) {} + + const container = docker.getContainer(containerName); + try { + const inspect = await container.inspect(); + if (inspect.State.Status === "running" && !force) { + console.log("Traefik already running"); + return; } - console.log("Traefik service already exists, continuing..."); + + await container.remove({ force: true }); + } catch (error) { + console.log(error); } - console.log("Traefik Not Found: Starting ✅"); + + await docker.createContainer(settings); + const newContainer = docker.getContainer(containerName); + await newContainer.start(); + } catch (error) { + console.log(error); } }; @@ -212,6 +187,9 @@ export const getDefaultTraefikConfig = () => { }, websecure: { address: `:${TRAEFIK_SSL_PORT}`, + http3: { + advertisedPort: TRAEFIK_HTTP3_PORT, + }, ...(process.env.NODE_ENV === "production" && { http: { tls: { @@ -267,6 +245,9 @@ export const getDefaultServerTraefikConfig = () => { }, websecure: { address: `:${TRAEFIK_SSL_PORT}`, + http3: { + advertisedPort: TRAEFIK_HTTP3_PORT, + }, http: { tls: { certResolver: "letsencrypt",