Modern Web Stack with Docker Compose and Caddy Ingress
Set up a clean, production-ready multi-container web stack with Docker Compose and Caddy, featuring automated Let's Encrypt TLS and isolated networks.
Netbay Cloud Team
Netbay Engineering
On this page
Multi-container application stacks composed of a web API, database, and background worker frequently encounter configuration drift when certificates and proxy routes are managed manually. While Nginx has traditionally served as the default reverse proxy, configuring certbot renewals, ACME HTTP challenge routing, and OCSP stapling adds operational friction.
Caddy simplifies container ingress. By running Caddy in a Docker Compose stack alongside your application and database services, TLS certificates are issued and renewed automatically with Let's Encrypt and ZeroSSL. Combined with Docker bridge networks, you can completely isolate internal database ports from the host's public network interface.
Designing the Multi-Tier Compose Architecture
Our production architecture consists of three distinct tiers:
- **Public Ingress (Caddy)**: Binds to host ports 80 and 443, handles TLS termination, and routes requests.
- **Application Tier**: Runs your Node.js, Python, or Go web container. It communicates only on the internal Docker network.
- **Data Tier (PostgreSQL & Valkey)**: Stores application state on High-Speed SSD persistent volumes. It has no exposed host ports.
Security at the network level is achieved by defining two distinct bridge networks:
- webnet: Connects Caddy to the application service.
- backend: Marked with internal: true, connecting the application service to PostgreSQL and Valkey.
Because the database containers attach exclusively to the backend network, they are physically unreachable from the public internet, even if database root passwords were weak or misconfigured.
Create your project directory:
mkdir -p /srv/webstack && cd /srv/webstack
mkdir -p caddy_data caddy_configWriting docker-compose.yml
Create docker-compose.yml defining services, internal networks, and volume persistence:
# /srv/webstack/docker-compose.yml
services:
caddy:
image: caddy:2.8-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 QUIC support
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
networks:
- webnet
depends_on:
- app
app:
image: myapp:production
restart: unless-stopped
environment:
- PORT=3000
- DATABASE_URL=postgres://appuser:appsecret@db:5432/appdb
- REDIS_URL=redis://cache:6379
networks:
- webnet
- backend
depends_on:
- db
- cache
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=appsecret
- POSTGRES_DB=appdb
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backend
cache:
image: valkey/valkey:8-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- cache_data:/data
networks:
- backend
networks:
webnet:
driver: bridge
backend:
internal: true
volumes:
db_data:
cache_data:Notice that the backend network specifies internal: true. This prevents any container attached exclusively to the backend network from initiating outbound internet connections or exposing ports to the public interface.
Configuring the Caddyfile
Create the Caddyfile with security headers, automatic HTTPS, and gzip/zstd compression:
# /srv/webstack/Caddyfile
app.example.com {
# Automatic TLS with security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Reverse proxy to the application container name and internal port
reverse_proxy app:3000 {
health_uri /health
health_interval 5s
health_timeout 2s
health_status 200
}
encode zstd gzip
log {
output stdout
format console
}
}Launching and Validating the Stack
Launch the stack using Docker Compose:
# Start all containers in detached mode
sudo docker compose up -d
# Verify container health and logs
sudo docker compose ps
sudo docker compose logs -f caddyTest the public HTTPS endpoint and confirm security headers:
curl -I https://app.example.comThis stack delivers automated certificates, container network isolation, and reliable zero-touch maintenance.
Deploy your Docker Compose applications on Netbay Linux VPS in under 60 seconds with High-Speed SSD in Lucknow DC01 — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance