279 lines
12 KiB
YAML
279 lines
12 KiB
YAML
|
|
# Docker Swarm stack for BETA, co-resident with prod on vps2.
|
||
|
|
#
|
||
|
|
# Deployed by the same deploy/stack/deploy-stack.sh as prod, which picks this
|
||
|
|
# file (and beta's ports, env file, LB and DB role) out of deploy/env-topology.sh
|
||
|
|
# when THERMOGRAPH_ENV=beta. Beta moved here from its own box so that a beta
|
||
|
|
# green light is evidence about prod: same orchestrator, same host kernel, same
|
||
|
|
# Postgres build, same Caddy, same mail path, same mesh position.
|
||
|
|
#
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# WHY THIS IS A SEPARATE FILE AND NOT AN OVERLAY ON thermograph-stack.yml
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# `docker stack deploy` accepts multiple -c files and MERGES them. Merging can
|
||
|
|
# add and override, but it cannot REMOVE a service — and the single most
|
||
|
|
# important fact about beta is a removal: it has no `db`. It uses prod's. An
|
||
|
|
# overlay would therefore still create a second Postgres, which is the exact
|
||
|
|
# thing this design exists to avoid. The same goes for the two autoscalers,
|
||
|
|
# which beta deliberately does not run.
|
||
|
|
#
|
||
|
|
# The cost is a file that must be kept roughly in step with prod's by hand.
|
||
|
|
# Keep them in step for anything that affects whether the APP works (env vars,
|
||
|
|
# entrypoints, healthchecks, the migrate contract). Do NOT keep them in step on
|
||
|
|
# scale, replicas or resource limits — those differ on purpose (below).
|
||
|
|
#
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# THE THREE THINGS THAT MAKE CO-RESIDENCY SAFE
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 1. SERVICE NAMES ARE PREFIXED (beta-web, not web). Swarm registers a service's
|
||
|
|
# short name as a DNS alias on every network it joins. Beta's tasks share the
|
||
|
|
# `data` network with prod's, so two services both called `web` would make
|
||
|
|
# `web` ambiguous — prod's frontend could resolve a beta task, and vice
|
||
|
|
# versa. The prefix removes the collision without touching prod's stack file.
|
||
|
|
#
|
||
|
|
# 2. THE DATABASE IS SHARED, THE DATA IS NOT. Beta connects to prod's `db`
|
||
|
|
# service as the role `thermograph_beta`, to the database `thermograph_beta`.
|
||
|
|
# That role owns only its own database (see deploy/db/provision-env-db.sh),
|
||
|
|
# so a beta deploy running an unmerged branch — or a migration that goes
|
||
|
|
# wrong — cannot read or write production data. One server is a capacity
|
||
|
|
# decision, not a trust decision.
|
||
|
|
#
|
||
|
|
# 3. NOTHING ELSE IS SHARED BY ACCIDENT. Separate checkout (/opt/thermograph-beta),
|
||
|
|
# separate rendered env file (/etc/thermograph-beta.env), separate stack env
|
||
|
|
# (/etc/thermograph/beta-stack.env), separate volumes, separate loopback
|
||
|
|
# ports (8237/8180 — prod owns 8137/8080), separate LB container, separate
|
||
|
|
# deploy lock and image-tag file. Every one of those is derived in
|
||
|
|
# env-topology.sh rather than repeated here by hand.
|
||
|
|
#
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# WHAT BETA DELIBERATELY DOES NOT DO
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# - No autoscaling: fixed 1 replica per service. Beta exists to answer "does
|
||
|
|
# this code work", not "does it scale"; a second replica would only add a
|
||
|
|
# variable prod's rehearsal doesn't need, on a host prod is also using.
|
||
|
|
# - No IndexNow ping and no city-archive warm (deploy-stack.sh gates both on
|
||
|
|
# TG_POST_DEPLOY). Pinging IndexNow from beta asks Bing/DuckDuckGo/Yandex to
|
||
|
|
# index beta.thermograph.org; the warm spends the shared upstream archive
|
||
|
|
# quota to fill a cache only a rehearsal reads.
|
||
|
|
# - No real mail and no Discord gateway. Both are governed by beta's vault
|
||
|
|
# (THERMOGRAPH_MAIL_BACKEND=console, THERMOGRAPH_DISCORD_BOT=0) rather than
|
||
|
|
# pinned here, so the operator can opt in with `sops edit` if a release ever
|
||
|
|
# genuinely needs to rehearse them. Discord in particular allows ONE gateway
|
||
|
|
# connection per bot token — beta and prod must never both hold one.
|
||
|
|
|
||
|
|
services:
|
||
|
|
beta-web:
|
||
|
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
|
||
|
|
entrypoint: ["/host/env-entrypoint.sh"]
|
||
|
|
environment:
|
||
|
|
# Beta's OWN role and OWN database on the shared instance. `db` resolves
|
||
|
|
# across the external `data` network to prod's db service.
|
||
|
|
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph_beta:${POSTGRES_PASSWORD}@db:5432/thermograph_beta
|
||
|
|
THERMOGRAPH_BASE: /
|
||
|
|
PORT: 8137
|
||
|
|
THERMOGRAPH_SERVICE_ROLE: backend
|
||
|
|
THERMOGRAPH_FRONTEND_BASE_INTERNAL: http://beta-frontend:8080
|
||
|
|
WORKERS: ${BETA_WEB_WORKERS:-2}
|
||
|
|
THERMOGRAPH_DATA_DIR: /state
|
||
|
|
# Never the notifier/scheduler — that is beta-worker's job, exactly as in
|
||
|
|
# prod, so the two files stay honest about which process owns what.
|
||
|
|
THERMOGRAPH_ROLE: web
|
||
|
|
# Migrations run as the one-shot task in deploy-stack.sh.
|
||
|
|
RUN_MIGRATIONS: "0"
|
||
|
|
# Overlay tasks reach the HOST's Postfix via the docker_gwbridge gateway.
|
||
|
|
# Same host, same Postfix as prod — but see the mail note in the header:
|
||
|
|
# beta's vault selects the console backend, so nothing is actually sent.
|
||
|
|
THERMOGRAPH_SMTP_HOST: ${STACK_SMTP_HOST:-172.18.0.1}
|
||
|
|
THERMOGRAPH_LAKE_URL: http://beta-lake:8141
|
||
|
|
volumes:
|
||
|
|
- appdata:/state
|
||
|
|
- applogs:/app/logs
|
||
|
|
- /opt/thermograph-beta/infra/deploy/stack/env-entrypoint.sh:/host/env-entrypoint.sh:ro
|
||
|
|
- /etc/thermograph/beta-stack.env:/host/thermograph.env:ro
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
- data
|
||
|
|
deploy:
|
||
|
|
replicas: 1
|
||
|
|
# vps2 is the Swarm manager and every volume here is local to it. The
|
||
|
|
# desktop is a worker on this mesh and must never be scheduled the app.
|
||
|
|
placement:
|
||
|
|
constraints: ["node.role == manager"]
|
||
|
|
resources:
|
||
|
|
limits:
|
||
|
|
cpus: "${BETA_WEB_CPUS:-2}"
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
update_config:
|
||
|
|
order: start-first
|
||
|
|
failure_action: rollback
|
||
|
|
|
||
|
|
beta-worker:
|
||
|
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
|
||
|
|
entrypoint: ["/host/env-entrypoint.sh"]
|
||
|
|
environment:
|
||
|
|
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph_beta:${POSTGRES_PASSWORD}@db:5432/thermograph_beta
|
||
|
|
THERMOGRAPH_BASE: /
|
||
|
|
PORT: 8137
|
||
|
|
THERMOGRAPH_SERVICE_ROLE: backend
|
||
|
|
THERMOGRAPH_FRONTEND_BASE_INTERNAL: http://beta-frontend:8080
|
||
|
|
WORKERS: "1"
|
||
|
|
THERMOGRAPH_DATA_DIR: /state
|
||
|
|
THERMOGRAPH_ROLE: worker
|
||
|
|
# The advisory lock is taken in beta's OWN database, so it can never
|
||
|
|
# contend with prod's worker despite the shared server.
|
||
|
|
THERMOGRAPH_SINGLETON_PG: "1"
|
||
|
|
RUN_MIGRATIONS: "0"
|
||
|
|
THERMOGRAPH_SMTP_HOST: ${STACK_SMTP_HOST:-172.18.0.1}
|
||
|
|
THERMOGRAPH_LAKE_URL: http://beta-lake:8141
|
||
|
|
volumes:
|
||
|
|
- appdata:/state
|
||
|
|
- applogs:/app/logs
|
||
|
|
- /opt/thermograph-beta/infra/deploy/stack/env-entrypoint.sh:/host/env-entrypoint.sh:ro
|
||
|
|
- /etc/thermograph/beta-stack.env:/host/thermograph.env:ro
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
- data
|
||
|
|
deploy:
|
||
|
|
replicas: 1
|
||
|
|
placement:
|
||
|
|
constraints: ["node.role == manager"]
|
||
|
|
resources:
|
||
|
|
limits:
|
||
|
|
cpus: "${BETA_WORKER_CPUS:-1}"
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
|
||
|
|
beta-lake:
|
||
|
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
|
||
|
|
entrypoint: ["/host/env-entrypoint.sh"]
|
||
|
|
environment:
|
||
|
|
THERMOGRAPH_ROLE: lake
|
||
|
|
PORT: 8141
|
||
|
|
THERMOGRAPH_SERVICE_ROLE: backend
|
||
|
|
WORKERS: "1"
|
||
|
|
THERMOGRAPH_LAKE_CACHE: /state/lake-cache
|
||
|
|
volumes:
|
||
|
|
# Beta's own cache volume. Deliberately not prod's: they are read caches
|
||
|
|
# of the same bucket, but sharing a volume across two stacks would couple
|
||
|
|
# their lifecycles for no gain.
|
||
|
|
- lakecache:/state
|
||
|
|
- /opt/thermograph-beta/infra/deploy/stack/env-entrypoint.sh:/host/env-entrypoint.sh:ro
|
||
|
|
- /etc/thermograph/beta-stack.env:/host/thermograph.env:ro
|
||
|
|
# No `data` network: the lake reads object storage, never Postgres.
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
deploy:
|
||
|
|
replicas: 1
|
||
|
|
placement:
|
||
|
|
constraints: ["node.role == manager"]
|
||
|
|
resources:
|
||
|
|
limits:
|
||
|
|
cpus: "${BETA_LAKE_CPUS:-1}"
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
update_config:
|
||
|
|
order: start-first
|
||
|
|
failure_action: rollback
|
||
|
|
|
||
|
|
beta-daemon:
|
||
|
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
|
||
|
|
# Same reasoning as prod's daemon: NOT env-entrypoint.sh, because that shim
|
||
|
|
# execs the image's own entrypoint (Alembic + uvicorn) and migrations belong
|
||
|
|
# to the one-shot task. Source the host-rendered env and exec the binary.
|
||
|
|
#
|
||
|
|
# Note the ordering consequence, which is load-bearing here: this sources
|
||
|
|
# the env file AFTER the `environment:` block is applied, so a key present
|
||
|
|
# in /etc/thermograph/beta-stack.env WINS over one set below. That is why
|
||
|
|
# THERMOGRAPH_DISCORD_BOT=0 lives in beta's vault and not in this file — a
|
||
|
|
# value set here would be silently overridden if the vault ever set one.
|
||
|
|
entrypoint:
|
||
|
|
- /bin/bash
|
||
|
|
- -c
|
||
|
|
- 'set -a; [ -f /host/thermograph.env ] && . /host/thermograph.env; set +a; exec /usr/local/bin/thermograph-daemon'
|
||
|
|
environment:
|
||
|
|
THERMOGRAPH_API_BASE_INTERNAL: http://beta-web:8137
|
||
|
|
volumes:
|
||
|
|
- /etc/thermograph/beta-stack.env:/host/thermograph.env:ro
|
||
|
|
# The image HEALTHCHECK curls /healthz on ${PORT}; the daemon serves
|
||
|
|
# nothing, so without this override Swarm restarts it forever.
|
||
|
|
healthcheck:
|
||
|
|
disable: true
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
deploy:
|
||
|
|
# EXACTLY 1, and in beta's case the Discord gateway is off entirely
|
||
|
|
# (vault: THERMOGRAPH_DISCORD_BOT=0) because prod's daemon holds the only
|
||
|
|
# permitted gateway connection for that bot token.
|
||
|
|
replicas: 1
|
||
|
|
placement:
|
||
|
|
constraints: ["node.role == manager"]
|
||
|
|
resources:
|
||
|
|
limits:
|
||
|
|
cpus: "0.5"
|
||
|
|
memory: 128m
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
update_config:
|
||
|
|
order: stop-first
|
||
|
|
failure_action: rollback
|
||
|
|
|
||
|
|
beta-frontend:
|
||
|
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required}
|
||
|
|
entrypoint: ["/host/env-entrypoint.sh"]
|
||
|
|
# REQUIRED: overriding `entrypoint:` with no `command:` drops the image's
|
||
|
|
# CMD entirely, and env-entrypoint.sh's fallback (`exec uvicorn app:app`)
|
||
|
|
# does not exist in this Go image — the task would exit 127 every deploy.
|
||
|
|
command: ["/usr/local/bin/thermograph-frontend"]
|
||
|
|
environment:
|
||
|
|
THERMOGRAPH_BASE: /
|
||
|
|
PORT: 8080
|
||
|
|
THERMOGRAPH_SERVICE_ROLE: frontend
|
||
|
|
THERMOGRAPH_API_BASE_INTERNAL: http://beta-web:8137
|
||
|
|
volumes:
|
||
|
|
- /opt/thermograph-beta/infra/deploy/stack/env-entrypoint.sh:/host/env-entrypoint.sh:ro
|
||
|
|
- /etc/thermograph/beta-stack.env:/host/thermograph.env:ro
|
||
|
|
networks:
|
||
|
|
- internal
|
||
|
|
deploy:
|
||
|
|
replicas: 1
|
||
|
|
placement:
|
||
|
|
constraints: ["node.role == manager"]
|
||
|
|
resources:
|
||
|
|
limits:
|
||
|
|
cpus: "${BETA_FRONTEND_CPUS:-1}"
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
update_config:
|
||
|
|
order: start-first
|
||
|
|
failure_action: rollback
|
||
|
|
|
||
|
|
networks:
|
||
|
|
# Beta's own east-west network: beta-web <-> beta-frontend <-> beta-lake, and
|
||
|
|
# the loopback LB bridge joins it (hence attachable). Keeping this separate
|
||
|
|
# from prod's overlay means beta's ordinary traffic never touches it.
|
||
|
|
internal:
|
||
|
|
driver: overlay
|
||
|
|
attachable: true
|
||
|
|
|
||
|
|
# Prod's overlay, joined ONLY to reach the shared `db` service. Declared
|
||
|
|
# external because prod's stack owns it: `docker stack deploy` of THIS file
|
||
|
|
# must never create, modify or (on `docker stack rm thermograph-beta`) remove
|
||
|
|
# the network prod's database is on.
|
||
|
|
#
|
||
|
|
# Consequence worth knowing before you tear anything down: `docker stack rm
|
||
|
|
# thermograph` would take this network with it and beta would lose its
|
||
|
|
# database link until prod is redeployed.
|
||
|
|
data:
|
||
|
|
external: true
|
||
|
|
name: thermograph_internal
|
||
|
|
|
||
|
|
volumes:
|
||
|
|
# Beta's own, created by this stack under the thermograph-beta_ prefix. Unlike
|
||
|
|
# prod's (which are `external` because they were inherited from the compose
|
||
|
|
# era and hold live data), these can be recreated: beta's appdata is a parquet
|
||
|
|
# cache plus derived files, and its DATABASE — the part that matters — lives
|
||
|
|
# on the shared instance, not here.
|
||
|
|
appdata: {}
|
||
|
|
applogs: {}
|
||
|
|
lakecache: {}
|