#!/usr/bin/env bash # Deploy the server to the production host, from this checkout. # # Until this existed the deployment was manual to a box nobody had written # down, and production ran code months behind main - three of the desktop # app's screens talked to routes that were not there. A deploy that is a # script gets run; one that is a memory does not. # # server/deploy.sh build, back up the database, migrate, switch # BASELINE=3 server/deploy.sh first run against a database that predates # migration tracking: adopt 1..3 unrun # DRY_RUN=1 server/deploy.sh build and ship, touch nothing running # # What it does, in order, and why the order matters: # 1. builds the head-office web app INTO the Go module, then a static # linux/amd64 binary here - the host has 3.6 GB of RAM and must not compile # 2. pg_dumps the database to backups/ on the host BEFORE anything changes # 3. builds the runtime-only image on the host from the shipped binary # 4. runs `migrate` with the NEW binary while the OLD server still serves; # a failing migration therefore stops here with production untouched # 5. switches the container, then proves the routes answer over the public URL set -euo pipefail cd "$(dirname "$0")" HOST=${HOST:-root@66.116.226.161} KEY=${KEY:-$HOME/.ssh/behavision_deploy} REMOTE_DIR=/root/behavision PUBLIC=https://mcp.loyaly.ai SSH=(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=10 "$HOST") VERSION=$(git describe --tags --always --dirty) case "$VERSION" in *-dirty) echo "refusing to deploy uncommitted changes ($VERSION)" >&2; exit 1;; esac step() { printf '\n\033[1m%s\033[0m\n' "$*"; } step "1. Build $VERSION" (cd ../web && npm run build >/dev/null) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \ -ldflags "-s -w -X main.version=${VERSION}" -o /tmp/behavision-server ./cmd/behavision-server ls -la /tmp/behavision-server | awk '{print " " $5 " bytes"}' step "2. Ship" "${SSH[@]}" "mkdir -p $REMOTE_DIR/release/$VERSION $REMOTE_DIR/backups" scp -q -i "$KEY" /tmp/behavision-server Dockerfile.runtime "$HOST:$REMOTE_DIR/release/$VERSION/" git rev-parse HEAD | "${SSH[@]}" "cat > $REMOTE_DIR/release/$VERSION/GIT_SHA" if [ "${DRY_RUN:-}" != "" ]; then echo "DRY_RUN: shipped to $REMOTE_DIR/release/$VERSION, nothing changed"; exit 0; fi step "3. Back up the database" "${SSH[@]}" "docker exec behavision-db sh -c 'PGPASSWORD=\$POSTGRES_PASSWORD pg_dump -U behavision -d behavision' | gzip > $REMOTE_DIR/backups/pre-$VERSION-\$(date +%Y%m%d-%H%M%S).sql.gz && ls -la $REMOTE_DIR/backups | tail -1" step "4. Image" "${SSH[@]}" "cd $REMOTE_DIR/release/$VERSION && docker build -q -t behavision-backend:$VERSION -f Dockerfile.runtime . && docker tag behavision-backend:$VERSION behavision-backend:latest" step "5. Migrate (old server still serving)" # `run` uses the compose service's environment and network, so the new binary # reaches postgres exactly as the server will. --no-deps: do not restart the # broker or the database to run a migration. if [ -n "${BASELINE:-}" ]; then "${SSH[@]}" "cd $REMOTE_DIR && docker compose run --rm --no-deps -T backend migrate -baseline $BASELINE" fi "${SSH[@]}" "cd $REMOTE_DIR && docker compose run --rm --no-deps -T backend migrate && docker compose run --rm --no-deps -T backend migrate -status" step "6. Switch" "${SSH[@]}" "cd $REMOTE_DIR && docker compose up -d --no-build --no-deps backend && sleep 4 && docker logs --tail 15 behavision-backend" step "7. Verify over $PUBLIC" for p in /healthz /api/admin/clients /api/team /api/visits /api/cameras; do printf ' %-20s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' -m 15 "$PUBLIC$p")" done curl -s -m 15 "$PUBLIC/healthz" | head -c 300; echo