Files
parking_solution/scripts/booth.sh
T
julian 83298bc0c5
Build desktop / desktop (push) Successful in 4m37s
Build & push images / images (push) Successful in 2m52s
CI / check (push) Successful in 37s
fix(deploy): booth.sh works in the flat /opt layout; .env TAG=dev default
The booth deploys the compose files FLAT (e.g. /opt/parking_systems/) with
booth.sh next to them, but the script assumed it lived in <repo>/scripts/ and
blindly did `cd ..` — so REPO_DIR resolved to the parent, where there are no
compose files, and every subcommand operated on the wrong dir. `usage()` then
sed-read a relative $0 that no longer existed after the cd ("can't read
booth.sh"). Discover the compose files instead: check the script's own dir,
then ../, then $PWD, and cd to whichever has docker-compose.yml. usage() reads
an absolute $SELF so it survives the cd.

Also: .env.example defaulted TAG=main, but the registry only has dev-* tags
(no main build yet), so `compose pull` 404s. Default to TAG=dev and document
the moving-vs-immutable (dev / dev-<sha>) tag scheme.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-27 08:42:11 +02:00

190 lines
7.2 KiB
Bash
Executable File

#!/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.
#
# ./booth.sh up # start the stack (detached)
# ./booth.sh update # pull newer images + recreate (the "there are new
# # images" case) — see `update` below
# ./booth.sh down # stop the stack
# ./booth.sh restart # restart without pulling
# ./booth.sh status # what's running
# ./booth.sh logs # follow logs (Ctrl-C to stop)
# ./booth.sh ps|pull|config|exec …
#
# Runs from wherever it sits next to the compose files (the booth deploys them
# flat, e.g. /opt/parking_systems/) or from the repo at scripts/booth.sh.
#
# 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 ./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 compose files -------------------------------------------------
# The script must work in BOTH layouts: in the repo at <repo>/scripts/booth.sh
# (files one level up), AND deployed flat on the booth (booth.sh sits next to the
# compose files, e.g. /opt/parking_systems/). So we don't assume a `scripts/`
# parent — we look for docker-compose.yml in the script's own dir, then ../,
# then $PWD, and cd there. (An absolute SELF is also kept for usage()/sed.)
SELF="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/$(basename -- "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname -- "$SELF")"
REPO_DIR=""
for d in "$SCRIPT_DIR" "$SCRIPT_DIR/.." "$PWD"; do
if [ -f "$d/docker-compose.yml" ]; then REPO_DIR="$(cd -- "$d" && pwd)"; break; fi
done
[ -n "$REPO_DIR" ] || {
printf 'ERROR: docker-compose.yml not found (looked in %s, its parent, and %s).\n' \
"$SCRIPT_DIR" "$PWD" >&2
exit 1
}
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,26p' "$SELF" | 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