Running multiple Docker containers often leads to issues like irregular port numbers in URLs, lack of HTTPS, and manual service tracking. This becomes unmanageable quickly. Ask anyone who’s juggled five ports at 2 a.m., it’s not fun.
A single reverse proxy container addresses these issues. It handles TLS termination, routes traffic by hostname, and shields backend containers from direct public exposure. This article provides Docker Compose files for Nginx, Traefik, and Caddy with working HTTPS and container routes.
1. Pick the Reverse Proxy That Fits Your Setup
Choose based on your needs, not popularity. As of 2026, all three are solid choices.
Quick Decision Table
| Tool | Best for | HTTPS / Let’s Encrypt effort | Docker auto-discovery | Ongoing config effort | Common consideration |
|---|---|---|---|---|---|
| Caddy | Simple setups, homelab, quick HTTPS | Automatic | No (static Caddyfile by default) | Low | Wildcard certificates need custom build or DNS plugin |
| Traefik | Dynamic microservices, changing containers | Low (label-based) | Yes (native) | Low after setup | Docker socket exposure; avoid --api.insecure=true in production |
| Nginx | High-traffic applications, maximum control | Manual (Certbot) | No | High | Manual certificate renewal, static configuration |

Choose Based on Your Situation
- Choose Caddy if straightforward HTTPS with minimal configuration is key.
- Choose Traefik if you have many Docker services that change frequently and prefer label-based routing.
- Choose Nginx if you need extensive control, high performance, and are comfortable with manual configuration and Certbot.
Real-World Tradeoffs
- Performance: Nginx performs well at around 25,000+ requests per second with an average latency of about 4ms. Traefik handles about 18,000–20,000 requests per second with an average latency of about 5ms. Caddy is reliable but has slightly higher latency (around 6–7ms average), which is acceptable for most tasks.
- Static vs dynamic config: Nginx uses static configuration, requiring file edits and reloads. Traefik uses dynamic configuration; add labels to a container, and routes update instantly without restarting.
- Caddy automatically handles HTTPS for public domains upon the first request, eliminating the need for Certbot or cron jobs.
- Traefik requires mounting the Docker socket, which needs careful security management (see DevOps best practices).
- Nginx offers the most flexibility for custom headers, upstreams, and Web Application Firewall (WAF) integration.
2. Target Setup Used Throughout
Example Architecture
One reverse proxy container routes:
app1.example.com→whoamicontainerapp2.example.com→whoami2container
Placeholders Used in All Examples
| Setting | Value |
|---|---|
| Domain | example.com |
| Subdomains | app1.example.com, app2.example.com |
| ACME email | you@example.com |
| Shared Docker network | proxy |
| Compose project folder | reverse-proxy/ |
3. Prerequisites
- Docker and Docker Compose installed on the server.
- DNS A/AAAA records for
app1.example.comandapp2.example.compointing to your server IP. - Ports 80 and 443 open on your firewall and/or router.
- Two backend containers running HTTP inside Docker (the
whoamiimage works well for testing).
4. Create the Shared Docker Network
This step applies regardless of the chosen proxy.
- Run
docker network create proxy. - In every Compose file, attach both the proxy service and all application services to this network.
- Use Docker service names for upstream addresses (e.g.,
http://whoami:80), not IP addresses orlocalhost.
Common mistake:
localhostinside a container refers to that container, not the host machine or another container. Always use service names. We’ve all done it at least once.
5. Option A , Caddy as a Docker Reverse Proxy (Fastest HTTPS)
Caddy manages certificate issuance and renewal automatically. No additional tools are required.
Create the Folders and Files
mkdir -p reverse-proxy/caddy
touch reverse-proxy/caddy/Caddyfile
Caddy Docker Compose
version: "3.8"
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
