server/deploy.sh: build here, back up, migrate, switch, verify

Production ran code from 31 August and answered 404 to most of the API
the merchant and mobile clients are written against. The script builds
the web app into a static linux binary on the developer machine (the
host has 3.6 GB shared with other services and must not compile), backs
the database up, runs the migrations with the new binary while the old
server still serves so a failure stops with nothing changed, switches,
and proves the routes over the public URL. API.md now names the API host
correctly: mcp.loyaly.ai, not the console's platform.loyaly.ai.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-18 11:37:47 +05:30
parent 8c88aad06e
commit a74cb899b4
3 changed files with 86 additions and 1 deletions

14
server/Dockerfile.runtime Normal file
View File

@@ -0,0 +1,14 @@
# Runtime-only image, for a host that must not compile.
#
# The production box has 3.6 GB of RAM shared with other tenants' services;
# `Dockerfile` pulls a Go toolchain and builds there, which is how deploys
# became something nobody wanted to run. deploy.sh builds the static binary
# on the developer's machine and ships only that. Same runtime layer as
# Dockerfile, on purpose - the two must not drift.
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata && \
adduser -D -u 10001 behavision
COPY behavision-server /usr/local/bin/behavision-server
USER behavision
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/behavision-server"]

71
server/deploy.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/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