mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
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.
140 lines
4.7 KiB
Bash
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
|