Skip to content

feat(traefik): detect version drift and allow updating to pinned version (#5221) - #5446

Open
fliptrigga13 wants to merge 1 commit into
Dokploy:canaryfrom
fliptrigga13:fix/issue-5221-traefik-version-update
Open

fliptrigga13 wants to merge 1 commit into
Dokploy:canaryfrom
fliptrigga13:fix/issue-5221-traefik-version-update

Conversation

@fliptrigga13

@fliptrigga13 fliptrigga13 commented Sep 13, 2026

Copy link
Copy Markdown

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_VERSION in packages/server/src/setup/traefik-setup.ts often advances to newer Traefik releases. However, the running dokploy-traefik Docker container or Swarm service remains on whatever image tag it was initially deployed with. As reported in #5221, updating Dokploy does not update the dokploy-traefik container to the newly pinned Traefik version.

Solution

  1. Docker Resource Image Inspection & Version Parsing:

    • Added readDockerResourceImage(resourceName, serverId) to inspect the current running Docker image on both standalone Docker containers and Swarm services (supporting local and remote servers).
    • Added parseTraefikVersion(imageString) to robustly extract semantic version tags from image strings (supporting traefik:v3.6.25, traefik:3.6.25, registry prefixes like docker.io/library/traefik:v3.1.2, sha256 digest tags, and prereleases).
    • Added isVersionOlder(running, pinned) to detect when the active Traefik container is running an outdated version compared to the pinned TRAEFIK_VERSION.
  2. tRPC Settings Router:

    • Added settings.getTraefikVersionInfo: queries the running Traefik version, image tag, pinned version, and returns whether an update is available (isOutdated).
    • Added settings.updateTraefik: admin mutation calling updateTraefikToPinnedVersion(serverId) in the background so HTTP proxies do not time out, preserving existing environment variables and custom port mappings.
  3. Dashboard UI UX:

    • In show-traefik-actions.tsx, when Traefik version drift is detected (isOutdated = true), an amber animated ping indicator appears on the Traefik button.
    • Inside the dropdown menu, the current Traefik version is displayed in the header label.
    • When an update is available, an "Update to v{pinnedVersion}" action is rendered with a confirmation dialog explaining that existing environment variables and ports will be preserved, and executes with useHealthCheckAfterMutation polling /api/health.
  4. Testing:

    • Added unit test suite apps/dokploy/__test__/traefik/server/traefik-version-drift.test.ts verifying image version parsing across multiple formats, version drift detection logic, equal/newer versions, and null container handling.
    • 100% test pass rate (14/14 tests passing).

RetriggerConfidence Score: 1/5

The PR is not safe to merge until server ownership is enforced and update completion and environment preservation are made reliable.

Summary

  • Supports local and remote standalone/Swarm image inspection.
  • Exposes version information and update actions through the settings router.
  • Adds dashboard version display, drift indication, confirmation, and health polling.
  • Adds parsing and comparison tests, although those tests currently exercise copied implementations.

Reviews (1) · Last reviewed commit: "feat(traefik): detect version drift and ..."

Comment on lines 131 to +153
});
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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

Comment on lines +143 to +149
// Avoids proxy timeouts (520) while Traefik is recreated.
void updateTraefikToPinnedVersion(input?.serverId).catch((err) => {
console.error(
"updateTraefik background updateTraefikToPinnedVersion:",
err,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Update reports success early

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:

Comment on lines +605 to +609
const preparedEnv = prepareEnvironmentVariables(env);

await writeTraefikSetup({
env: preparedEnv,
additionalPorts: ports,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

Comment on lines +556 to +567
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;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +3 to +59
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,
};
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Updating Dokploy does not update the dokploy-traefik container to the newly pinned Traefik version

1 participant