Infrastructure guide

Caddy Docker Compose Reverse Proxy: Automatic HTTPS Without Fragile State

Run Caddy as a durable reverse proxy with automatic HTTPS, persistent certificate state, clear Docker networking, and safe reloads.

Published and reviewed by OpenAlt · September 25, 2026

Detailed image of illuminated server racks showcasing modern technology infrastructure.
Photo by panumas nikhomkhai on Pexels

TL;DR

Caddy is a practical Docker Compose reverse proxy when you want automatic HTTPS without maintaining a large label-based control plane. Run the official Caddy image, mount a persistent /data volume for certificates and other runtime state, persist /config, and keep routing rules in a short Caddyfile.

For a public hostname, point DNS to the server, expose ports 80 and 443, and use a hostname in the Caddyfile. Caddy can then obtain and renew certificates automatically when the hostname resolves correctly and the server is publicly reachable. Private names do not qualify for automatic public certificates.

The complete pattern is:

  1. Create a shared Docker network.
  2. Start Caddy with persistent caddy_data and caddy_config volumes.
  3. Attach the application to the same network.
  4. Proxy to the application by its Compose service name.
  5. Validate and reload the Caddyfile.
  6. Back up the Caddy state before upgrades.

TOC

Choose a public or private hostname

Use a public DNS name when you want a publicly trusted certificate. For example, app.example.com must resolve to the server running Caddy, and inbound TCP ports 80 and 443 must reach the Caddy container. The official automatic HTTPS documentation explains the hostname and reachability requirements.

Do not promise automatic public certificates for names such as app.local, internal-only DNS records, or hostnames reachable only through a private network. Those names can still be served through HTTP, internal certificates, or certificates issued by your own private authority, but that is a different trust model.

Before starting, decide whether another service already owns ports 80 or 443. Only one process can bind each host port. If Caddy is the public entry point, expose those ports from Caddy and keep application ports private.

Create the Compose stack

The official Caddy installation guide links to the official Docker image and its recommended Compose usage. This example keeps Caddy and a sample application in one project:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    networks:
      - proxy

  app:
    image: nginx:alpine
    restart: unless-stopped
    expose:
      - "80"
    networks:
      - proxy

volumes:
  caddy_data:
  caddy_config:

networks:
  proxy:

The two named volumes are essential. The official Docker image documentation requires persistent /data and recommends persisting /config. The /data directory contains certificates, private keys, and other operational state. Losing it can cause unnecessary certificate reissuance and can make a previously stable deployment difficult to recover. Persisting /config keeps Caddy’s configuration-related state durable as well.

The application uses expose, not ports, because it does not need to publish a host port. Caddy reaches it over the Docker network. This reduces the number of externally reachable services and makes Caddy the single HTTPS boundary.

Write a minimal Caddyfile

Create Caddyfile beside compose.yaml:

app.example.com {
    reverse_proxy app:80
}

This is enough for a basic reverse proxy. Caddy matches requests for app.example.com, obtains HTTPS when requirements are satisfied, and forwards traffic to the container named app on port 80. The official reverse proxy directive documentation covers upstream selection, load balancing, health checks, headers, and transport options.

Keep the first configuration small. Add headers, authentication, caching, or health checks only when the application needs them. A short Caddyfile is easier to audit, review, and restore than a large collection of labels spread across multiple services.

Name the network and upstream correctly

Inside a Compose network, containers reach one another through service names. In this example, app is the upstream hostname and 80 is the port exposed by the application container. Do not use localhost:80: from inside the Caddy container, localhost means Caddy itself.

If Caddy and the application live in separate Compose projects, create one external network and attach both projects to it:

networks:
  proxy:
    external: true

Create the network once with your normal Docker workflow, then use the same network name in both projects. The target service must be attached to that network, and the hostname in reverse_proxy must resolve to the other container. If the projects use different service names, use the actual reachable container or network alias.

An IT professional operates a computer in a server room, managing network systems and connected devices.
Photo by panumas nikhomkhai on Pexels
Detailed view of a server rack with a focus on technology and data storage.
Photo by panumas nikhomkhai on Pexels

Validate and reload configuration

Validate before applying a change:

docker compose exec -w /etc/caddy caddy \
  caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

A valid configuration can be reloaded without recreating the container:

docker compose exec caddy \
  caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

Reloading preserves the running container and normally avoids an interruption. If validation fails, fix the Caddyfile before reloading. For a first deployment, starting the stack is straightforward:

docker compose up -d
docker compose logs -f caddy

Verify HTTPS and the upstream

Test the redirect and certificate from a client that can resolve the public hostname:

curl -I http://app.example.com
curl -I https://app.example.com

The HTTP request should normally redirect to HTTPS. The HTTPS request should return a response from the upstream application, not a Caddy error page. Check the browser certificate details and inspect Caddy logs if issuance is still in progress.

If the certificate is missing, verify DNS, port forwarding, firewall rules, and whether another process occupies ports 80 or 443. If HTTPS works but the response is a 502, Caddy is reachable but cannot connect successfully to the configured upstream.

Back up state and plan updates

Back up both named volumes and the Caddyfile. The most important item is /data, because it contains certificate state and keys. Protect backups as sensitive data, especially when they include private keys.

Pin a deliberate Caddy image version in production rather than relying indefinitely on an unreviewed floating tag. Before updating, save the current image reference, back up volumes, validate the configuration, and review the container logs after deployment.

A rollback should restore the previous image reference and the previous known-good configuration. Avoid deleting volumes during troubleshooting or upgrades. Removing caddy_data destroys durable certificate state and can turn a recoverable deployment issue into a reissuance and trust problem.

Troubleshooting table

SymptomLikely causeCheck
No certificateDNS does not point to the server, or ports 80/443 are blockedConfirm public DNS, firewall rules, router forwarding, and Caddy logs
HTTP works but HTTPS failsTLS issuance or port 443 reachability problemTest port 443 externally and inspect certificate errors
502 Bad GatewayWrong service name, wrong port, stopped upstream, or network mismatchCheck docker compose ps, network attachments, and the upstream address
Caddy starts, then exitsInvalid Caddyfile or malformed Compose configurationRun caddy validate and inspect startup logs
Changes do not appearThe file changed but Caddy was not reloadedValidate, then run caddy reload
Repeated certificate activity/data is missing, ephemeral, or being replacedConfirm the caddy_data volume is mounted and retained

CTA

Once this pattern is working, compare it with the broader options in OpenAlt’s Caddy profile, browse self-hosting platforms, or find more services in the self-hosting directory.

FAQ

Which Caddy volumes must persist?

Persist /data as the priority. It stores certificates, keys, and other runtime state. Persist /config as recommended by the official image documentation. In Compose, named volumes such as caddy_data and caddy_config are a simple durable choice.

Does Caddy always get a public certificate?

No. Caddy can obtain a publicly trusted certificate only when the hostname and public reachability satisfy the certificate authority’s requirements. Private names and internal-only endpoints do not automatically receive public certificates.

Can Caddy proxy another Compose project?

Yes. Attach both projects to the same external Docker network and use the reachable service name or network alias as the upstream. The application does not need a published host port.

How do you reload without downtime?

Validate the new Caddyfile, then run caddy reload inside the running container. This applies configuration without recreating the container. Keep the previous configuration available so you can restore it if validation or runtime checks reveal a problem.

What causes a 502?

A 502 usually means Caddy cannot connect to the upstream. Common causes include an incorrect service name, incorrect container port, stopped application, or missing shared network. Check container status, network membership, and connectivity from the Caddy container.