Client IP Preservation#
Dokploy routes inbound traffic through Traefik, which sits behind Docker's overlay networking. Without explicit configuration, the real client IP is replaced by a Docker gateway address before it ever reaches Traefik — so X-Real-IP and X-Forwarded-For headers carry an internal RFC 1918 address instead of the actual client IP. Fixing this requires changes at two independent layers: Traefik's forwardedHeaders configuration, and the Docker network stack itself.
Why the IP Gets Lost#
Traffic flows:
Client → [public interface] → Docker bridge/overlay NAT → Traefik container → app service
Docker NATs packets through docker_gwbridge (the Docker gateway bridge) before they reach Traefik. By the time Traefik receives the connection, the source IP is the gateway's RFC 1918 address (e.g. 172.18.0.1), not the original client IP. This is an IPv4 problem but is especially visible with IPv6 clients, where Docker's IPv4-only network stack drops the IPv6 source address entirely via NAT .
Layer 1 — Traefik: forwardedHeaders#
Traefik's forwardedHeaders.trustedIPs controls which upstream proxies are trusted to pass X-Forwarded-For and X-Real-IP headers. If the Docker gateway IP is not in the trusted list, Traefik ignores headers forwarded by that hop.
The MainTraefikConfig type exposes both forwardedHeaders and proxyProtocol per entry point:
forwardedHeaders?: {
insecure?: boolean;
trustedIPs?: string[];
};
proxyProtocol?: {
insecure?: boolean;
trustedIPs?: string[];
};
Neither is set in the generated default config — the entry points for web and websecure are written with only address and http3/tls settings, with no forwardedHeaders block.
To add trusted IPs, edit /etc/dokploy/traefik/traefik.yml directly and restart the dokploy-traefik container. Example:
entryPoints:
web:
address: ':80'
forwardedHeaders:
trustedIPs:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
websecure:
address: ':443'
forwardedHeaders:
trustedIPs:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
Important:
forwardedHeaders.trustedIPsalone does not recover a client IP that was already lost to Docker NAT. It only tells Traefik to trust forwarded headers from those upstream IPs — it cannot reconstruct an address that was never forwarded .
Layer 2 — Docker: Dual-Stack Overlay Networks#
The structural cause of IPv6 IP loss is that dokploy-network, the Docker ingress network, and docker_gwbridge are all created as IPv4-only by default. IPv6 client traffic gets NATed to an IPv4 gateway address at the Docker level, before Traefik is involved.
The validated fix requires host-level changes :
-
Enable IPv6 tables in Docker daemon (
/etc/docker/daemon.json):{ "ip6tables": true }Then restart Docker.
-
Recreate
docker_gwbridge,ingress, anddokploy-networkas dual-stack:docker network rm -f docker_gwbridge docker network create docker_gwbridge --ipv4 --ipv6 --driver bridge docker network rm -f ingress # briefly disrupts published ports sleep 1 docker network create --ipv4 --ipv6 --driver overlay --ingress ingress docker network rm -f dokploy-network docker network create --driver overlay --attachable --ipv4 --ipv6 dokploy-networkFull diff: kedare's gist
Warning: Recreating the
ingressnetwork briefly interrupts all published ports. This is why dual-stack is not the default in Dokploy'sinstall.sh. Treat this as an opt-in, disruptive migration.
IPv4-only environments also benefit: with dual-stack networks, X-Forwarded-For can correctly carry the real client IPv4 address. However, forwardedHeaders.trustedIPs should still be configured to ensure Traefik trusts forwarded headers from the Docker gateway hops.
Swarm VIP Routing and the tasks. Prefix (Related Issue)#
A separate but related IP routing issue affects Traefik→app connectivity in Docker Swarm: Traefik resolves a service name to both the Swarm VIP and task IPs, and may intermittently route to the (non-routable) VIP, causing timeouts .
The fix is not in forwardedHeaders but at the Swarm service level:
- Run
docker service update --endpoint-mode dnsrr <service>to switch affected app services to DNS round-robin, which avoids VIP allocation. (Dokploy's own services usednsrron LXC hosts via the install script; user-deployed app services do not inherit this automatically .) - Alternatively, use
tasks.<service-name>as the upstream URL in the Traefik config — this bypasses the VIP but does not fix service-to-service DNS .
On Proxmox LXC (unprivileged containers), VIP-based overlay routing is unsupported at the kernel level; dnsrr is required for all services.
Key Files and References#
| Resource | Notes |
|---|---|
packages/server/src/utils/traefik/types.ts | forwardedHeaders and proxyProtocol type definitions |
packages/server/src/setup/traefik-setup.ts | Default entry point config (no forwardedHeaders set) |
| Issue #2349 | IPv6 X-Real-IP wrong; maintainer resolution and dual-stack setup |
| Issue #3480 | Swarm VIP stale routing; dnsrr vs tasks. prefix discussion |
| kedare's dual-stack gist | Full install.sh diff for dual-stack network setup |