01Introduction
A reverse proxy is the traffic controller for your self-hosted services. It accepts incoming requests on ports 80/443 and forwards them to the right container. Understanding how they work is essential for any multi-service Docker setup.
02Why You Need a Reverse Proxy
Without a reverse proxy, each service needs its own port (app1:8080, app2:8081, etc.). Users must remember ports, SSL is complicated, and your firewall becomes a mess. A reverse proxy solves all of this.
1# Without reverse proxy:2http://server:8080 -> App 13http://server:8081 -> App 24http://server:8082 -> App 35# Users hate remembering ports67# With reverse proxy:8https://app1.domain.com -> App 19https://app2.domain.com -> App 210https://app3.domain.com -> App 311# Clean URLs, automatic HTTPS03How Reverse Proxies Work
The reverse proxy listens on ports 80 and 443. When a request arrives, it checks the Host header to determine which backend should handle it. It then forwards the request and relays the response.
1# Request flow:21. User visits https://app.example.com32. DNS resolves to your server IP43. Request hits your server on port 44354. Reverse proxy receives it, sees Host: app.example.com65. Proxy looks up routing rules, finds: app.example.com -> container "app" port 8076. Proxy forwards request to app container87. App container responds98. Proxy relays response to user1011# The app container never sees the user directly12# From the app's perspective, requests come from the proxy04Routing to Docker Containers
Reverse proxies connect to containers via Docker networks. They don't need exposed ports—just a shared network.
1services: 2 proxy: 3 image: traefik:v3.04 ports: 5 - "80:80"6 - "443:443"7 networks: 8 - web910 app: 11 image: myapp:1.012 # No ports exposed to host!13 networks: 14 - web15 # Proxy reaches app via Docker network at "app:80"1617networks: 18 web: 1920# The proxy resolves "app" to the container's IP via Docker DNS21# Traffic flows: Internet -> Proxy:443 -> app:80 (internal)Containers don't need published ports to be reached by a reverse proxy on the same Docker network.
05Host-Based Routing
The most common routing method. Different domains route to different backends.
1# Traefik example with labels2services: 3 app1: 4 labels: 5 - "traefik.http.routers.app1.rule=Host(`app1.example.com`)"67 app2: 8 labels: 9 - "traefik.http.routers.app2.rule=Host(`app2.example.com`)"1011# Caddy example (Caddyfile)12# app1.example.com {13# reverse_proxy app1:8014# }15# app2.example.com {16# reverse_proxy app2:8017# }1819# Nginx example20# server {21# server_name app1.example.com;22# location / { proxy_pass http://app1:80; }23# }06Path-Based Routing
Route based on URL path when you can't use separate domains. Useful for APIs or when subdomains aren't available.
1# Single domain, multiple apps via path2# example.com/app1 -> app1 container3# example.com/app2 -> app2 container4# example.com/api -> api container56# Traefik7services: 8 app1: 9 labels: 10 - "traefik.http.routers.app1.rule=Host(`example.com`) && PathPrefix(`/app1`)"11 - "traefik.http.middlewares.app1-strip.stripprefix.prefixes=/app1"12 - "traefik.http.routers.app1.middlewares=app1-strip"1314# Caddy (Caddyfile)15# example.com {16# handle_path /app1/* {17# reverse_proxy app1:8018# }19# handle_path /api/* {20# reverse_proxy api:300021# }22# }Path-based routing can break applications that expect to be at the root. Test thoroughly.
07SSL Termination
The reverse proxy handles SSL/TLS certificates. Backend containers receive plain HTTP traffic, simplifying their configuration.
1# SSL Termination flow:21. User connects with HTTPS (encrypted)32. Proxy decrypts using SSL certificate43. Proxy forwards to backend via HTTP (unencrypted, but internal)54. Backend responds65. Proxy encrypts response76. User receives HTTPS response89# Benefits:10# - Backends don't need SSL configuration11# - Centralized certificate management12# - Easier debugging (can see unencrypted traffic internally)1314# Security note:15# Traffic inside Docker network is unencrypted but isolated16# For high-security needs, use end-to-end encryption08Important Headers
The proxy must forward certain headers so backends know the real client information.
1# Essential headers to forward:2# X-Real-IP: Original client IP3# X-Forwarded-For: Chain of proxies4# X-Forwarded-Proto: Original protocol (http/https)5# X-Forwarded-Host: Original hostname67# Traefik does this automatically89# Nginx config10# location / {11# proxy_pass http://backend;12# proxy_set_header Host $host;13# proxy_set_header X-Real-IP $remote_addr;14# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;15# proxy_set_header X-Forwarded-Proto $scheme;16# }1718# Caddy does this automatically with reverse_proxyConfigure your applications to trust these headers from the proxy. Otherwise, they'll log the proxy's IP instead of the real client.
09WebSocket Support
WebSockets require special handling. The proxy must upgrade the connection and maintain it.
1# Traefik: WebSockets work automatically23# Nginx needs explicit configuration:4# location /ws {5# proxy_pass http://backend;6# proxy_http_version 1.1;7# proxy_set_header Upgrade $http_upgrade;8# proxy_set_header Connection "upgrade";9# proxy_read_timeout 86400;10# }1112# Caddy: WebSockets work automatically with reverse_proxy10Load Balancing
With multiple container replicas, the proxy can distribute traffic across them.
1services: 2 app: 3 image: myapp:1.04 deploy: 5 replicas: 367# Traefik automatically load balances across replicas8# It discovers all containers with matching labels910# Caddy (explicit upstream)11# app.example.com {12# reverse_proxy app:80 {13# lb_policy round_robin14# }15# }1617# Nginx (explicit upstream)18# upstream backend {19# server app1:80;20# server app2:80;21# server app3:80;22# }11Debugging Proxy Issues
When things don't work, here's how to diagnose.
1# Check if proxy is receiving requests2docker logs traefik -f34# Check if backend is reachable from proxy5docker exec traefik wget -qO- http://app:8067# Verify DNS resolution8docker exec traefik nslookup app910# Check routing rules (Traefik)11curl http://localhost:8080/api/http/routers # If dashboard enabled1213# Test with curl, show headers14curl -v https://app.example.com1516# Check certificate17echo | openssl s_client -connect app.example.com:443 2>/dev/null | openssl x509 -text