14.3.1.2 Single Reverse Proxy
A focused guide to Single Reverse Proxy, connecting core concepts with practical Docker and container operations.
A single reverse proxy in front of one or more Docker containers is the pattern of placing one proxy process, such as Nginx, Traefik, or Caddy, between the public internet and the application, handling TLS termination, request routing, and a handful of cross-cutting concerns that are easier to manage in one place than duplicated inside every backend service.
What the proxy is responsible for
A reverse proxy in this pattern typically owns TLS certificate handling, routes incoming requests to the correct backend container based on hostname or path, and may apply rate limiting, compression, or caching before a request ever reaches the application:
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/api.example.com.crt;
ssl_certificate_key /etc/nginx/certs/api.example.com.key;
location / {
proxy_pass http://my-api:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Centralizing TLS termination here means individual application containers never need to handle certificates directly; they receive plain HTTP traffic on an internal network and trust that the proxy has already verified the connection's encryption.
Running the proxy as its own container
The proxy itself is typically deployed as a container, sitting on the same Docker network as the backend services it routes to, with only its own ports published to the host:
services:
proxy:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
networks:
- app-network
api:
image: my-api:latest
networks:
- app-network
# no ports published directly to the host
networks:
app-network:
Because the api service has no published ports, it is reachable only through the proxy, which removes any path for a client to bypass the proxy's TLS termination and routing logic entirely.
Routing to multiple services by hostname or path
A single reverse proxy commonly fronts more than one backend, distinguishing between them by the hostname or path of the incoming request:
server {
listen 443 ssl;
server_name app.example.com;
location / {
proxy_pass http://frontend:3000;
}
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://backend-api:8080;
}
}
This keeps the public-facing surface consistent (one IP address, one set of certificates managed in one place) while still allowing each backend service to be deployed, scaled, and updated entirely independently behind it.
Automatic configuration with Traefik
Traefik and similar proxies designed specifically for containerized environments can discover backend services automatically through Docker labels, rather than requiring a manually maintained configuration file:
services:
proxy:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
api:
image: my-api:latest
labels:
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.services.api.loadbalancer.server.port=8080"
This approach means adding a new backend service to the routing table is just adding labels to its own Compose definition, with no separate proxy configuration file to keep in sync as services are added or removed.
Health checking and failover at the proxy layer
A reverse proxy can be configured to check the health of the backends it routes to and stop sending traffic to one that has become unhealthy, even before the container runtime's own restart logic has reacted:
upstream backend {
server api-1:8080 max_fails=3 fail_timeout=30s;
server api-2:8080 max_fails=3 fail_timeout=30s;
}
http:
services:
api:
loadBalancer:
healthCheck:
path: /healthz
interval: "10s"
This health-aware routing is what makes patterns like rolling updates and canary deployments work cleanly: the proxy stops sending traffic to a replica the moment it reports unhealthy, regardless of whether that replica is mid-deployment, crashed, or otherwise degraded.
The proxy as a single point of failure
Running one reverse proxy in front of everything introduces exactly the risk its simplicity implies: if that one proxy container goes down, every backend service it fronts becomes unreachable, even if those backends are themselves healthy and running normally. Mitigating this typically means running the proxy itself with a restart policy and, for higher availability requirements, more than one proxy instance behind a layer that does not itself have a single point of failure, such as DNS round-robin or a cloud load balancer in front of multiple proxy replicas:
docker run -d --restart=unless-stopped --name proxy nginx:alpine
services:
proxy:
image: nginx:alpine
deploy:
replicas: 2
Certificate renewal in a single-proxy setup
Because TLS termination is centralized at the proxy, certificate renewal also becomes a single, centralized concern rather than something each backend service needs to handle independently:
docker run --rm -v ./certs:/etc/letsencrypt certbot/certbot certonly \
--webroot -w /var/www/certbot -d api.example.com
docker exec proxy nginx -s reload
A renewal process that updates the certificate files and then reloads (rather than restarts) the proxy avoids any interruption to in-flight connections during the renewal itself.
Common mistakes
- Publishing backend service ports directly to the host in addition to routing through the proxy, creating an unintended path that bypasses the proxy's TLS termination and routing logic entirely.
- Running the proxy as a single, unmonitored instance with no restart policy, turning it into an unmitigated single point of failure for every service behind it.
- Maintaining a manually written proxy configuration file that drifts out of sync with the actual set of running backend services, especially in setups that could instead use label-based automatic discovery.
- Forgetting to configure health checks at the proxy layer, leaving it routing traffic to backends that the container runtime itself already considers unhealthy.
- Restarting rather than reloading the proxy during certificate renewal, causing a brief, avoidable interruption to active connections.
A single reverse proxy is an effective, widely used pattern for centralizing TLS, routing, and health-aware traffic management in front of one or more Docker services, provided the proxy itself is treated as critical infrastructure deserving its own restart policy, monitoring, and, where availability requirements justify it, redundancy.