feat: containerize doctate-server for minerva deployment
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.
This commit is contained in:
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user