When you want to run multiple projects on a single server, setting up the right architecture takes some time upfront but saves your life in the long run. In this post, I'm sharing the Caddy + Docker architecture I use on my Hetzner server.
The Problem
I have several different projects: an API service, a Markdown viewer, a management panel, and this blog. Each needs to run under its own subdomain, get automatic SSL certificates, and be deployable independently of the others.
Architecture
The solution is simple but effective: Caddy sits at the front as a reverse proxy, each project runs independently with its own docker-compose.yml, and they all communicate over a shared Docker network.
Internet ──:80/:443──▶ Caddy (Auto SSL)
│
┌─────────────┼─────────────┐
▼ ▼ ▼
api.sdmr.dev md.sdmr.dev blog.sdmr.dev
(container) (container) (static files)
Why Caddy?
There are several reasons I chose Caddy over Nginx or Traefik:
- Automatic HTTPS: No extra steps needed to obtain and renew Let's Encrypt certificates
- Simple configuration: The Caddyfile syntax is extremely readable
- Zero-downtime reload: Seamless updates with
caddy reloadon config changes - Performance: Written in Go, low resource consumption
Setup
First, we create a shared Docker network for all projects to communicate:
docker network create proxy
Caddy has its own directory and compose file:
# /root/caddy/docker-compose.yml
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./sites:/etc/caddy/sites:ro
- ./static:/srv:ro
- caddy-data:/data
- caddy-config:/config
networks:
- proxy
The main Caddyfile is a single line — it imports all site configurations from separate files:
import /etc/caddy/sites/*
Adding a New Project
Adding a new project is dead simple. Three steps:
1. Add a DNS record
Create an A record for the subdomain.
2. Write the project's docker-compose.yml
services:
app:
build: .
container_name: my-project # unique name
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
3. Create a Caddy site config
# /root/caddy/sites/myproject.sdmr.dev
myproject.sdmr.dev {
reverse_proxy my-project:3000
}
Then start the project and reload Caddy:
cd /root/my-project
docker compose up -d --build
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
That's it. The SSL certificate is obtained automatically, and the project is instantly accessible.
Static Sites
For static sites like this blog, you don't even need a container. Just copy the files to Caddy's static directory and serve them with file_server:
blog.sdmr.dev {
root * /srv/blog
file_server
header {
-Server
X-Content-Type-Options nosniff
}
}
Conclusion
Thanks to this architecture, every project is completely independent. Stopping, updating, or deleting one doesn't affect the others. Caddy makes the whole process easy with automatic SSL and simple configuration.
Got a new project idea? A DNS record, a compose file, and a Caddy config — you're in production in five minutes.
If you have questions, feel free to reach out via GitHub.