cf80bc7e97
Multi-stage Dockerfile, host-network compose, idempotent deploy script. Configs and persistent data bind-mounted from /opt/stacks/doctate-server. deploy-server.sh handles bootstrap + re-deploy in one command with auto-prune (keep 5 last SHA-tagged images). See docs/deployment.md for bootstrap, rollback, and TLS-proxy notes.
125 lines
4.4 KiB
Bash
Executable File
125 lines
4.4 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 [host]
|
|
# host default: minerva.lan
|
|
#
|
|
# See docs/deployment.md for first-time bootstrap.
|
|
|
|
set -euo pipefail
|
|
|
|
HOST="${1:-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
|
|
echo ">>> [0/10] cargo fmt --check (server)"
|
|
( cd server && cargo fmt --check )
|
|
|
|
if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then
|
|
echo " WARNING: working tree is dirty — building from on-disk state, not from $GIT_SHA" >&2
|
|
fi
|
|
|
|
# ─── 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
|