diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3ebc69d..d40dd0f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -19,7 +19,10 @@ services: # PostgreSQL Database for AppView postgres: - image: postgres:15 + # Pinned by digest: `docker compose pull` must never silently change what + # runs in prod. Bump tag + digest together in a reviewed commit + # (`docker buildx imagetools inspect postgres:` resolves the digest). + image: postgres:15@sha256:926f8799aef36e00001cfe15fba7abbd37d3c5224ea57e4c858e4bb670f10561 container_name: coves-prod-postgres restart: unless-stopped environment: @@ -50,6 +53,8 @@ services: build: context: . dockerfile: Dockerfile + # VERSION defaults to the git short SHA in scripts/deploy.sh so the image + # tag on the box says exactly which commit is running. image: coves/appview:${VERSION:-latest} container_name: coves-prod-appview restart: unless-stopped @@ -198,7 +203,10 @@ services: # Bluesky PDS (Personal Data Server) # Handles community accounts and their repositories pds: - image: ghcr.io/bluesky-social/pds:latest + # Pinned by tag + digest — the PDS holds community repo signing keys, so + # upgrades are an explicit reviewed commit, never an implicit pull. + # Bump: `docker buildx imagetools inspect ghcr.io/bluesky-social/pds:`. + image: ghcr.io/bluesky-social/pds:0.4.193@sha256:50e60af25fa7b580ddae8fc18ae97b1d7cdeb1a3068a3d345f9161cc7796417b container_name: coves-prod-pds restart: unless-stopped ports: @@ -276,10 +284,14 @@ services: # Handles HTTPS automatically via Let's Encrypt # Uses Cloudflare plugin for wildcard SSL certificates (*.coves.social) caddy: - # Pre-built Caddy with Cloudflare DNS plugin - # Updates automatically with docker-compose pull - # Alternative: build your own with Dockerfile.caddy - image: ghcr.io/slothcroissant/caddy-cloudflaredns:latest + # Built locally from the official caddy image + caddy-dns/cloudflare, both + # pinned (see docker/caddy/Dockerfile). Never a floating third-party tag: + # this container holds the Cloudflare tokens and terminates all TLS. + # Upgrade = bump the Dockerfile pins, then `build caddy` + `up -d caddy`. + build: + context: ./docker/caddy + dockerfile: Dockerfile + image: coves/caddy:${CADDY_VERSION:-2.11.4} container_name: coves-prod-caddy restart: unless-stopped ports: diff --git a/docker/caddy/Dockerfile b/docker/caddy/Dockerfile new file mode 100644 index 0000000..e5db107 --- /dev/null +++ b/docker/caddy/Dockerfile @@ -0,0 +1,24 @@ +# Production Caddy with the Cloudflare DNS plugin, built from the official +# Docker Official Image rather than pulled from a third-party registry. +# +# Why: this container terminates TLS for every host we serve and holds both +# Cloudflare DNS tokens. A mutable third-party tag (the previous +# ghcr.io/slothcroissant/caddy-cloudflaredns:latest) meant anyone who could +# push that tag could ship a Caddy that intercepts all traffic. Building it +# here narrows trust to the official caddy image (pinned by digest) plus the +# caddy-dns/cloudflare Go module (pinned to a tag; Go's sumdb verifies it). +# +# Upgrading: bump the caddy version + BOTH digests together (resolve with +# `docker buildx imagetools inspect caddy:` and `caddy:-builder`), +# bump the plugin tag, then `docker compose -f docker-compose.prod.yml build caddy` +# and `up -d caddy`. Certs/ACME state live in the caddy-data volume and +# survive the recreate. + +FROM caddy:2.11.4-builder@sha256:4bdeabce8e79d36b23d1cba7d20598cec2c1117ace960d8ca06071f945e8fc9b AS builder + +RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare@v0.2.4 + +FROM caddy:2.11.4@sha256:df7f1c2fb114453b951de51a98efc010db1655a92c2e86be6706714e2417a78d + +COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 60d2038..580f588 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -66,6 +66,11 @@ if [ "$PULL_GIT" = true ]; then git pull origin main fi +# Tag the AppView image with the commit being deployed so `docker image ls` +# on the box says exactly what is running (compose defaults to :latest). +export VERSION="${VERSION:-$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo latest)}" +log "AppView image tag: coves/appview:$VERSION" + # Check database connectivity before deployment log "Checking database connectivity..." if docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then diff --git a/scripts/setup-production.sh b/scripts/setup-production.sh index 7767c6a..fde35c3 100755 --- a/scripts/setup-production.sh +++ b/scripts/setup-production.sh @@ -60,13 +60,15 @@ fi # If you need host-accessible logs, uncomment and run as root: # mkdir -p /var/log/caddy && chown 1000:1000 /var/log/caddy -# Pull Docker images +# Pull the digest-pinned external images (see docker-compose.prod.yml) log "Pulling Docker images..." -docker compose -f docker-compose.prod.yml pull postgres pds caddy +docker compose -f docker-compose.prod.yml pull postgres pds -# Build AppView -log "Building AppView..." -docker compose -f docker-compose.prod.yml build appview +# Build locally-built images: AppView and Caddy (official image + Cloudflare +# DNS plugin, pinned in docker/caddy/Dockerfile) +log "Building AppView and Caddy..." +export VERSION="${VERSION:-$(git rev-parse --short HEAD 2>/dev/null || echo latest)}" +docker compose -f docker-compose.prod.yml build appview caddy # Start services log "Starting services..."