This quickstart is for educational purposes only and should not be used in production. It demonstrates how to configure Traefik as a TLS passthrough load balancer in front of a clustered Keycloak deployment.
In TLS passthrough mode, the load balancer forwards encrypted TLS traffic directly to the backend servers without decrypting it. Traefik operates at the TCP layer (Layer 4) and has no visibility into the HTTP content. The TLS connection is terminated by Keycloak itself, which means:
- Keycloak holds the TLS certificate and private key, not the proxy.
- Traefik cannot inspect, modify, or cache HTTP headers or the request body.
- End-to-end encryption is preserved between the client and Keycloak.
- Traefik listens on port 8443 and forwards raw TCP traffic to both Keycloak instances using round-robin.
It uses PROXY protocol v2 to pass the original client IP address to Keycloak.
It is attached to both the
frontendandbackendnetworks, making it the single entry point. - Keycloak 1 & 2 are clustered via embedded Infinispan.
They terminate TLS and share the same PostgreSQL database.
They live exclusively on the
backendnetwork, which is marked asinternaland unreachable from the host. - PostgreSQL provides the shared database for Keycloak on the
backendnetwork.
- Docker and Docker Compose
openssl(for certificate generation)
./generate-certs.sh <hostname>This example uses nip.io, a DNS service that maps 127.0.0.1.nip.io to 127.0.0.1, avoiding the need
to edit /etc/hosts:
./generate-certs.sh 127.0.0.1.nip.ioKC_HOST=<hostname> docker compose up -dFor example:
KC_HOST=127.0.0.1.nip.io docker compose up -dOnce the services are up, Keycloak is available at https://<hostname>:8443.
Log in to the admin console using credentials admin / admin.
The browser will show a certificate warning because the certificate is self-signed. This is expected and can be safely accepted for local testing.
Open http://127.0.0.1:8080/dashboard/ in a browser to verify that Traefik is running and both Keycloak backends are registered as TCP services.
This is a walkthrough through a graceful shutdown of one of the Keycloak instances:
-
Open http://127.0.0.1:8080/dashboard/ in a browser to verify that Traefik is running and both Keycloak backends are healthy.
-
Send a
TERMsignal to one of the Keycloak containers for a graceful shutdown (takes 30 seconds). Container exits with code 143.docker stop passthrough-keycloak1-1 -t 60
-
Observe that Traefik detects the backend is unavailable and stops routing traffic to it. Requests are still served by the remaining node.
-
Start the Keycloak container again:
docker start passthrough-keycloak1-1
-
Observe that Traefik automatically re-registers the backend and resumes routing traffic to it.
docker compose downTraefik separates its configuration into two files:
- traefik.yaml — static configuration loaded at startup (entry points, dashboard, and providers).
- keycloak.yaml — dynamic configuration for runtime routing rules, services, and health checks.
Both files are required for this setup to work correctly.
Entry point for TLS traffic:
entryPoints:
websecure:
address: ":8443"Defines the entry point on port 8443 where Traefik accepts incoming TCP/TLS connections.
Dashboard:
api:
insecure: trueEnables the Traefik dashboard on port 8080 without authentication. The dashboard is accessible at http://127.0.0.1:8080/dashboard/. This must never be used in production.
TCP router with TLS passthrough:
tcp:
routers:
keycloak-router:
rule: "HostSNI(`*`)"
service: "keycloak-service"
priority: 100
entryPoints:
- "websecure"
tls:
passthrough: trueHostSNI(*)matches all TLS connections regardless of the SNI hostname.priority: 100ensures this router takes precedence over any other TCP routers that may be defined.tls.passthrough: trueinstructs Traefik to forward the raw TLS stream without terminating it. Keycloak handles TLS termination.
Servers transport with PROXY protocol:
serversTransports:
kc-passthrough:
proxyProtocol:
version: 2- Defines a named transport
kc-passthroughthat enables PROXY protocol v2 on all backend connections. This allows Keycloak to see the original client IP address and requires Keycloak to be configured withKC_PROXY_PROTOCOL_ENABLED=true.
Load balancer with health check:
services:
keycloak-service:
loadBalancer:
serversTransport: kc-passthrough
# Use the JSON syntax to have explicit CR/LF encoding for send/expect.
healthCheck: {
interval: "5s",
timeout: "3s",
port: 9000,
send: "HEAD /health/ready HTTP/1.0\r\n\r\n",
expect: "HTTP/1.0 200 OK\r\ncontent-type: application/json; charset=UTF-8\r\ncache-control: no-store\r\n\r\n"
}
servers:
- address: "keycloak1:8443"
- address: "keycloak2:8443"serversTransport: kc-passthroughlinks the load balancer to the transport defined above, enabling PROXY protocol v2 on all backend connections. This requires Keycloak to be configured withKC_PROXY_PROTOCOL_ENABLED=true.- Traffic is distributed across both Keycloak instances using round-robin.
- Traefik checks each backend every 5s, marking a server as unhealthy if it does not respond within 3s. Unhealthy backends are automatically removed from the load balancer rotation and re-added once they recover.
Traefik health check:
When Traefik is set up for TLS passthrough and TCP services, it currently does not support an HTTPS or HTTP based health check for those services.
Making use of the available features, this setup uses a TCP-level send/expect check against Keycloak's management port (9000).
As the Traefik's TCP check does not support TLS, Keycloak's management endpoint is configured to use plain HTTP (via KC_HTTP_MANAGEMENT_SCHEME=http) so that Traefik can parse the response.
The check sends a raw HEAD /health/ready HTTP/1.0 request and matches the expected HTTP response headers.
This approach has known trade-offs:
- Fragile: The
expectstring must match the full response headers exactly, includingcontent-typecharset. Any change to the Keycloak response format (e.g. a different charset) will break the check. - No TLS: The management port runs plain HTTP for this to work, which is acceptable since port 9000 is only accessible from the internal
backendnetwork. - Alternatives considered: A simple TCP connect check (no
send/expect) would be less fragile but would not detect split-brain, database connectivity failures, or an overloaded node — it would only detect a dead process. Without a readiness probe,KC_SERVER_ASYNC_BOOTSTRAPwould also need to be disabled, and graceful shutdown behaviour would need a different approach (manually removing the node before stopping it).
Note: Traefik does not yet support native HTTP health checks for TCP services. Track upstream support at traefik/traefik#12606. Once that feature is available, the send/expect workaround can be replaced with a cleaner HTTP check.