feat(traefik): detect version drift and allow updating to pinned version (#5221) - #5446
fliptrigga13 wants to merge 1 commit into
Conversation
| }); | ||
| return true; | ||
| }), | ||
| getTraefikVersionInfo: adminProcedure | ||
| .input(apiServerSchema) | ||
| .query(async ({ input }) => { | ||
| return await getTraefikVersionInfo(input?.serverId); | ||
| }), | ||
| updateTraefik: adminProcedure | ||
| .input(apiServerSchema) | ||
| .mutation(async ({ input, ctx }) => { | ||
| // Run in background so the request returns immediately; client polls /api/health. | ||
| // Avoids proxy timeouts (520) while Traefik is recreated. | ||
| void updateTraefikToPinnedVersion(input?.serverId).catch((err) => { | ||
| console.error( | ||
| "updateTraefik background updateTraefikToPinnedVersion:", | ||
| err, | ||
| ); | ||
| }); | ||
| await audit(ctx, { | ||
| action: "update", | ||
| resourceType: "settings", | ||
| resourceName: "traefik-version", |
There was a problem hiding this comment.
Missing server ownership check
These procedures accept any serverId, but neither the router nor the remote execution helpers verify that the server belongs to the caller's active organization. An owner or admin who obtains another organization's server ID can inspect its Traefik image or trigger a cross-tenant proxy recreation. Validate the server's organizationId against ctx.session.activeOrganizationId before either operation.
How this was verified: The request-provided server ID reaches findServerById and SSH execution without any active-organization comparison.
Knowledge Base Used: API boundary
| // Avoids proxy timeouts (520) while Traefik is recreated. | ||
| void updateTraefikToPinnedVersion(input?.serverId).catch((err) => { | ||
| console.error( | ||
| "updateTraefik background updateTraefikToPinnedVersion:", | ||
| err, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
The mutation returns immediately after starting the update in the background, so the client begins its health check before recreation completes. Standalone recreation includes at least eight seconds of fixed waits, while the client checks after five seconds. The still-running old Traefik can therefore answer successfully, causing a false success toast and a version refetch that reads the old image. A later update failure is only logged. Return or expose a completion signal tied to the actual update instead of treating generic health as completion.
Knowledge Base Used:
| const preparedEnv = prepareEnvironmentVariables(env); | ||
|
|
||
| await writeTraefikSetup({ | ||
| env: preparedEnv, | ||
| additionalPorts: ports, |
There was a problem hiding this comment.
Environment values are reparsed
readEnvironmentVariables already returns resolved Docker KEY=VALUE entries, but this code reparses them with prepareEnvironmentVariables, which is intended for dotenv and template input. Runtime values containing dotenv-significant text can change: an unquoted # value is truncated as a comment, while literal ${{...}} text may be substituted or rejected. The recreated Traefik instance therefore may not preserve its existing environment as promised. Round-trip the inspected values without dotenv or template processing.
Knowledge Base Used: Docker networking and Traefik
| export const isVersionOlder = (running: string, pinned: string): boolean => { | ||
| const parseParts = (v: string) => { | ||
| const clean = v.replace(/^v/, "").split("-")[0] || ""; | ||
| return clean.split(".").map((n) => Number.parseInt(n, 10) || 0); | ||
| }; | ||
| const [rMajor = 0, rMinor = 0, rPatch = 0] = parseParts(running); | ||
| const [pMajor = 0, pMinor = 0, pPatch = 0] = parseParts(pinned); | ||
|
|
||
| if (rMajor !== pMajor) return rMajor < pMajor; | ||
| if (rMinor !== pMinor) return rMinor < pMinor; | ||
| return rPatch < pPatch; | ||
| }; |
There was a problem hiding this comment.
Prereleases compare as releases
The comparison removes prerelease metadata before ordering versions. A running 3.6.25-rc1 is therefore treated as equal to pinned 3.6.25, which hides the update action even though the prerelease is older than the final release. Using the semver implementation already imported by this service would preserve prerelease ordering.
| export const parseTraefikVersion = ( | ||
| imageString: string | null | undefined, | ||
| ): string | null => { | ||
| if (!imageString) { | ||
| return null; | ||
| } | ||
| const match = imageString.match( | ||
| /(?:^|\/)traefik:(?:v)?([0-9]+(?:\.[0-9]+)+(?:-[a-zA-Z0-9.]+)?)/, | ||
| ); | ||
| if (match?.[1]) { | ||
| return match[1]; | ||
| } | ||
| const genericMatch = imageString.match(/:v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)/); | ||
| if (genericMatch?.[1]) { | ||
| return genericMatch[1]; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| export const isVersionOlder = (running: string, pinned: string): boolean => { | ||
| const parseParts = (v: string) => { | ||
| const clean = v.replace(/^v/, "").split("-")[0] || ""; | ||
| return clean.split(".").map((n) => Number.parseInt(n, 10) || 0); | ||
| }; | ||
| const [rMajor = 0, rMinor = 0, rPatch = 0] = parseParts(running); | ||
| const [pMajor = 0, pMinor = 0, pPatch = 0] = parseParts(pinned); | ||
|
|
||
| if (rMajor !== pMajor) return rMajor < pMajor; | ||
| if (rMinor !== pMinor) return rMinor < pMinor; | ||
| return rPatch < pPatch; | ||
| }; | ||
|
|
||
| export interface TraefikVersionInfo { | ||
| pinnedVersion: string; | ||
| runningVersion: string | null; | ||
| runningImage: string | null; | ||
| isOutdated: boolean; | ||
| } | ||
|
|
||
| export const computeTraefikVersionInfo = ( | ||
| runningImage: string | null, | ||
| pinnedVersion: string, | ||
| ): TraefikVersionInfo => { | ||
| const runningVersion = parseTraefikVersion(runningImage); | ||
|
|
||
| let isOutdated = false; | ||
| if (runningVersion && pinnedVersion) { | ||
| isOutdated = isVersionOlder(runningVersion, pinnedVersion); | ||
| } | ||
|
|
||
| return { | ||
| pinnedVersion, | ||
| runningVersion, | ||
| runningImage, | ||
| isOutdated, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
Tests duplicate production logic
These tests redefine parseTraefikVersion, isVersionOlder, and a test-only computeTraefikVersionInfo instead of importing the production exports. They can keep passing after the real implementation or its image-reading and pinned-version wiring regresses, creating false confidence in the feature. Import the production helpers and mock only Docker inspection where needed.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary of Changes
Closes #5221
Linear: DOK-705
Context & Problem
When Dokploy is updated (e.g. from canary, tags, or upstream releases), Dokploy's internal pinned
TRAEFIK_VERSIONinpackages/server/src/setup/traefik-setup.tsoften advances to newer Traefik releases. However, the runningdokploy-traefikDocker container or Swarm service remains on whatever image tag it was initially deployed with. As reported in #5221, updating Dokploy does not update thedokploy-traefikcontainer to the newly pinned Traefik version.Solution
Docker Resource Image Inspection & Version Parsing:
readDockerResourceImage(resourceName, serverId)to inspect the current running Docker image on both standalone Docker containers and Swarm services (supporting local and remote servers).parseTraefikVersion(imageString)to robustly extract semantic version tags from image strings (supportingtraefik:v3.6.25,traefik:3.6.25, registry prefixes likedocker.io/library/traefik:v3.1.2, sha256 digest tags, and prereleases).isVersionOlder(running, pinned)to detect when the active Traefik container is running an outdated version compared to the pinnedTRAEFIK_VERSION.tRPC Settings Router:
settings.getTraefikVersionInfo: queries the running Traefik version, image tag, pinned version, and returns whether an update is available (isOutdated).settings.updateTraefik: admin mutation callingupdateTraefikToPinnedVersion(serverId)in the background so HTTP proxies do not time out, preserving existing environment variables and custom port mappings.Dashboard UI UX:
show-traefik-actions.tsx, when Traefik version drift is detected (isOutdated = true), an amber animated ping indicator appears on theTraefikbutton.useHealthCheckAfterMutationpolling/api/health.Testing:
apps/dokploy/__test__/traefik/server/traefik-version-drift.test.tsverifying image version parsing across multiple formats, version drift detection logic, equal/newer versions, and null container handling.The PR is not safe to merge until server ownership is enforced and update completion and environment preservation are made reliable.
Summary
Reviews (1) · Last reviewed commit: "feat(traefik): detect version drift and ..."