From a74cb899b48ec3f5a54cdb133abca1e669cd40af Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Fri, 18 Sep 2026 11:37:47 +0530 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- API.md | 2 +- server/Dockerfile.runtime | 14 ++++++++ server/deploy.sh | 71 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 server/Dockerfile.runtime create mode 100755 server/deploy.sh diff --git a/API.md b/API.md index aacf5e8..3e5f53e 100644 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ # Behavision API — for the web console, a mobile app, and platform administration -Base URL: `https://platform.loyaly.ai` (locally `http://127.0.0.1:8088`). +Base URL: `https://mcp.loyaly.ai` — the API host. (`platform.loyaly.ai` serves the head-office web console, not the API.) Everything is JSON unless stated. All times are RFC 3339 UTC unless a field says otherwise. Every shape below is taken from the server's own types, not written from memory — if the two ever disagree, the server is right and this file has a diff --git a/server/Dockerfile.runtime b/server/Dockerfile.runtime new file mode 100644 index 0000000..7311423 --- /dev/null +++ b/server/Dockerfile.runtime @@ -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"] diff --git a/server/deploy.sh b/server/deploy.sh new file mode 100755 index 0000000..4612c3b --- /dev/null +++ b/server/deploy.sh @@ -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