diff --git a/deploy/AUTOPUSH.md b/deploy/AUTOPUSH.md index 19cba00..f02627b 100644 --- a/deploy/AUTOPUSH.md +++ b/deploy/AUTOPUSH.md @@ -8,9 +8,14 @@ droplet, `crate-pull.timer` polls once a minute; when the digest behind push, so a red build never becomes an image. Nothing SSHes into production to deploy, and production holds no CI -credentials. The pull unit extracts the versioned Caddyfile from the pulled -app image and recreates Caddy only when that file changed, before replacing -the app container. `deploy/publish.sh` does the same two steps by hand for +credentials. The pull unit's `ExecStartPre` runs `deploy/sync-caddy.sh` +(installed on the droplet at `/opt/crate/deploy/sync-caddy.sh`), which +extracts the versioned Caddyfile from the pulled app image and recreates +Caddy only when that file changed, before replacing the app container. It's +a real script file rather than an inline `sh -c '...'` on the unit's +`Exec` line because systemd expands `$VAR` on those lines itself before the +shell sees them, which would silently break the script's own `$(...)` and +`"$image"` use. `deploy/publish.sh` does the same two steps by hand for break-glass: build and push from your machine, then start the timer's unit immediately instead of waiting for the next poll. @@ -27,12 +32,12 @@ immediately instead of waiting for the next poll. deploy/setup-autopull.sh ``` -Installs `crate-pull.{service,timer}` into `/etc/systemd/system`, enables -the timer, installs the initial Caddyfile, points `.env` at the `latest` tag, -and prints the schedule. Subsequent image pulls extract the versioned -Caddyfile, validates it in the Caddy image, and force-recreates Caddy only -when it changes. Invalid configuration leaves the running proxy untouched. -Idempotent. +Installs `crate-pull.{service,timer}` into `/etc/systemd/system` and +`sync-caddy.sh` into `/opt/crate/deploy/`, enables the timer, installs the +initial Caddyfile, points `.env` at the `latest` tag, and prints the +schedule. Subsequent image pulls extract the versioned Caddyfile, validate +it in the Caddy image, and force-recreate Caddy only when it changes. +Invalid configuration leaves the running proxy untouched. Idempotent. If the ghcr package is private (GitHub creates new packages private, and the first CI push is what creates this one), the droplet needs read credentials diff --git a/deploy/crate-pull.service b/deploy/crate-pull.service index a1462e3..58e4287 100644 --- a/deploy/crate-pull.service +++ b/deploy/crate-pull.service @@ -7,7 +7,7 @@ After=docker.service Type=oneshot WorkingDirectory=/opt/crate ExecStartPre=/usr/bin/docker compose pull --quiet app -ExecStartPre=/bin/sh -c 'image=$(/usr/bin/docker compose config --images | sed -n "1p"); /usr/bin/docker run --rm --entrypoint cat "$image" /app/deploy/Caddyfile > /tmp/crate.Caddyfile && if ! cmp -s /tmp/crate.Caddyfile /opt/crate/Caddyfile; then /usr/bin/docker run --rm -v /tmp/crate.Caddyfile:/etc/caddy/Caddyfile:ro caddy:2 caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile && install -m 644 /tmp/crate.Caddyfile /opt/crate/Caddyfile && /usr/bin/docker compose up -d --force-recreate caddy; fi' +ExecStartPre=/opt/crate/deploy/sync-caddy.sh ExecStart=/usr/bin/docker compose up -d app # Each pull leaves the previous :latest dangling; nothing else prunes here. ExecStartPost=-/usr/bin/docker image prune -f diff --git a/deploy/setup-autopull.sh b/deploy/setup-autopull.sh index ea39ff2..ff19879 100755 --- a/deploy/setup-autopull.sh +++ b/deploy/setup-autopull.sh @@ -14,14 +14,16 @@ set -euo pipefail droplet="${CRATE_DROPLET:-root@206.189.15.37}" echo "installing the pull timer on $droplet" -tar cf - -C deploy crate-pull.service crate-pull.timer Caddyfile \ +tar cf - -C deploy crate-pull.service crate-pull.timer Caddyfile sync-caddy.sh \ | ssh -o ConnectTimeout=20 "$droplet" " set -e tar xf - -C /etc/systemd/system + install -d /opt/crate/deploy install -m 644 /etc/systemd/system/Caddyfile /tmp/crate.Caddyfile docker run --rm -v /tmp/crate.Caddyfile:/etc/caddy/Caddyfile:ro caddy:2 caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile install -m 644 /tmp/crate.Caddyfile /opt/crate/Caddyfile - rm /etc/systemd/system/Caddyfile + install -m 755 /etc/systemd/system/sync-caddy.sh /opt/crate/deploy/sync-caddy.sh + rm /etc/systemd/system/Caddyfile /etc/systemd/system/sync-caddy.sh chown root:root /etc/systemd/system/crate-pull.{service,timer} chmod 644 /etc/systemd/system/crate-pull.{service,timer} systemctl daemon-reload diff --git a/deploy/sync-caddy.sh b/deploy/sync-caddy.sh new file mode 100755 index 0000000..da1cdfc --- /dev/null +++ b/deploy/sync-caddy.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# ExecStartPre for crate-pull.service, run from /opt/crate on the droplet +# after the app image is pulled. Extracts the versioned Caddyfile baked into +# that image and, only if it differs from what's installed, validates it +# inside the caddy image before installing it and force-recreating caddy. +# Invalid config always leaves the running proxy untouched. +# +# Split out of the unit file rather than left as an inline `sh -c '...'`: +# systemd expands `$VAR` on Exec lines itself before the shell ever sees +# them, which silently breaks the `$(...)` and "$image" here -- an empty +# $image would make this whole step a no-op that nobody notices until a +# Caddyfile change fails to deploy. +set -euo pipefail + +image="$(docker compose config --images app)" +docker run --rm --entrypoint cat "$image" /app/deploy/Caddyfile \ + > /tmp/crate.Caddyfile + +if cmp -s /tmp/crate.Caddyfile /opt/crate/Caddyfile; then + exit 0 +fi + +docker run --rm -v /tmp/crate.Caddyfile:/etc/caddy/Caddyfile:ro caddy:2 \ + caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + +install -m 644 /tmp/crate.Caddyfile /opt/crate/Caddyfile +docker compose up -d --force-recreate caddy