docker.recipes
Networking10 min read

Reverse Proxies in Docker: How They Actually Work

Demystify reverse proxies and understand how they route traffic to your Docker containers.

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 1
3http://server:8081 -> App 2
4http://server:8082 -> App 3
5# Users hate remembering ports
6
7# With reverse proxy:
8https://app1.domain.com -> App 1
9https://app2.domain.com -> App 2
10https://app3.domain.com -> App 3
11# Clean URLs, automatic HTTPS

03How 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.com
32. DNS resolves to your server IP
43. Request hits your server on port 443
54. Reverse proxy receives it, sees Host: app.example.com
65. Proxy looks up routing rules, finds: app.example.com -> container "app" port 80
76. Proxy forwards request to app container
87. App container responds
98. Proxy relays response to user
10
11# The app container never sees the user directly
12# From the app's perspective, requests come from the proxy

04Routing 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.0
4 ports:
5 - "80:80"
6 - "443:443"
7 networks:
8 - web
9
10 app:
11 image: myapp:1.0
12 # No ports exposed to host!
13 networks:
14 - web
15 # Proxy reaches app via Docker network at "app:80"
16
17networks:
18 web:
19
20# The proxy resolves "app" to the container's IP via Docker DNS
21# 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 labels
2services:
3 app1:
4 labels:
5 - "traefik.http.routers.app1.rule=Host(`app1.example.com`)"
6
7 app2:
8 labels:
9 - "traefik.http.routers.app2.rule=Host(`app2.example.com`)"
10
11# Caddy example (Caddyfile)
12# app1.example.com {
13# reverse_proxy app1:80
14# }
15# app2.example.com {
16# reverse_proxy app2:80
17# }
18
19# Nginx example
20# 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 path
2# example.com/app1 -> app1 container
3# example.com/app2 -> app2 container
4# example.com/api -> api container
5
6# Traefik
7services:
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"
13
14# Caddy (Caddyfile)
15# example.com {
16# handle_path /app1/* {
17# reverse_proxy app1:80
18# }
19# handle_path /api/* {
20# reverse_proxy api:3000
21# }
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 certificate
43. Proxy forwards to backend via HTTP (unencrypted, but internal)
54. Backend responds
65. Proxy encrypts response
76. User receives HTTPS response
8
9# Benefits:
10# - Backends don't need SSL configuration
11# - Centralized certificate management
12# - Easier debugging (can see unencrypted traffic internally)
13
14# Security note:
15# Traffic inside Docker network is unencrypted but isolated
16# For high-security needs, use end-to-end encryption

08Important 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 IP
3# X-Forwarded-For: Chain of proxies
4# X-Forwarded-Proto: Original protocol (http/https)
5# X-Forwarded-Host: Original hostname
6
7# Traefik does this automatically
8
9# Nginx config
10# 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# }
17
18# Caddy does this automatically with reverse_proxy

Configure 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 automatically
2
3# 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# }
11
12# Caddy: WebSockets work automatically with reverse_proxy

10Load Balancing

With multiple container replicas, the proxy can distribute traffic across them.
1services:
2 app:
3 image: myapp:1.0
4 deploy:
5 replicas: 3
6
7# Traefik automatically load balances across replicas
8# It discovers all containers with matching labels
9
10# Caddy (explicit upstream)
11# app.example.com {
12# reverse_proxy app:80 {
13# lb_policy round_robin
14# }
15# }
16
17# 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 requests
2docker logs traefik -f
3
4# Check if backend is reachable from proxy
5docker exec traefik wget -qO- http://app:80
6
7# Verify DNS resolution
8docker exec traefik nslookup app
9
10# Check routing rules (Traefik)
11curl http://localhost:8080/api/http/routers # If dashboard enabled
12
13# Test with curl, show headers
14curl -v https://app.example.com
15
16# Check certificate
17echo | openssl s_client -connect app.example.com:443 2>/dev/null | openssl x509 -text