Files
drinktracker/deploy/deploy.sh
JP f3865c7901 Fix deploy: avoid cd into /root as the deploy user
The remote shell runs as drinkadmin, which cannot enter /root even
though sudo can operate there, so 'cd $REMOTE_DIR && sudo docker ...'
failed at the cd. Pass the build context and compose file as paths
instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:06:01 +00:00

119 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Deploy drinktracker to the production LXC.
#
# The image is built ON the LXC and tagged with the registry name that
# docker-compose.prod.yml expects. Compose then finds it locally and never pulls, so
# this needs no registry credentials on either machine. Pass --push to additionally
# publish the image to the Gitea registry (requires `docker login` on the LXC).
#
# Usage: ./deploy/deploy.sh [--push] [--no-build] [--yes]
#
set -euo pipefail
HOST="${DT_HOST:-drinkadmin@192.168.2.169}"
SSH_KEY="${DT_SSH_KEY:-$HOME/.ssh/drinktracker_ed25519}"
REMOTE_DIR="${DT_REMOTE_DIR:-/root/drinktracker}"
IMAGE="${DT_IMAGE:-192.168.2.140:3000/jpscott84/drinktracker:latest}"
COMPOSE_FILE="docker-compose.prod.yml"
HEALTH_URL="http://localhost:3000/"
PUSH=0; BUILD=1; ASSUME_YES=0
for arg in "$@"; do
case "$arg" in
--push) PUSH=1 ;;
--no-build) BUILD=0 ;;
--yes|-y) ASSUME_YES=1 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$SSH_KEY" "$HOST")
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
die() { printf '\n\033[1;31mFAILED: %s\033[0m\n' "$*" >&2; exit 1; }
# ─── Preflight (local) ───────────────────────────────────────────────
say "Preflight"
cd "$(dirname "$0")/.."
[ -n "$(git status --porcelain)" ] && die "working tree is dirty - commit or stash first"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
[ "$BRANCH" = "main" ] || die "on branch '$BRANCH', expected main"
npx tsc --noEmit || die "typecheck failed"
echo " typecheck ok, tree clean, on main"
# Push first so the server pulls exactly what was verified here.
git -c credential.helper=store push origin main
LOCAL_SHA=$(git rev-parse HEAD)
echo " pushed $(git rev-parse --short HEAD)"
# ─── Preflight (remote) ──────────────────────────────────────────────
say "Checking $HOST"
"${SSH[@]}" true || die "cannot reach $HOST with key $SSH_KEY"
"${SSH[@]}" 'sudo -n true' 2>/dev/null \
|| die "passwordless sudo required on the LXC (needed for $REMOTE_DIR). See deploy/README.md"
# A dirty server checkout means someone edited production by hand; clobbering that
# silently would destroy the only copy of the change.
if ! "${SSH[@]}" "sudo -n git -C $REMOTE_DIR diff --quiet && sudo -n git -C $REMOTE_DIR diff --cached --quiet"; then
die "$REMOTE_DIR has uncommitted changes - inspect before deploying"
fi
if [ "$ASSUME_YES" -ne 1 ]; then
REMOTE_SHA=$("${SSH[@]}" "sudo -n git -C $REMOTE_DIR rev-parse --short HEAD")
echo " remote is at $REMOTE_SHA, deploying ${LOCAL_SHA:0:7}"
read -r -p " proceed? [y/N] " reply
[[ "$reply" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
fi
# ─── Deploy ──────────────────────────────────────────────────────────
say "Syncing $REMOTE_DIR to $LOCAL_SHA"
"${SSH[@]}" "sudo -n git -C $REMOTE_DIR fetch origin main --quiet && sudo -n git -C $REMOTE_DIR reset --hard $LOCAL_SHA --quiet && sudo -n git -C $REMOTE_DIR log --oneline -1"
# SWITCHBOARD_BASE_URL arrived with the gateway migration and will be missing from
# any .env.production written before it. Compose supplies a default, but making it
# explicit keeps the file honest about what the app reads.
say "Checking .env.production for new variables"
"${SSH[@]}" "sudo -n grep -q '^SWITCHBOARD_BASE_URL=' $REMOTE_DIR/.env.production" \
&& echo " SWITCHBOARD_BASE_URL present" \
|| echo " NOTE: SWITCHBOARD_BASE_URL absent; compose default (http://192.168.2.11:8787/v1) applies"
if [ "$BUILD" -eq 1 ]; then
say "Building image on the LXC (this takes a few minutes)"
# Pass the build context as a path rather than cd-ing in: the shell runs as the
# deploy user, which cannot enter /root even though sudo can build there.
"${SSH[@]}" "sudo -n docker build -t $IMAGE $REMOTE_DIR" || die "image build failed"
fi
if [ "$PUSH" -eq 1 ]; then
say "Pushing image to registry"
"${SSH[@]}" "sudo -n docker push $IMAGE" || die "registry push failed (is docker login done on the LXC?)"
fi
say "Restarting stack"
# --no-build: compose must use the image we just built, not rebuild or pull it.
# Compose takes the project directory from the compose file's location, which keeps
# the project name "drinktracker" and so reuses the existing containers and volumes.
"${SSH[@]}" "sudo -n docker compose -f $REMOTE_DIR/$COMPOSE_FILE up -d --no-build"
# ─── Verify ──────────────────────────────────────────────────────────
say "Verifying"
"${SSH[@]}" "sudo -n docker compose -f $REMOTE_DIR/$COMPOSE_FILE ps --format 'table {{.Name}}\t{{.Status}}'" || true
ok=0
for i in $(seq 1 30); do
code=$("${SSH[@]}" "curl -s -o /dev/null -w '%{http_code}' -m 5 $HEALTH_URL" || echo 000)
# 307 is the unauthenticated redirect to /login - a healthy response here.
case "$code" in 200|307|302) echo " app responding (HTTP $code) after ${i}0s"; ok=1; break ;; esac
sleep 10
done
[ "$ok" -eq 1 ] || die "app did not become healthy - check: ${SSH[*]} 'sudo docker logs drinktracker-app-1 --tail 50'"
say "Recent gateway activity"
"${SSH[@]}" "sudo -n docker logs drinktracker-app-1 --tail 200 2>&1 | grep '\[switchboard\]' | tail -5 || echo ' (no AI calls yet)'"
say "Deployed ${LOCAL_SHA:0:7}"
echo "To roll back: ./deploy/deploy.sh after 'git revert', or on the LXC:"
echo " sudo git -C $REMOTE_DIR reset --hard <previous-sha> && cd $REMOTE_DIR && sudo docker build -t $IMAGE . && sudo docker compose -f $COMPOSE_FILE up -d --no-build"