Reverse proxy: frontend (Traefik) and backend config (knowledge base)¶
Overview¶
HTTPS is terminated at Traefik; backends receive HTTP. Traefik automatically creates X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-For, X-Forwarded-Port headers from the actual client connection. Each backend must be configured to trust and use those headers (or equivalent overwrite settings).
Traefik (frontend)¶
Role: Terminate TLS, forward HTTP to backends, automatically create X-Forwarded- headers from the real client connection.
Where:* docker-compose.yml → scs--reverse-proxy → ports and command (not labels; labels are for routers/services).
Port binding configuration Real client IP addresses are preserved with the current configuration:
ports:
- "80:80"
- "443:443"
Current working setup: The deployment successfully preserves real client IPs for both IPv4 and IPv6 connections using the standard short-form port syntax. This works correctly when:
- The Docker network has IPv6 enabled (enable_ipv6: true)
- Traefik is configured as the edge proxy (no upstream proxy)
- The network driver is set to bridge
Required network configuration:
networks:
reverse-proxy:
name: reverse-proxy
enable_ipv6: true
driver: bridge
Docker will automatically assign appropriate IPv6 subnets. The enable_ipv6: true setting is critical for preserving real client IPs with the standard port binding syntax. Without IPv6 enabled, you may need to use mode: host explicitly.
Network creation command:
docker network create reverse-proxy --driver bridge --ipv6
For detailed explanations of IPAM, ULA addresses, gateways, and IPv6 subnetting concepts, see the Docker IPv6 Networking guide.
For detailed network creation information, troubleshooting, and reverse engineering details, see the Network creation guide.
Alternative (explicit) configuration: For maximum compatibility across different Docker versions and network configurations, you can use the long-form syntax with mode: host:
ports:
- target: 80
published: 80
protocol: tcp
mode: host # Explicitly bypasses docker-proxy
- target: 443
published: 443
protocol: tcp
mode: host # Explicitly bypasses docker-proxy
Note: The mode: host approach explicitly bypasses Docker's NAT layer but may have compatibility considerations with IPv6 and certain network setups. The current short-form configuration is validated and working correctly for this deployment.
Command configuration (when Traefik is the edge proxy):
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# No forwardedHeaders configuration needed - Traefik creates headers automatically
Note: forwardedHeaders.trustedIPs is ONLY needed if Traefik is behind another proxy. When Traefik is the edge proxy (first contact with clients), it automatically creates accurate X-Forwarded-* headers and you should NOT configure trustedIPs.
Drupal (backend)¶
Role: Trust the reverse proxy and use X-Forwarded- for scheme, host, port and client IP so generated URLs and logging are correct.
Where: 00_custom_configs/scs-manager-stack/drupal/reverse-proxy.settings.php (included from Drupal's settings.php).
Required config:
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = ['172.18.0.0/16', '172.19.0.0/16'];
$settings['reverse_proxy_trusted_headers'] = \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_FOR
| \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_HOST
| \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PORT
| \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PROTO;
WissKI stacks (Portainer / wisski-base-image)¶
Role: Each WissKI instance runs the wisski-base-image Drupal container behind Traefik → Varnish. The image entrypoint runs sync-reverse-proxy.sh, which writes reverse_proxy settings into the instance settings.php.
Where: SCS Manager → /admin/config/soda-scs-manager/settings → WissKI → Drupal reverse proxy addresses (wisski.instances.proxyAddresses). Passed to Portainer as env DRUPAL_PROXY_ADDRESSES when creating stacks.
Values:
| Value | When to use |
|---|---|
auto |
SCS deployment behind Traefik (default); image detects Docker network CIDRs on each boot |
none |
Standalone stacks without TLS edge (e.g. local dockerWissKI) |
172.20.0.0/16\|… |
Explicit CIDRs if auto-detection is not sufficient |
Note: Changing the setting in SCS Manager affects new stacks and the env on stack recreate. Existing instances also need the updated wisski-base-image and a Drupal container recreate so sync-reverse-proxy.sh runs.
Varnish (backend)¶
Role: Sits between Traefik and Drupal. Receives HTTP from Traefik with X-Forwarded- already selt. Must pass them through to Drupal and only add/complete them when missing (e.g. append Varnish IP to X-Forwarded-For).
Where: 00_custom_configs/scs-manager-stack/varnish/default.vcl.tpl → vcl_recv.
Required pattern (Traefik-safe):
# X-Forwarded-For: append Varnish IP if Traefik already set it
if (req.http.X-Forwarded-For) {
set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
# X-Forwarded-Proto: trust Traefik; default only if missing
if (!req.http.X-Forwarded-Proto) {
set req.http.X-Forwarded-Proto = "http";
}
# X-Forwarded-Host / X-Forwarded-Port: pass through, set from Host/Proto if missing
if (!req.http.X-Forwarded-Host) {
set req.http.X-Forwarded-Host = req.http.Host;
}
if (!req.http.X-Forwarded-Port) {
if (req.http.X-Forwarded-Proto == "https") {
set req.http.X-Forwarded-Port = "443";
} else {
set req.http.X-Forwarded-Port = "80";
}
}
Nextcloud (backend)¶
Role: Generate HTTPS URLs and accept requests for the public hostname. Nextcloud is behind Traefik (and in this stack often behind an internal nginx reverse-proxy container); it does not use X-Forwarded- by default for URL generation unless overwrite is set.
Where: scs-nextcloud-stack/docker-compose.yml → nextcloud--nextcloud--app → environment.
Required config:
environment:
- OVERWRITEHOST=${NEXTCLOUD_NEXTCLOUD_DOMAIN}
- OVERWRITEPROTOCOL=https
- NEXTCLOUD_TRUSTED_DOMAINS=localhost ${NEXTCLOUD_NEXTCLOUD_DOMAIN} ${NEXTCLOUD_ONLYOFFICE_DOMAIN} nextcloud--nextcloud-reverse-proxy
Keycloak (backend)¶
Role: Generate correct HTTPS redirect URIs for OAuth/OIDC and trust the proxy. Keycloak must use X-Forwarded- (via proxy headers mode) and know its public hostname without protocol.
Where: keycloak/docker-compose.override.yml → keycloak → environment (use environment: !override so base env is fully replaced).
Required config:
environment: !override
KC_HOSTNAME: ${KC_DOMAIN}
KC_HOSTNAME_STRICT: "true"
KC_HTTP_ENABLED: "true"
KC_PROXY_HEADERS: xforwarded
KC_PROXY_TRUSTED_ADDRESSES: 172.16.0.0/12,192.168.0.0/16
# ... DB, admin, cache, etc.
KC_HOSTNAME is the public hostname only (e.g. auth.example.com), no https://
- KC_PROXY_HEADERS=xforwarded makes Keycloak read X-Forwarded- headers for URL building and client IP logging
- KC_PROXY_TRUSTED_ADDRESSES specifies which proxy IPs to trust (Docker networks)
- KC_HTTP_ENABLED=true allows HTTP between Traefik and Keycloak
- KC_HOSTNAME_STRICT restricts redirects to that hostname
Pitfalls: Do not set KC_HOSTNAME to https://...; KC_PROXY (deprecated) should not be used in Keycloak 26+.
Verification¶
To verify that real client IPs are being forwarded correctly, use the scs--echo service (whoami container):
Test from external source:
curl https://echo.scs.sammlungen.io
Expected output should include:
X-Forwarded-For: <your-real-public-ip>
X-Real-Ip: <your-real-public-ip>
What to check:
- X-Forwarded-For contains your real public IP (not a Docker gateway IP like 172.x.x.x)
- X-Real-Ip matches your real public IP
- Works for both IPv4 and IPv6 connections
Validation results (2026-01-29):
- ✅ IPv4: Real client IPs correctly forwarded (e.g., 37.27.64.184)
- ✅ IPv6: Real client IPs correctly forwarded (e.g., 2a01:4f9:3081:3204::2)
- ✅ No docker-proxy processes interfering with port bindings
- ✅ Traefik automatically creates accurate X-Forwarded-* headers
- ✅ Network configuration: IPv6 enabled on reverse-proxy network
Verify network configuration:
docker network inspect reverse-proxy --format '{{.EnableIPv6}}'
# Should return: true
Summary¶
| Component | What to set |
|---|---|
| Docker Network | REQUIRED: Enable IPv6 on the reverse-proxy network: enable_ipv6: true and driver: bridge. This is critical for the standard port binding syntax to preserve real client IPs. |
| Traefik (edge) | Standard port bindings (- "80:80", - "443:443") preserve real client IPs correctly with IPv6-enabled bridge networks. No forwardedHeaders config needed - automatically creates X-Forwarded-* headers. Only use forwardedHeaders.trustedIPs if behind another proxy. Alternative: Use mode: host for explicit docker-proxy bypass. |
| Drupal (SCS Manager) | reverse_proxy, reverse_proxy_addresses, reverse_proxy_trusted_headers in settings |
| WissKI (Portainer stacks) | DRUPAL_PROXY_ADDRESSES=auto via SCS Manager settings; image sync-reverse-proxy.sh syncs settings.php |
| Varnish | In vcl_recv: pass through X-Forwarded-* from Traefik, only set defaults when missing; append to X-Forwarded-For |
| Nextcloud | OVERWRITEPROTOCOL=https, OVERWRITEHOST=<domain>, NEXTCLOUD_TRUSTED_DOMAINS |
| Keycloak | KC_PROXY_HEADERS=xforwarded, KC_PROXY_TRUSTED_ADDRESSES=<Docker networks>, KC_HOSTNAME=<domain> (no protocol), KC_HTTP_ENABLED=true, KC_HOSTNAME_STRICT=true |