feat(deploy): booth.sh wrapper over the compose files + update flow
The booth PC (Ubuntu) needs one command instead of the long
`docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env …`
over the three compose files.
scripts/booth.sh — prod by default (ENV=dev for the dev override):
up/down/restart/status/logs/pull/config/exec, plus the requested `update` =
pull the moving branch tag → up -d --remove-orphans (recreates only
digest-changed services; named volumes / the SQLite ledger are preserved) →
docker image prune. Prod refuses to run without .env (no safe JWT_SECRET
default); dev with no .env injects the documented benign local secret (the
base file makes JWT_SECRET shell-required via ${JWT_SECRET:?}). down never
passes -v (would wipe the signed-ledger volume); help/unknown-command
short-circuit before any Docker/.env requirement.
.env.example — the vars the compose files consume (REGISTRY, TAG, JWT_SECRET,
EVENT_SIGNING_KEY, COOKIE_SECURE=0, WS_ALLOWED_ORIGINS). .env stays gitignored.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Booth deploy env — copy to `.env` and fill in, then run ./scripts/booth.sh up
|
||||
# (prod). Consumed by docker-compose.yml + the prod override via --env-file.
|
||||
# See wiki/decisions/container-deployment.md. Do NOT commit the filled-in .env.
|
||||
|
||||
# --- image source (prod pulls from the house Gitea registry) ------------------
|
||||
# The registry namespace; combined with the image name + TAG below.
|
||||
REGISTRY=git.infra.msai.al/mca/parking_solution
|
||||
# Moving branch tag to deploy: `main` for production, `dev` for staging. A push to
|
||||
# that branch republishes this tag, so `booth.sh update` pulls the latest of it.
|
||||
TAG=main
|
||||
|
||||
# --- secrets (NO safe defaults — the server refuses to boot without a real one) -
|
||||
# JWT signing secret. Generate yourself, never share it: openssl rand -hex 32
|
||||
# Must be 32+ chars and must NOT contain change-me / insecure / dev-only.
|
||||
JWT_SECRET=
|
||||
|
||||
# Ledger-signing key for the append-only signed event chain. Set a DISTINCT value
|
||||
# in prod (don't reuse JWT_SECRET). openssl rand -hex 32
|
||||
EVENT_SIGNING_KEY=
|
||||
|
||||
# --- booth LAN specifics ------------------------------------------------------
|
||||
# Auth cookie is HTTPS-only by default; the booth is plain HTTP behind Caddy on
|
||||
# :80, so this MUST stay 0 or operators cannot log in. Set to 1 only behind TLS.
|
||||
COOKIE_SECURE=0
|
||||
|
||||
# Remote origins the live WS feed must accept (same-origin always passes). Add any
|
||||
# address admins hit the UI from beyond the booth itself, comma-separated, e.g.
|
||||
# http://parksystems.msai.al (leave blank if only the local booth URL is used).
|
||||
WS_ALLOWED_ORIGINS=
|
||||
|
||||
# Vision/ANPR. Prod override already forces the fast_alpr engine; leave VISION_ENABLED=1
|
||||
# unless you are running without the camera. (Set 0 to disable the vision call entirely.)
|
||||
VISION_ENABLED=1
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# booth.sh — operate the parking stack on the booth PC (Ubuntu).
|
||||
#
|
||||
# Wraps the three compose files (base + a dev/prod override) so the operator runs
|
||||
# one command instead of a long `docker compose -f … -f … --env-file …` line.
|
||||
#
|
||||
# ./scripts/booth.sh up # start the stack (detached)
|
||||
# ./scripts/booth.sh update # pull newer images + recreate (the "there are
|
||||
# # new images" case) — see `update` below
|
||||
# ./scripts/booth.sh down # stop the stack
|
||||
# ./scripts/booth.sh restart # restart without pulling
|
||||
# ./scripts/booth.sh status # what's running
|
||||
# ./scripts/booth.sh logs # follow logs (Ctrl-C to stop)
|
||||
# ./scripts/booth.sh ps|pull|config|exec …
|
||||
#
|
||||
# Environment is PROD by default (the booth runs prod: pull pinned registry images,
|
||||
# Caddy on :80, fast_alpr). Override with ENV=dev for a local build/dev run:
|
||||
# ENV=dev ./scripts/booth.sh up
|
||||
#
|
||||
# Config comes from an .env file next to the compose files (REGISTRY, TAG,
|
||||
# JWT_SECRET, …). Copy .env.example → .env and fill it in. See
|
||||
# wiki/decisions/container-deployment.md.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- locate the repo (this script lives in <repo>/scripts) --------------------
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# --- environment selection (prod by default; the booth is prod) ---------------
|
||||
ENV="${ENV:-prod}"
|
||||
case "$ENV" in
|
||||
prod|production) ENV=prod; OVERRIDE="docker-compose.prod.yml" ;;
|
||||
dev|development) ENV=dev; OVERRIDE="docker-compose.dev.yml" ;;
|
||||
*) echo "ERROR: ENV must be 'prod' or 'dev' (got '$ENV')." >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
BASE="docker-compose.yml"
|
||||
ENV_FILE="${ENV_FILE:-.env}"
|
||||
|
||||
# --- colours (only when attached to a terminal) -------------------------------
|
||||
if [ -t 1 ]; then
|
||||
R="$(printf '\033[31m')"; G="$(printf '\033[32m')"; Y="$(printf '\033[33m')"
|
||||
B="$(printf '\033[1m')"; N="$(printf '\033[0m')"
|
||||
else
|
||||
R=""; G=""; Y=""; B=""; N=""
|
||||
fi
|
||||
info() { printf '%s==>%s %s\n' "$B" "$N" "$*"; }
|
||||
warn() { printf '%s!! %s%s\n' "$Y" "$*" "$N" >&2; }
|
||||
die() { printf '%sERROR:%s %s\n' "$R" "$N" "$*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
sed -n '3,30p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# --- preflight (only for commands that actually talk to Docker) ---------------
|
||||
# Deferred into a function so `help`/usage works with no Docker and no .env.
|
||||
ENV_ARGS=()
|
||||
DC=()
|
||||
preflight() {
|
||||
command -v docker >/dev/null 2>&1 || die "docker is not installed or not on PATH."
|
||||
# Prefer the v2 plugin (`docker compose`); fall back to legacy `docker-compose`.
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
DC=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
DC=(docker-compose)
|
||||
else
|
||||
die "Docker Compose v2 plugin not found ('docker compose'). Install docker-compose-plugin."
|
||||
fi
|
||||
|
||||
[ -f "$BASE" ] || die "missing $BASE in $REPO_DIR"
|
||||
[ -f "$OVERRIDE" ] || die "missing $OVERRIDE in $REPO_DIR"
|
||||
|
||||
# An .env is required for prod (JWT_SECRET et al. have no safe default); optional
|
||||
# for dev (we inject a benign local secret below). Pass --env-file only when it
|
||||
# exists so dev works without one.
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
ENV_ARGS=(--env-file "$ENV_FILE")
|
||||
elif [ "$ENV" = "prod" ]; then
|
||||
die "no $ENV_FILE found. Copy .env.example to $ENV_FILE and set JWT_SECRET/REGISTRY/TAG. (prod has no safe defaults.)"
|
||||
else
|
||||
# The BASE compose file makes JWT_SECRET shell-required (${JWT_SECRET:?}), which
|
||||
# the dev override's service-level default can't satisfy. For a dev run with no
|
||||
# .env, inject the same benign 32-char local secret the dev override documents so
|
||||
# `up`/`config` work out of the box. NEVER do this for prod (the die above).
|
||||
warn "no $ENV_FILE found — injecting the documented local-dev JWT_SECRET (dev only)."
|
||||
: "${JWT_SECRET:=localdevsecret0123456789abcdef0123}"
|
||||
export JWT_SECRET
|
||||
fi
|
||||
}
|
||||
|
||||
# The assembled compose invocation every subcommand builds on (runs preflight once).
|
||||
compose() { "${DC[@]}" -f "$BASE" -f "$OVERRIDE" "${ENV_ARGS[@]}" "$@"; }
|
||||
|
||||
# --- subcommands --------------------------------------------------------------
|
||||
cmd="${1:-}"; [ "$#" -gt 0 ] && shift || true
|
||||
|
||||
# Help/usage short-circuits before any Docker or .env requirement.
|
||||
case "$cmd" in ""|-h|--help|help) usage 0 ;; esac
|
||||
|
||||
# Reject an unknown command up front (before preflight) so a typo gets a clear
|
||||
# "unknown command" rather than a confusing "no .env" from the prod env check.
|
||||
case "$cmd" in
|
||||
up|start|update|upgrade|down|stop|restart|pull|status|ps|logs|config|exec) ;;
|
||||
*) warn "unknown command: $cmd"; usage 1 ;;
|
||||
esac
|
||||
|
||||
preflight
|
||||
|
||||
case "$cmd" in
|
||||
up|start)
|
||||
info "Starting the parking stack ($B$ENV$N) …"
|
||||
compose up -d "$@"
|
||||
info "Up. ${G}$(compose ps --services 2>/dev/null | tr '\n' ' ')${N}"
|
||||
info "Booth UI: prod → http://<booth-ip>/ · dev → http://<booth-ip>:3000/"
|
||||
;;
|
||||
|
||||
update|upgrade)
|
||||
# The "I know there are new images" path: pull the moving branch tag, then
|
||||
# recreate only what changed. Compose recreates a service whose image digest
|
||||
# moved; unchanged services (and the named volumes — the SQLite DB!) are left
|
||||
# alone. Old image layers are pruned afterwards to reclaim disk.
|
||||
[ "$ENV" = "prod" ] || warn "update on ENV=$ENV: dev builds locally, so 'pull' may be a no-op. Use 'up --build' to rebuild dev."
|
||||
info "Pulling newer images for the ${B}$ENV_FILE${N} TAG …"
|
||||
compose pull
|
||||
info "Recreating changed services (volumes/DB preserved) …"
|
||||
compose up -d --remove-orphans
|
||||
info "Pruning dangling image layers …"
|
||||
docker image prune -f >/dev/null || true
|
||||
info "${G}Update complete.${N} Running:"
|
||||
compose ps
|
||||
;;
|
||||
|
||||
down|stop)
|
||||
info "Stopping the parking stack ($ENV) …"
|
||||
# NOTE: never pass -v here — that would delete the parking-data volume (the
|
||||
# signed event ledger). Volumes are intentionally preserved across down/up.
|
||||
compose down "$@"
|
||||
;;
|
||||
|
||||
restart)
|
||||
info "Restarting (no pull) …"
|
||||
compose restart "$@"
|
||||
;;
|
||||
|
||||
pull)
|
||||
info "Pulling images only (no recreate) …"
|
||||
compose pull "$@"
|
||||
;;
|
||||
|
||||
status|ps)
|
||||
compose ps "$@"
|
||||
;;
|
||||
|
||||
logs)
|
||||
# Follow by default; pass a service name to scope, e.g. `logs server`.
|
||||
compose logs -f --tail=200 "$@"
|
||||
;;
|
||||
|
||||
config)
|
||||
# Render the merged, variable-substituted compose config (debugging).
|
||||
compose config "$@"
|
||||
;;
|
||||
|
||||
exec)
|
||||
[ "$#" -ge 1 ] || die "usage: $0 exec <service> [cmd…] (e.g. exec server sh)"
|
||||
compose exec "$@"
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user