Skip to content

feat(traefik): allow pinning the Traefik container to a static address - #5805

Open
fahrim wants to merge 4 commits into
Dokploy:canaryfrom
fahrim:feat/traefik-static-ip
Open

fahrim wants to merge 4 commits into
Dokploy:canaryfrom
fahrim:feat/traefik-static-ip

Conversation

@fahrim

@fahrim fahrim commented Oct 28, 2026 •

Copy link
Copy Markdown

What is this PR about?

Dokploy recreates the Traefik container whenever the webserver is reloaded, and the container is created without an address, so it gets a new one from the dokploy-network every time:

NetworkingConfig: {
    EndpointsConfig: {
        "dokploy-network": {},
    },
},

Any setup that points at Traefik's address breaks on the next recreate:

  • split-DNS: a DNS server that answers internal hostnames with Traefik's container address silently starts resolving to an address that no longer exists. A real case: a WireGuard VPN whose clients resolve internal hostnames through the tunnel to Traefik's container address - every reload leaves every client resolving an address that is gone, and the only fix is re-creating Traefik by hand with --ip.
  • anything else that references the address: firewall rules, SNAT targets, monitoring, host entries.

This adds a TRAEFIK_STATIC_IP setting that pins the container to a fixed address on the dokploy-network, so those setups keep working across recreates.

Changes

The setting. TRAEFIK_STATIC_IP is read from the "Traefik Env" setting (Settings -> Web Server -> Edit Traefik Env), which is read from the running container and written back on every recreate. No schema migration is needed and the address survives a recreate. Callers that don't pass an env (the startup path in apps/dokploy/setup.ts) inherit it from the running container.

Applying the address. In standalone mode the address is passed to the container:

NetworkingConfig: {
    EndpointsConfig: {
        "dokploy-network": staticIp
            ? { IPAMConfig: { IPv4Address: staticIp } }
            : {},
    },
},

Validation. A wrong address otherwise fails with Docker's invalid endpoint settings, which says nothing about the cause. The address is checked before the container is created:

  • it must be a valid IPv4 address
  • it must be inside one of the IPv4 subnets of the dokploy-network
  • it must not be used by another container - the Traefik containers themselves keep the address across a recreate, so they don't count as a conflict

Swarm mode. Swarm assigns an address per task, so there is nothing to pin. Instead of ignoring the setting silently, swarm mode fails with TRAEFIK_STATIC_IP is not supported in swarm mode.

Known limitation: overlay networks

On an overlay network the swarm assigns the addresses of the tasks from the same pool as the network. A pinned address can therefore be taken by a task after a reboot, without anyone changing the setting, and Traefik would then not start.

The recreate warns about it, and the error of a taken address names the container that holds the address and explains the overlay case:

TRAEFIK_STATIC_IP "10.0.1.45" is already used by the container "some-task.1".
On an overlay network a swarm task can be given the same address after a reboot.

To avoid it, the address can be placed outside the pool of the network, or the network can use a smaller pool. Happy to restrict the setting to bridge networks if that is preferred - the validation knows the driver of the network.

Notes for review

  • Changing the address requires reloading the webserver, like the other Traefik settings.
  • I went with an env var to avoid a schema migration, following the existing TRAEFIK_PORT / TRAEFIK_SSL_PORT / TRAEFIK_VERSION pattern. Happy to move it to a webServerSettings column instead if that is preferred.
  • For swarm the alternatives would be a stable service VIP through the network's IPAM aux-address, or leaving swarm out of scope. I chose the explicit error because the VIP route needs a different mechanism and shouldn't be guessed - happy to switch to it if you prefer.
  • The validation runs before the old container is renamed and stopped, so a rejected address leaves the running Traefik untouched.

Testing

  • New unit tests in apps/dokploy/__test__/traefik/standalone-traefik-static-ip.test.ts: the address is applied, no address leaves the endpoint untouched, the address and the env are inherited from the running container, the address survives two consecutive recreates, several IPv4 subnets, a dual-stack network, the Traefik containers are not a conflict, the validation errors, the pre-save validation and the overlay caveat.
  • Full test suite, typecheck and Biome pass.
  • Verified against a real Docker daemon (28.5.0) that EndpointsConfig + IPAMConfig.IPv4Address results in the container actually getting that address, and that Docker rejects a conflicting address with Address already in use - which is the failure the new validation turns into an actionable message.

Checklist

Issues related (if applicable)

Closes #5793

Screenshots (if applicable)

N/A - no UI change beyond a placeholder hint in the "Traefik Env" editor.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with no new actionable issues found.

Summary

Adds TRAEFIK_STATIC_IP to keep Traefik at a fixed container address across recreates.

  • Standalone Traefik can keep a chosen address across container recreates.
  • Swarm setup names the unsupported static-address setting.

Reviews (4) · Last reviewed commit: "feat(traefik): warn about the overlay ca..." · Reviewed by Greptile

Dokploy recreates the Traefik container whenever the webserver is
reloaded, and the container is created without an address, so it gets a
new one from the network every time. Any setup that points at Traefik's
address breaks on the next recreate:

- split-DNS: a DNS server answering internal hostnames with Traefik's
  container address silently resolves to an address that no longer exists
- anything else that references the address: firewall rules, SNAT
  targets, monitoring, host entries

This adds a TRAEFIK_STATIC_IP setting that pins the container to a fixed
address on the dokploy-network, so those setups keep working across
recreates.

The value is read from the "Traefik Env" setting, which is read from the
running container and written back on every recreate, so no schema
migration is needed and the address survives a recreate. Callers that
don't pass an env (the startup path) inherit it from the running
container.

The address is validated before the container is created, so a
misconfiguration reports its cause instead of Docker's "invalid endpoint
settings":

- it must be a valid IPv4 address
- it must be inside the dokploy-network subnet
- it must not be used by another container (the Traefik containers
  themselves keep the address across a recreate, so they don't count)

Swarm assigns an address per task, so there is nothing to pin there.
Instead of ignoring the setting silently, swarm mode fails with a clear
error.

Fixes Dokploy#5793
// Restores a container left behind by an interrupted recreate before the
// address is read, so the value can be inherited from it.
await restoreInterruptedTraefik(serverId);
const staticIp = await resolveStaticIp(docker, env);

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 Saved address disappears

When initializeStandaloneTraefik() runs without env, resolveStaticIp reads the saved address, but the replacement container still receives Env: undefined. This pins the address once without saving the setting. If the Dokploy process does not also define TRAEFIK_STATIC_IP, the next recreate loses the pin and can break DNS or firewall rules that point at Traefik. Preserve the inherited setting in the replacement container and test two consecutive recreates.

Comment on lines +123 to +125
const subnet = network.IPAM?.Config?.find(
(config: { Subnet?: string }) => config.Subnet,
)?.Subnet;

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 Valid addresses are rejected

assertStaticIpIsUsable checks only the first subnet in IPAM.Config. If a dual-stack network lists IPv6 first, every IPv4 address is rejected because isAddressInSubnet only accepts IPv4. An address in a later IPv4 subnet is also rejected. This blocks reloads with otherwise valid settings. Check the configured IPv4 subnets rather than whichever subnet appears first.

Comment on lines +181 to +182
if (staticIp) {
await assertStaticIpIsUsable(docker, staticIp);

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 Rejected settings look saved

The new address errors never reach users saving “Traefik Env”. writeTraefikEnv runs writeTraefikSetup in the background and only logs failures. Since these checks leave the old Traefik running, the health check succeeds and the editor reports “Traefik Env Updated” even for an invalid or conflicting address. Run these checks before returning success, or expose the background result to the editor.

…te before saving

Address the review feedback on the static address handling:

- The replacement container was created with `Env: undefined` when the
  caller didn't pass an env (the startup path), so the inherited address
  was pinned once and then dropped on the next recreate. The env is now
  inherited together with the address, and the container keeps it.

- The subnet check only looked at the first entry of `IPAM.Config`, so a
  dual-stack network with the IPv6 subnet first rejected every IPv4
  address, and an address in a second IPv4 subnet was rejected too. All
  IPv4 subnets of the network are checked now.

- The new errors only reached the log of the background recreate, so the
  editor reported a successful update for an address that was rejected.
  `validateTraefikStaticIp` checks the address before it is stored, and
  the editor shows the error.
@fahrim

fahrim commented Oct 28, 2026

Copy link
Copy Markdown
Author

Thanks for the review - all three findings were valid and are fixed in 1229252.

1. "Saved address disappears" - correct. The startup path called initializeStandaloneTraefik() without an env, so the replacement container was created with Env: undefined: the address was pinned once, and the next recreate read an env that no longer had it. The env is now inherited together with the address and the replacement container keeps it, so the setting survives any number of recreates. Added a test that recreates twice and checks the address is still pinned after the second one.

2. "Valid addresses are rejected" - correct. The check only looked at the first entry of IPAM.Config. It now collects every IPv4 subnet of the network and accepts the address if it is in any of them, so a dual-stack network with the IPv6 subnet first and a network with several IPv4 subnets both work. Added tests for both cases.

3. "Rejected settings look saved" - correct. writeTraefikEnv runs the recreate in the background and only logs failures, so the editor reported a successful update for an address that had been rejected. validateTraefikStaticIp now checks the address in the mutation before it returns, so the error reaches the editor (the dialog already renders the mutation error).

writeTraefikEnv: adminProcedure
	.mutation(async ({ input, ctx }) => {
		const envs = prepareEnvironmentVariables(input.env);
		const ports = await readPorts("dokploy-traefik", input?.serverId);

		// Check the pinned address before storing it: the recreate runs in
		// the background, so a wrong value would only be logged and the
		// editor would report a successful update.
		await validateTraefikStaticIp({ env: envs, serverId: input.serverId });
		...

Tests: 14 in standalone-traefik-static-ip.test.ts (the address is applied, no address leaves the endpoint untouched, the address and the env are inherited from the running container, the address survives two consecutive recreates, several IPv4 subnets, a dual-stack network, the Traefik containers are not a conflict, the three validation errors, and the pre-save validation). Full suite: 1569 passed; the 3 failures in application.real.test.ts are the ones that need Nixpacks and also fail on canary without this PR. Typecheck and Biome pass.

@fahrim
fahrim force-pushed the feat/traefik-static-ip branch from bf2684a to 1229252 Compare October 28, 2026 20:07
@fahrim fahrim closed this Oct 28, 2026
@fahrim fahrim reopened this Oct 28, 2026
Comment on lines +203 to +205
const staticIp = hasEnvValue(resolvedEnv, STATIC_IP_ENV_KEY)
? readEnvValue(resolvedEnv, STATIC_IP_ENV_KEY)
: TRAEFIK_STATIC_IP;

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 Local address overrides server settings

If the Dokploy process has TRAEFIK_STATIC_IP set, an explicit env that omits the key now falls back to that address. Previously, the supplied env took precedence. Saving a remote server’s settings can therefore apply Dokploy’s local address and fail the reload if it is outside the remote subnet or already used. Deleting the key in the editor also no longer removes the pin.

Keep the process default limited to callers that omit env.

The process default (TRAEFIK_STATIC_IP in the Dokploy process env) was
applied whenever the env didn't set the key. An env is the operator's
choice though, so:

- saving a remote server's settings could apply the address of the local
  Dokploy process, which is outside the remote subnet or already taken
  there
- removing the key in the editor no longer removed the pin

The process default now only applies to the callers that don't pass an env
at all, which is the startup path. The same applies to the swarm mode
check.

Added tests for both cases.
@fahrim fahrim closed this Oct 28, 2026
@fahrim fahrim reopened this Oct 28, 2026
@fahrim

fahrim commented Oct 28, 2026

Copy link
Copy Markdown
Author

Good catch - fixed in 8da117d.

The process default (TRAEFIK_STATIC_IP in the Dokploy process env) was applied whenever the env didn't set the key. An env is the operator's choice, so:

  • saving a remote server's settings could apply the address of the local Dokploy process, which is outside the remote subnet or already taken there
  • removing the key in the editor no longer removed the pin

The process default now only applies to the callers that don't pass an env at all, which is the startup path:

const resolvedEnv = await resolveTraefikEnv(docker, env);
// The process default only applies to callers that don't pass an env at
// all: an env is the operator's choice, and for a remote server the
// address of the local Dokploy process would be the wrong one.
const staticIp = hasEnvValue(resolvedEnv, STATIC_IP_ENV_KEY)
	? readEnvValue(resolvedEnv, STATIC_IP_ENV_KEY)
	: env === undefined
		? TRAEFIK_STATIC_IP
		: "";

The swarm mode check got the same treatment.

Two tests cover it: the process default is applied on the startup path, and it is not applied when an env is given without the key (the address stays unset). Tests: 16 in the file, 155 in the traefik suite. Typecheck and Biome pass.

@fahrim

fahrim commented Oct 28, 2026

Copy link
Copy Markdown
Author

One clarification on the test results, to be precise:

The full suite has 4 failures on my machine, and all 4 also fail on pristine canary without this PR (I checked by running them on 96fb004):

Everything else passes: 1570 passed, 5 skipped. The traefik suite is 155/155.

@greptile-apps the review on the latest commit is clean, and the four findings from the previous rounds are addressed:

Finding Fixed in
Saved address disappears 1229252
Valid addresses are rejected 1229252
Rejected settings look saved 1229252
Local address overrides server settings 8da117d

@greptile-apps

greptile-apps Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for clarifying. I’ll record the test results precisely: 1,570 passed and 5 skipped; the 4 failures also reproduce on pristine canary (3 Nixpacks-dependent application tests and 1 environment-dependent backup timeout), while the Traefik suite passes 155/155. The latest review is clean, and the four earlier findings are addressed.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

On an overlay network the swarm assigns the addresses of the tasks from
the same pool as the network, so a pinned address can be taken by a task
after a reboot without anyone changing the setting - and Traefik would
then not start.

The recreate now warns about it, and the error of a taken address
explains it instead of only naming the container that holds the address.
@fahrim fahrim closed this Oct 30, 2026
@fahrim fahrim reopened this Oct 30, 2026

This branch has not been deployed

No deployments
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.

Allow pinning the Traefik container to a stable address (TRAEFIK_STATIC_IP)

1 participant