Files
webmail/deploy/demo-deploy.sh
Linus Rath c6bb4aace9 fix(deploy): go back to an image id, not the bulwark-demo:previous tag
Rolling back ran the container from the local tag, so the demo recorded
`bulwark-demo:previous` as its image — and that tag is moved by the very
next deploy. Run the id instead; the tag stays only to keep the image from
being pruned.
2026-09-20 03:03:19 +02:00

140 lines
4.7 KiB
Bash

#!/usr/bin/env bash
# Deploys the demo at https://demo.bulwarkmail.org on mail.rath.li.
#
# The demo is a single container off the published release image, so there is
# nothing to build here: this pulls a tag from GHCR and recreates the
# container. Installed as /usr/local/bin/bulwark-demo-deploy and run as the
# `ubuntu` user through a forced command on the deploy key in
# ~ubuntu/.ssh/authorized_keys:
#
# command="/usr/local/bin/bulwark-demo-deploy",restrict ssh-ed25519 AAAA... demo deploy
#
# so the key can do nothing but this. Re-install it by hand when this file
# changes. The requested action comes in SSH_ORIGINAL_COMMAND:
#
# deploy [tag] pull ghcr.io/bulwarkmail/webmail:<tag> (default latest),
# recreate the container, check it, roll back on failure
# rollback go back to the image the last deploy replaced
# status print the running image, its version and health
#
# Replaces Watchtower for this container: the webmail repo deploys here after
# it publishes a release image (.github/workflows/deploy-demo.yml). The two
# rbm.systems containers are still on Watchtower's watch list.
set -euo pipefail
IMAGE=ghcr.io/bulwarkmail/webmail
NAME=bulwark-demo
PORT=3002
DATA=/opt/bulwark-demo-data
# The image this deploy replaced. Containers always run an image id; this tag
# exists only so pruning cannot collect the image we may need to go back to.
PREVIOUS=bulwark-demo:previous
STATE=/opt/bulwark-demo-deploy
log() { echo "[demo-deploy] $*"; }
docker_() { sudo -n docker "$@"; }
image_id() { docker_ image inspect -f '{{.Id}}' "$1" 2>/dev/null || true; }
running_image_id() { docker_ inspect -f '{{.Image}}' "$NAME" 2>/dev/null || true; }
version_of() { docker_ image inspect -f '{{index .Config.Labels "org.opencontainers.image.version"}}' "$1" 2>/dev/null || true; }
# Recreate the container from $1. Mirrors the original `docker run`: demo mode,
# host port 3002, and the three bind mounts that keep the demo past the setup
# wizard (admin/config.json holds setupComplete).
run_container() {
docker_ rm -f "$NAME" >/dev/null 2>&1 || true
docker_ run -d \
--name "$NAME" \
--restart unless-stopped \
-p "${PORT}:3000" \
-e DEMO_MODE=true \
-v "$DATA/admin:/app/data/admin" \
-v "$DATA/settings:/app/data/settings" \
-v "$DATA/telemetry:/app/data/telemetry" \
"$1" >/dev/null
}
healthy() {
local i
for i in $(seq 1 30); do
if curl -fsS -o /dev/null "http://127.0.0.1:${PORT}/api/health"; then
return 0
fi
sleep 2
done
return 1
}
cmd=${SSH_ORIGINAL_COMMAND:-${1:-}}
mkdir -p "$STATE"
exec 9>"$STATE/.deploy.lock"
flock -n 9 || { log "another deploy is running"; exit 1; }
case "$cmd" in
deploy|deploy\ *)
tag=${cmd#deploy}
tag=${tag# }
tag=${tag:-latest}
[[ "$tag" =~ ^[A-Za-z0-9._-]{1,128}$ ]] || { log "bad tag: $tag"; exit 2; }
log "pulling $IMAGE:$tag"
docker_ pull -q "$IMAGE:$tag" >/dev/null
new=$(image_id "$IMAGE:$tag")
[[ -n "$new" ]] || { log "no such image after pull"; exit 3; }
old=$(running_image_id)
if [[ "$new" == "$old" ]] && healthy; then
log "already on $(version_of "$new") ($tag), nothing to do"
exit 0
fi
[[ -n "$old" ]] && docker_ tag "$old" "$PREVIOUS"
run_container "$IMAGE:$tag"
if healthy; then
log "live: $(version_of "$new") ($tag)"
else
log "health check failed"
if [[ -n "$old" ]]; then
run_container "$old"
healthy && log "back on $(version_of "$old")" || log "rollback is not answering either"
fi
exit 4
fi
# Only untagged leftovers; $PREVIOUS stays because it is tagged.
docker_ image prune -f >/dev/null || true
;;
rollback)
prev=$(image_id "$PREVIOUS")
[[ -n "$prev" ]] || { log "no earlier image to roll back to"; exit 5; }
cur=$(running_image_id)
run_container "$prev"
# Point $PREVIOUS at what we just left, so a second rollback comes back.
[[ -n "$cur" ]] && docker_ tag "$cur" "$PREVIOUS"
healthy && log "rolled back to $(version_of "$prev")" || { log "rolled back, but the health check failed"; exit 4; }
;;
status)
cur=$(running_image_id)
if [[ -n "$cur" ]]; then
echo "running: $(version_of "$cur") ($(docker_ inspect -f '{{.Config.Image}}' "$NAME")) ${cur:7:12}"
echo "state: $(docker_ inspect -f '{{.State.Status}}' "$NAME")"
else
echo "running: none"
fi
prev=$(image_id "$PREVIOUS")
[[ -n "$prev" ]] && echo "previous: $(version_of "$prev") ${prev:7:12}" || echo "previous: none"
curl -fsS -o /dev/null -w "health: %{http_code}\n" "http://127.0.0.1:${PORT}/api/health" || true
;;
*)
echo "usage: deploy [tag] | rollback | status" >&2
exit 2
;;
esac