# The edge web server for this box.
#
# One Caddy, owning ports 80 and 443, serving every site on the machine
# straight from disk. No second proxy hop, no per-project container: a static
# file server has no runtime to isolate, so adding one would be complexity
# bought with nothing.
#
# docker compose -f /opt/web/caddy/docker-compose.yml up -d
# docker compose -f /opt/web/caddy/docker-compose.yml logs -f
services:
caddy:
image: caddy:2-alpine
container_name: web-caddy
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '443:443/udp' # HTTP/3
volumes:
# All sites, read-only. Each project deploys into a subdirectory and
# points its conf.d file at /srv/<project>/current/<site>.
#
# The parent directory is mounted rather than each release, because
# `current` is a RELATIVE symlink into releases/ — mounting the whole
# tree is what lets a deploy flip that symlink and have it take effect
# immediately, with no restart.
- /opt/web/sites:/srv:ro
# The config DIRECTORY, never an individual file.
#
# Docker pins a single-file bind mount to that file's inode. rsync (and
# most editors) write a temporary file and rename it over the original,
# which creates a NEW inode — so the container keeps serving the old
# contents forever while the host file plainly shows the new ones. That
# failure is completely silent: the reload succeeds, reports no error,
# and applies nothing. Mounting the directory makes replacements visible.
- /opt/web/caddy/etc:/etc/caddy:ro
# Certificates and ACME account keys. Losing this volume means
# re-issuing every certificate, so it is the one thing here worth
# backing up.
- caddy-data:/data
- caddy-config:/config
security_opt:
- no-new-privileges:true
# No healthcheck on purpose. Any meaningful probe has to name a hostname,
# which would tie this server-wide file to one particular site, and a probe
# that does not would report "unhealthy" on a box whose sites are all
# simply fine. `restart: unless-stopped` covers a crash; deploy.sh verifies
# the sites over real HTTPS afterwards, which is the check that matters.
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'
volumes:
caddy-data:
caddy-config: