Files
Brummel 3d67cbc1c8 Add --force flag to deploy script
Introduces a `--force` or `-f` flag to the `deploy-server.sh` script,
allowing deployments even when the working tree is dirty. The script now
also checks for a dirty working tree and exits with an error unless
`--force` is provided.
2026-05-04 11:50:40 +02:00

148 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Deploy doctate-server to a remote Docker host (default: minerva.lan).
# Idempotent: re-runs preserve operator-owned config, data, logs.
#
# Usage: ./scripts/deploy-server.sh [--force|-f] [host]
# --force, -f build even if the working tree is dirty
# host default: minerva.lan
#
# See docs/deployment.md for first-time bootstrap.
set -euo pipefail
FORCE=0
HOST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--force|-f) FORCE=1 ;;
-h|--help)
sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
-*) echo "Unknown flag: $1" >&2; exit 2 ;;
*) HOST="$1" ;;
esac
shift
done
HOST="${HOST:-minerva.lan}"
REMOTE="/opt/stacks/doctate-server"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GIT_SHA="$(cd "$REPO_ROOT" && git rev-parse --short HEAD)"
IMAGE_LATEST="doctate-server:latest"
IMAGE_TAGGED="doctate-server:$GIT_SHA"
cd "$REPO_ROOT"
# ─── Phase 0 ─── local pre-flight
if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
if (( FORCE )); then
echo " WARNING: working tree is dirty — building from on-disk state, not from $GIT_SHA (--force given)" >&2
else
echo "ERROR: working tree is dirty. Commit/stash your changes, or pass --force to deploy anyway." >&2
echo >&2
git -C "$REPO_ROOT" status --short >&2
exit 1
fi
fi
echo ">>> [0/10] cargo fmt --check (server)"
( cd server && cargo fmt --check )
# ─── Phase 1 ─── remote bootstrap (idempotent)
echo ">>> [1/10] Ensuring remote layout at $HOST:$REMOTE"
ssh "$HOST" "mkdir -p $REMOTE/config $REMOTE/data $REMOTE/logs"
# ─── Phase 2 ─── seed templates only if missing
echo ">>> [2/10] Seeding config templates (only if missing)"
for pair in \
".env.example:.env" \
"settings.toml.example:settings.toml" \
"users.toml.example:users.toml"
do
src="server/${pair%%:*}"
dst="${pair##*:}"
remote_path="$REMOTE/config/$dst"
if ssh "$HOST" "test -f $remote_path"; then
echo " KEEP $remote_path"
else
echo " SEED $remote_path <- $src"
scp -q "$src" "$HOST:$remote_path"
fi
done
# ─── Phase 3 ─── placeholder guard
echo ">>> [3/10] Verifying configs have been edited from defaults"
if ssh "$HOST" "grep -qE '\\\$2b\\\$12\\\$\\.\\.\\.|change-me-to-a-secure-key' $REMOTE/config/users.toml" 2>/dev/null; then
cat >&2 <<EOF
ERROR: $REMOTE/config/users.toml still has placeholder values.
Edit it on $HOST (e.g. via nano or Dockge) and replace:
- '\$2b\$12\$...' with a real bcrypt hash
- 'change-me-to-a-secure-key' with a real api_key
For the bcrypt hash, the image is already loaded on $HOST (after the
first run reaches phase 5); generate one with:
docker run --rm -i $IMAGE_LATEST hash-password
Then re-run this script. See docs/deployment.md "First deploy" section.
EOF
exit 1
fi
# ─── Phase 4 ─── build
echo ">>> [4/10] Building $IMAGE_TAGGED + $IMAGE_LATEST"
DOCKER_BUILDKIT=1 docker build \
-f server/Dockerfile \
-t "$IMAGE_TAGGED" \
-t "$IMAGE_LATEST" \
.
# ─── Phase 5 ─── stream image to host
echo ">>> [5/10] Transferring image to $HOST (gzip-streamed via ssh)"
docker save "$IMAGE_TAGGED" "$IMAGE_LATEST" | gzip | ssh "$HOST" 'gunzip | docker load'
# ─── Phase 6 ─── compose file sync
echo ">>> [6/10] Syncing compose file"
scp -q deploy/doctate-server.compose.yml "$HOST:$REMOTE/compose.yaml"
# ─── Phase 7 ─── restart
echo ">>> [7/10] (Re)starting container"
ssh "$HOST" "cd $REMOTE && docker compose up -d --force-recreate"
# ─── Phase 8 ─── health poll
echo ">>> [8/10] Waiting for /api/health"
for i in $(seq 1 20); do
if curl -fsS "http://$HOST:3000/api/health" >/dev/null 2>&1; then
echo " OK after ${i} attempt(s)"
# ─── Phase 9 ─── prune old image tags (keep last KEEP + :latest)
# Containers hold images via internal IDs, not tags, so removing
# an old <sha>-tag never affects the running container as long as
# :latest still points to it.
KEEP=5
echo ">>> [9/10] Pruning old image tags on $HOST (keep last $KEEP)"
ssh "$HOST" "
set -e
docker images doctate-server --format '{{.CreatedAt}}\t{{.Tag}}' | \
grep -v -P '\\tlatest\$' | \
sort -r | \
awk 'NR > $KEEP { print \$NF }' | \
xargs -I{} -r docker rmi doctate-server:{} 2>/dev/null || true
docker image prune -f >/dev/null 2>&1 || true
"
# ─── Phase 10 ─── final echo
echo ">>> [10/10] Deployed $GIT_SHA"
echo
echo "Rollback:"
echo " ssh $HOST 'docker tag doctate-server:<prev-sha> $IMAGE_LATEST && cd $REMOTE && docker compose up -d --force-recreate'"
exit 0
fi
sleep 3
done
# health timeout
echo ">>> TIMEOUT (60s) — last 50 lines of container logs:" >&2
ssh "$HOST" "docker logs --tail 50 doctate-server" >&2 || true
exit 1