#!/usr/bin/env bash # Dev deploy: roll the per-service registry-pull stack (see deploy.sh) onto the # dev compose overlay instead of beta/prod's Swarm stacks. # # Dev lives on vps1 now, at /opt/thermograph-dev — not on the operator's desktop. # The desktop hosts no Thermograph environment any more; it runs the AI models # and offers flex Swarm capacity. What moved is only WHERE and HOW dev is # reached: it is a normal fleet host now (SSH deploy from CI, SOPS render at # /etc/thermograph.env, mesh-only exposure on 10.10.0.2:8137), rather than a # sudo-free systemd --user stack on someone's Wi-Fi. # # $APP_DIR is a checkout of the monorepo (deploy assets under infra/), the same # way /opt/thermograph is for prod. A laptop can still point APP_DIR at a # checkout under $HOME and get the old local behaviour. # # Design: a thin wrapper around deploy.sh, not a duplicate. deploy.sh already owns # the entire per-service registry-pull mechanism -- secrets sourcing, docker login, # the retry-pull loop, --no-deps single-service rolls vs. --remove-orphans `all`, # .image-tags.env persistence, and the 8137/8080 health checks. None of that is # dev-specific; the only things dev actually changes are WHERE it deploys (a # separate checkout + branch), WHICH compose files are in play (the base file # plus docker-compose.dev.yml's uncapped/mesh-published overrides), and its # secrets policy (below). All are expressible as environment (APP_DIR/BRANCH # that deploy.sh already reads, and docker compose's own COMPOSE_FILE # variable), so re-exec'ing deploy.sh with that environment set covers it with # no forked copy of the pull/roll/health logic to drift out of sync. If dev ever # needs deploy logic that genuinely diverges from beta/prod (not just "different # files/directory"), promote this to a standalone script at that point rather # than growing special cases into deploy.sh. # # Usage, mirrors deploy.sh directly: # # roll just the backend onto a dev-tagged image: # ssh vps1 'SERVICE=backend BACKEND_IMAGE_TAG=sha-<12hex> /opt/thermograph-dev/infra/deploy/deploy-dev.sh' # # bring the whole dev stack up (both tags required, same as deploy.sh): # ssh vps1 'SERVICE=all BACKEND_IMAGE_TAG=sha- FRONTEND_IMAGE_TAG=sha- /opt/thermograph-dev/infra/deploy/deploy-dev.sh' set -euo pipefail # Dev context: its own checkout and its own branch, on its own host (vps1). # Both come from deploy/env-topology.sh so there is one place that says where # dev lives. $APP_DIR is still honoured when set explicitly — that is how a # laptop points this at a checkout under $HOME. _dev_self="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=infra/deploy/env-topology.sh . "$_dev_self/env-topology.sh" thermograph_topology dev APP_DIR="${APP_DIR:-$TG_APP_DIR}" BRANCH="${BRANCH:-$TG_BRANCH}" export APP_DIR BRANCH # deploy.sh resolves the environment itself; say so explicitly rather than # letting it fall back to a host marker that, on vps1, names dev anyway. export THERMOGRAPH_ENV=dev # Point every `docker compose` invocation inside deploy.sh at the LAN-dev overlay # (uncapped CPU, backend published on 0.0.0.0:8137, frontend unpublished -- see # docker-compose.dev.yml) without deploy.sh needing to know dev exists at all. # COMPOSE_FILE is docker compose's own env var for this; ':' is the Linux/macOS # path-list separator it expects (';' only on Windows). export COMPOSE_FILE="docker-compose.yml:docker-compose.dev.yml" # Pin the dev project name: docker-compose.yml pins `name: thermograph` for # prod/beta, but dev's stack has always been the separate "thermograph-dev" # project (originally derived from this checkout's directory name). The env # var wins over the compose-file `name:` key, preserving that split. export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-thermograph-dev}" # --- Secrets ----------------------------------------------------------------- # dev renders from the SOPS vault the same way prod and beta do -- same files, same # tool, same edit/commit/deploy loop -- with two dev-only adjustments. Both are # expressed as environment that render-secrets.sh already parameterises, so nothing # about the rendering logic is forked. See deploy/secrets/README.md. # (1) dev renders deploy/secrets/dev.yaml ALONE; it does NOT layer common.yaml. # # common.yaml is the internet-facing fleet's SHARED PRODUCTION credential set: the # registry token, both S3 keypairs (the bucket keypair is read-write, and that # bucket also holds prod's database backups), the VAPID private key that signs push # to real subscribers, the IndexNow key, the metrics token. Layering it here would # write all of them in plaintext to /etc/thermograph.env on the box that is also the # Forgejo CI runner, and hand them to every container in the dev stack through # `env_file:` -- that is, to whatever unreviewed branch dev happens to be running. # It would also silently apply THERMOGRAPH_COOKIE_SECURE=1, which is correct for the # TLS hosts and breaks login on dev's plain-HTTP LAN URL. # # dev.yaml is self-contained instead: it carries dev's own copy of everything dev # genuinely needs, and nothing else. export THERMOGRAPH_SECRETS_SKIP_COMMON=1 # (1b) Snap-packaged Docker is confined to $HOME (plus a short allowlist) and # silently can't see /etc/thermograph.env at all, so env_file: /etc/thermograph.env # resolves to nothing for every container no matter how correctly it's rendered. # Where that is the case, mirror the render to a path under $APP_DIR; # docker-compose.dev.yml carries a matching optional env_file entry. # # CONDITIONAL, not unconditional: this writes a plaintext copy of the render # into the checkout, and on vps1 (ordinary apt Docker, which reads /etc fine) # that copy would be pure liability. Detect the confinement rather than add a # knob someone has to remember — the docker binary resolving under /snap is # exactly the condition that breaks the /etc path. _docker_bin="$(command -v docker || true)" if [ -n "$_docker_bin" ] && case "$(readlink -f "$_docker_bin")" in /snap/*) true ;; *) false ;; esac; then export THERMOGRAPH_SECRETS_ENV_FILE_MIRROR="$APP_DIR/infra/deploy/dev-secrets.env" fi # (2) The age key is read from wherever it is READABLE, not necessarily # /etc/thermograph/age.key. render-secrets.sh falls back to `sudo cat` for a # root-owned 0400 key, which needs passwordless sudo. # # On vps1 that is the normal case: the `agent` user has passwordless sudo and # the key sits at the fleet-standard path, so this block does nothing. The # fallback exists for the laptop case — a checkout where the key lives only in # the operator's own keyring (that machine is where `sops` is run), and where a # non-interactive shell could not prompt for sudo even if the key were in /etc. # Preferring the standard path when readable keeps vps1 on the fleet convention # and means provisioning dev needs no second copy of the estate's recovery root. if [ -z "${THERMOGRAPH_AGE_KEY:-}" ] && [ ! -r /etc/thermograph/age.key ] \ && [ -r "$HOME/.config/sops/age/keys.txt" ]; then export THERMOGRAPH_AGE_KEY="$HOME/.config/sops/age/keys.txt" fi # Fail closed rather than leak. If this box is provisioned to render (an env marker # AND a key) but the render-secrets.sh in this checkout predates # THERMOGRAPH_SECRETS_SKIP_COMMON, the render would quietly concatenate common.yaml # into dev's /etc/thermograph.env -- exactly the leak (1) exists to prevent, with a # success exit code. Checking a sibling script for the flag is blunt, but the two # files are versioned together in this same checkout, and the alternative failure # mode is unreviewed code running with the estate's object-storage keys. Remove this # once the flag is unconditionally present in render-secrets.sh. _render_sh="$(dirname "$0")/render-secrets.sh" _secrets_marker="${THERMOGRAPH_SECRETS_ENV_FILE:-/etc/thermograph/secrets-env}" _age_key="${THERMOGRAPH_AGE_KEY:-/etc/thermograph/age.key}" if [ -s "$_secrets_marker" ] && [ -f "$_age_key" ] && [ -f "$_render_sh" ] \ && ! grep -q 'THERMOGRAPH_SECRETS_SKIP_COMMON' "$_render_sh"; then echo "!! This box is provisioned to render SOPS secrets (env marker + age key)," >&2 echo "!! but $_render_sh does not honour" >&2 echo "!! THERMOGRAPH_SECRETS_SKIP_COMMON, so it would render common.yaml -- the" >&2 echo "!! fleet's shared PRODUCTION credentials -- into /etc/thermograph.env on the" >&2 echo "!! CI-runner box. Refusing to deploy." >&2 echo "!! Fix: update deploy/render-secrets.sh, or remove $_secrets_marker to fall" >&2 echo "!! back to the un-vaulted dev path." >&2 exit 1 fi # deploy.sh needs POSTGRES_PASSWORD to interpolate the compose file (both to init the # db container and to build backend's THERMOGRAPH_DATABASE_URL). On a provisioned dev # box it arrives from dev.yaml via the render above -- deploy.sh sources # /etc/thermograph.env AFTER calling the renderer, so the vault value wins over this # line. This default is the fallback for the un-vaulted paths that remain: a fresh # box before provisioning, and a bare `make dev-up`. It is the same value dev.yaml # holds and the value dev's existing pgdata volume was initialized with -- keep the # two in step, and note that changing it needs an ALTER ROLE against the running # database as well (see deploy/secrets/README.md), not just an edit. export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-thermograph-dev}" # REGISTRY_TOKEN (for `docker login` in deploy.sh) is deliberately NOT in dev.yaml # and has no default here. dev pulls with the persistent `docker login` credential # already in this host's docker config -- the same arrangement the prod/beta CI # deploy paths use, and the reason deploy.sh's login step is conditional. Putting a # registry token in dev's vault would only add a second copy of a credential the box # already has, in a file more processes can read. Export it by hand for a one-off run # on a box that has never logged in; a genuine auth problem fails loudly at `pull`. echo "==> LAN-dev deploy: APP_DIR=$APP_DIR BRANCH=$BRANCH COMPOSE_FILE=$COMPOSE_FILE" exec "$(dirname "$0")/deploy.sh" "$@"