NoctisNode
← GuidesSelf-hosting2026-10-239 min

Caddy Docker Reverse Proxy: Compose Setup with Automatic HTTPS

Run Caddy as a Docker reverse proxy: one compose stack, a shared network, reverse_proxy to service names and automatic HTTPS, plus when caddy-docker-proxy labels help.

Short answer: run Caddy in its own container that publishes ports 80 and 443, attach it and your app containers to one shared Docker network, and point `reverse_proxy` at each service name and internal port (for example `reverse_proxy app:3000`). Caddy resolves the name through Docker's embedded DNS and gets a TLS certificate for every site address automatically.

This guide shows the plain Caddyfile approach first, then the label-driven caddy-docker-proxy variant, and explains when each one fits on a single VPS. If you run Caddy directly on the host instead of in Docker, see the base Caddy reverse proxy guide.

How the pieces fit together

Only the Caddy container publishes ports to the outside world. Every backend container joins a user-defined bridge network (here called `web`) and exposes its port only on that network. Docker's embedded DNS server, available on user-defined networks, lets Caddy reach `app:3000` by service or container name, so you never hard-code container IPs that change on every recreate.

Keeping backends unpublished matters more than it looks: Docker writes its own iptables rules, so a port published with `ports:` can be reachable from the internet even when ufw says it is blocked. Use `expose:` or no port mapping at all for anything that should sit behind the proxy.

ComponentRolePublished on host?
caddy containerTLS termination, routing by hostname, HTTP to HTTPS redirectYes: 80/tcp, 443/tcp, 443/udp
web networkShared bridge network with built-in DNS for service namesNo
app containersYour services (web apps, APIs, dashboards)No, reachable only via Caddy
caddy_data volumeCertificates, private keys, ACME accountNo, but must persist

Step-by-step: plain Caddyfile with docker compose

  1. 01.Point an A record (and an AAAA record if you use IPv6) for each hostname to the VPS before starting Caddy, or certificate issuance will fail.
  2. 02.Create the shared network once: `docker network create web`.
  3. 03.Create `/opt/proxy/compose.yaml` and `/opt/proxy/Caddyfile` as shown below, then run `docker compose up -d` in `/opt/proxy`.
  4. 04.Attach each app stack to the external `web` network and start it with `docker compose up -d`.
  5. 05.Watch the first certificate being issued with `docker compose logs -f caddy` in `/opt/proxy`.
  6. 06.After editing the Caddyfile, reload without downtime: `docker compose exec -w /etc/caddy caddy caddy reload`.
# /opt/proxy/compose.yaml
services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"   # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data      # certificates and ACME account keys
      - caddy_config:/config
    networks:
      - web

volumes:
  caddy_data:
  caddy_config:

networks:
  web:
    external: true

Attach an app and write the Caddyfile

Each application stack declares the same external network. The service name becomes the hostname Caddy dials. The `caddy_data` volume is the one you must never lose: deleting it forces Caddy to request every certificate again, which can run into ACME rate limits.

# /opt/app/compose.yaml
services:
  app:
    image: ghcr.io/example/app:latest
    restart: unless-stopped
    expose:
      - "3000"        # reachable on the network, NOT published on the host
    networks:
      - web

networks:
  web:
    external: true

# /opt/proxy/Caddyfile
{
	email [email protected]
}

app.example.com {
	reverse_proxy app:3000
}

grafana.example.com {
	reverse_proxy grafana:3000
}

api.example.com {
	reverse_proxy api-1:8080 api-2:8080 {
		lb_policy round_robin
		health_uri /healthz
		health_interval 10s
	}
}

Caddy backends: upstreams, load balancing and health checks

In Caddy terms a backend is an upstream of `reverse_proxy`. You can list several upstreams in one directive, choose a `lb_policy` such as `round_robin`, `least_conn` or `ip_hash`, and add active health checks with `health_uri` so failed containers drop out of rotation. Passive checks (`fail_duration`, `max_fails`) are useful too when the app has no health endpoint.

Caddy sets `X-Forwarded-For`, `X-Forwarded-Proto` and `X-Forwarded-Host` on proxied requests by default. Configure your app to trust the proxy's network range so it sees the real client IP and builds `https://` links. WebSockets and gRPC work through `reverse_proxy` without extra settings; for gRPC over cleartext, use `reverse_proxy h2c://service:50051`.

Labels instead of a Caddyfile: caddy-docker-proxy

People searching for a "Caddy proxy manager" usually want what Nginx Proxy Manager offers: add a container, get a hostname and certificate without editing a central file. Caddy has no official GUI, but the community project caddy-docker-proxy (github.com/lucaslorentz/caddy-docker-proxy) generates the Caddyfile from container labels and reloads Caddy when containers start or stop.

It needs read access to the Docker socket, which is effectively root on the host. Mount it read-only, keep the proxy image updated, and prefer the plain Caddyfile if you only run a handful of services.

services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:ci-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CADDY_INGRESS_NETWORKS=web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - caddy_data:/data
    networks:
      - web

  whoami:
    image: traefik/whoami
    networks:
      - web
    labels:
      caddy: whoami.example.com
      caddy.reverse_proxy: "{{upstreams 80}}"

volumes:
  caddy_data:

networks:
  web:
    external: true

Plain Caddyfile vs caddy-docker-proxy

Plain Caddyfilecaddy-docker-proxy labels
Configuration lives inOne file you version in gitLabels spread across compose files
Docker socket accessNot neededRequired (security-sensitive)
New serviceEdit Caddyfile, run `caddy reload`Add labels, start the container
ImageOfficial `caddy:2`Community image built on Caddy
DebuggingRead the file directlyInspect the generated config in the logs
Best forSmall, stable VPS setupsMany short-lived or frequently changed services

Running it on a VPS

On a NoctisNode KVM VPS you get full root and a dedicated IPv4, so publishing 80 and 443 from one Caddy container and hiding everything else is straightforward. A Wisp (1 vCPU, 1 GB RAM) is enough for Caddy plus a couple of light containers; heavier stacks fit Pulse or Umbra. Certificate issuance details and failure modes are covered in how Caddy automatic HTTPS works, and raw TCP or SSH forwarding is in Caddy TCP and SSH proxying. Lock down host SSH with the SSH hardening guide.

FAQ

QHow do I make Caddy reverse proxy to a Docker container?

APut Caddy and the container on the same user-defined Docker network and use `reverse_proxy service-name:port` in the Caddyfile. Do not use `localhost`, because inside the Caddy container it refers to Caddy itself.

QWhy does Caddy return 502 Bad Gateway for my container?

AUsually the containers are not on a shared network, the port is wrong (use the container's internal port, not a published host port), or the app listens only on 127.0.0.1 inside its container. Make the app bind to 0.0.0.0 and check `docker network inspect web`.

QIs caddy-docker-proxy safe to use?

AIt works well, but it requires the Docker socket, which grants root-equivalent control over the host. Mount it read-only, keep the image updated and avoid exposing the Caddy admin endpoint.

QWhere does Caddy store certificates in Docker?

AIn `/data` inside the official image, so mount a named volume there. Losing it means re-issuing every certificate and possibly hitting ACME rate limits.

QIs there a Caddy proxy manager with a web UI?

ACaddy itself has no official web UI; it is configured with a Caddyfile, JSON or its admin API. Label-based tools like caddy-docker-proxy give a similar add-a-container-and-go workflow.

Caddy Docker Reverse Proxy: Compose Setup with Automatic HTTPS · NoctisNode