# How to self-host Caddy with Docker Compose

> Caddy Docker Compose setup as a reverse proxy with automatic HTTPS: Caddyfile, persistent certificate storage, HTTP/3, routing to other containers and reloads.

## Key facts

| Fact | Value |
|---|---|
| App | Caddy (https://appsgit.com/apps/caddy) |
| Difficulty | beginner |
| Time | about 10 minutes |
| Requirements | 1 vCPU / 256 MB RAM; Docker + Docker Compose v2; A domain with DNS records pointing at the server; Ports 80 and 443 reachable from the internet |
| Last updated | 2026-10-06 |

## What is Caddy?

Caddy is a modern web server and reverse proxy written in Go, best known for automatic HTTPS: give it a domain name and it obtains and renews a Let's Encrypt or ZeroSSL certificate with no extra steps. It supports HTTP/2 and HTTP/3 out of the box, has a famously short config format (the Caddyfile), and is open source under the Apache-2.0 license. In a self-hosted setup it is the front door that serves all your apps over HTTPS.

## Requirements

- A Linux server with Docker Engine and Docker Compose v2. Caddy is tiny: 256 MB of RAM is plenty.
- A domain with A records (or a wildcard) for each hostname you want to serve.
- Ports 80 and 443 open to the internet, and no other web server already using them.

## Step 1: Prepare the server

This guide assumes Ubuntu 24.04 with Docker installed from the [official Docker Engine guide](https://docs.docker.com/engine/install/ubuntu/).

```bash
mkdir -p ~/caddy/conf ~/caddy/site && cd ~/caddy
docker network create proxy
```

The shared `proxy` network lets Caddy reach other Compose projects by container name, so those apps never need published ports.

## Step 2: Create the Docker Compose file

This follows the Compose example from the official Caddy image docs. Save it as `docker-compose.yml`:

```yaml
services:
  caddy:
    image: caddy:2.11
    container_name: caddy
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - proxy

volumes:
  caddy_data:
  caddy_config:

networks:
  proxy:
    external: true
```

`443/udp` enables HTTP/3, and `NET_ADMIN` lets the QUIC library tune UDP buffer sizes. The `2.11` tag follows Caddy 2.11 patch releases.

Now create `conf/Caddyfile`:

```caddyfile
{
    email you@example.com
}

example.com {
    root * /srv
    file_server
}

app.example.com {
    reverse_proxy whoami:80
}
```

The first block sets the contact email for certificate expiry notices. The second serves static files from `./site`. The third proxies to a container named `whoami` on the `proxy` network.

## Step 3: Start and open the app

Start a test container on the shared network, then Caddy:

```bash
docker run -d --name whoami --network proxy --restart unless-stopped traefik/whoami:v1.12
echo '<h1>It works</h1>' > site/index.html
docker compose up -d
docker compose logs -f caddy
```

Within a few seconds the logs show "certificate obtained successfully" for each hostname. Open `https://example.com` for the static page and `https://app.example.com` to see the whoami response headers. HTTP requests redirect to HTTPS automatically.

## Step 4: Put it behind HTTPS

Caddy is the HTTPS layer, so this step is about adding your real apps. In each app's Compose file, join the external `proxy` network and remove its `ports:` section:

```yaml
services:
  myapp:
    image: your/app
    networks: [proxy]

networks:
  proxy:
    external: true
```

Add a site block such as `myapp.example.com { reverse_proxy myapp:3000 }` and reload. Removing published ports matters: Docker-published ports bypass `ufw`, so an app published on `0.0.0.0` stays reachable over plain HTTP no matter what the firewall says. If an app must stay on the host network, publish it on `127.0.0.1` and proxy to `host.docker.internal` after adding `extra_hosts: ["host.docker.internal:host-gateway"]` to Caddy.

## Backups and upgrades

Back up the `conf` folder and the `caddy_data` volume (certificates and keys):

```bash
docker run --rm -v caddy_caddy_data:/data -v "$PWD":/backup alpine \
  tar czf /backup/caddy-data-$(date +%F).tgz -C /data .
```

Apply Caddyfile changes without downtime:

```bash
docker compose exec -w /etc/caddy caddy caddy reload
```

To upgrade, run `docker compose pull && docker compose up -d`. For a new minor version, change the tag and skim the release notes.

## Troubleshooting

- **Certificate errors in the logs:** DNS does not point at this server yet, or ports 80 and 443 are blocked by a cloud firewall. Fix it and Caddy retries automatically.
- **502 Bad Gateway:** Caddy cannot reach the upstream. Check the container name, port and that both are on the `proxy` network.
- **"address already in use" on port 80:** Apache or Nginx is running on the host. Stop and disable it.
- **Too many certificates already issued:** you hit Let's Encrypt rate limits by recreating Caddy without its `/data` volume. Keep the volume and wait for the limit to reset.

## Next steps

Add `basic_auth` or forward authentication through Authentik for private apps, use `encode zstd gzip` for compression, set security headers with the `header` directive, and build a custom image with `xcaddy` if you need DNS-challenge plugins for wildcard certificates.

## FAQ

### What ports does Caddy use?

Caddy listens on port 80 for HTTP (redirected to HTTPS and used for certificate validation) and port 443 for HTTPS. Publishing 443/udp as well enables HTTP/3. The admin API on port 2019 stays inside the container.

### Is Caddy free?

Yes. Caddy is free and open source under the Apache-2.0 license, including automatic HTTPS, and there is no paid edition of the server.

### Caddy vs Nginx: which is better?

Caddy turns on HTTPS automatically, renews certificates itself and needs a few lines of config for a reverse proxy. Nginx is extremely fast and flexible but needs Certbot and longer configuration. For self-hosting apps, Caddy is usually the quicker and safer choice.

### Caddy vs Traefik?

Caddy uses a short, readable Caddyfile you edit by hand. Traefik discovers containers through Docker labels and reconfigures itself as they start and stop. Choose Caddy for simplicity, Traefik if you run many changing containers.

### Why must I persist Caddy's /data folder?

Caddy stores TLS certificates, private keys and its ACME account in /data. The official image docs warn it must not be treated as a cache. Losing it forces new certificates on every recreate and can hit Let's Encrypt rate limits.

### How do I reload the Caddyfile without downtime?

Run docker compose exec -w /etc/caddy caddy caddy reload. Caddy validates the new config and applies it gracefully, keeping existing connections open.

Prefer not to do it yourself? [appsgit installation help](https://appsgit.com/services/install) installs it on your server for a fixed quote.

---

Canonical page: https://appsgit.com/guides/caddy
Source: appsgit (https://appsgit.com), the app store for github. Data from the GitHub API, refreshed nightly.
Machine access: JSON API https://appsgit.com/api/v1/apps (OpenAPI: https://appsgit.com/openapi.json), MCP server https://mcp.appsgit.com/mcp, full index https://appsgit.com/llms-full.txt.
