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 reload on 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.