thermograph/deploy/deploy.sh

144 lines
6.7 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
Decouple Terraform from the app repo; add a GCP host scaffold Content-change pass following the extraction from the app monorepo (this repo now stands alone, sourced via git filter-repo to preserve history): - terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every app-secret Terraform variable (postgres_password, auth_secret, VAPID keys, registry_token, Discord/SMTP creds, ...) and the random_password/random_id generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole source of app secrets, rendered at deploy time by deploy/render-secrets.sh; Terraform renders only a non-secret /etc/thermograph-topology.env (sizing, routing) via the renamed thermograph-topology.env.tftpl template. - hosts gains a required app_image_tag field: the host's own checkout is now this infra repo, not the app repo, so there is no "current commit" to derive an image tag from — every host pins one explicitly. repo_url now points at this repo (private; typically needs an embedded read token). - deploy.sh: IMAGE_TAG is now required from the environment instead of derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which would have silently resolved to the wrong or a nonexistent tag. - New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall only, then feeds its IP into the same thermograph-host module every SSH-managed host already uses — one provisioning path regardless of how a host came to exist. var.gcp_hosts defaults to {}, so no google_* resource is planned and the provider is never invoked without it (verified: plan and validate succeed with no GCP credentials configured). - terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated for the new secrets model, the GCP scaffold, and this repo's own identity. Verified: terraform fmt/validate/init clean; plan succeeds against realistic dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans 6 resources with no live credentials, confirming the composition wires correctly end to end).
2026-07-22 04:46:05 +00:00
# Pull this INFRA repo's checkout up to date, then roll the docker-compose stack
# (backend + frontend + PostgreSQL) onto the app image tagged IMAGE_TAG. Run on
# the VPS — the app repo's Forgejo Actions workflow invokes this over SSH (see
# .forgejo/workflows/deploy.yml there), and you can run it by hand too.
#
Decouple Terraform from the app repo; add a GCP host scaffold Content-change pass following the extraction from the app monorepo (this repo now stands alone, sourced via git filter-repo to preserve history): - terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every app-secret Terraform variable (postgres_password, auth_secret, VAPID keys, registry_token, Discord/SMTP creds, ...) and the random_password/random_id generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole source of app secrets, rendered at deploy time by deploy/render-secrets.sh; Terraform renders only a non-secret /etc/thermograph-topology.env (sizing, routing) via the renamed thermograph-topology.env.tftpl template. - hosts gains a required app_image_tag field: the host's own checkout is now this infra repo, not the app repo, so there is no "current commit" to derive an image tag from — every host pins one explicitly. repo_url now points at this repo (private; typically needs an embedded read token). - deploy.sh: IMAGE_TAG is now required from the environment instead of derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which would have silently resolved to the wrong or a nonexistent tag. - New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall only, then feeds its IP into the same thermograph-host module every SSH-managed host already uses — one provisioning path regardless of how a host came to exist. var.gcp_hosts defaults to {}, so no google_* resource is planned and the provider is never invoked without it (verified: plan and validate succeed with no GCP credentials configured). - terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated for the new secrets model, the GCP scaffold, and this repo's own identity. Verified: terraform fmt/validate/init clean; plan succeeds against realistic dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans 6 resources with no live credentials, confirming the composition wires correctly end to end).
2026-07-22 04:46:05 +00:00
# ssh deploy@vps 'IMAGE_TAG=sha-<12hex> /opt/thermograph/deploy/deploy.sh'
#
# This checkout is thermograph-infra, not the app repo: BRANCH is this repo's
# branch (compose files, db init, the secrets vault, this script itself);
# IMAGE_TAG is the separately-published app image to run (see the IMAGE_TAG
# guard below) -- the two are independent and rarely change together.
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/thermograph}"
BRANCH="${BRANCH:-main}"
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
HEALTH_PORT="${HEALTH_PORT:-8137}"
cd "$APP_DIR"
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
# Secrets (POSTGRES_PASSWORD, VAPID keys, AUTH_SECRET, ...) drive compose
# interpolation and are also loaded into the backend container via env_file.
#
# When this host is configured for SOPS (an age key + /etc/thermograph/secrets-env),
# first render /etc/thermograph.env from the committed encrypted source of truth
# (deploy/secrets/*.yaml) so a key rotation is just an edit+commit+deploy. The guard
# on the helper's existence keeps the very deploy that INTRODUCES this file safe: on
# the first pass the checkout may predate it (it arrives with the git reset below,
# after which deploy.sh re-execs), so a missing helper simply falls back to the
# existing /etc/thermograph.env. Then source it so a by-hand run interpolates the
# same as the systemd unit does. See deploy/render-secrets.sh + deploy/secrets/.
if [ -f "$APP_DIR/deploy/render-secrets.sh" ]; then
# shellcheck source=deploy/render-secrets.sh
. "$APP_DIR/deploy/render-secrets.sh"
render_thermograph_secrets "$APP_DIR"
fi
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
set -a; . /etc/thermograph.env 2>/dev/null || true; set +a
# Pre-warm the ~750 city-page archives so /climate pages serve from cache and a
# search-engine crawl never bursts the archive API quota. Detached inside the
# backend container (compose exec -d), idempotent (skips already-cached cells),
# so it never blocks the deploy or health check and is cheap on every deploy
# after the first full warm.
warm_city_archives() {
echo "==> Warming city-page archives in the background (backend:/app/logs/warm-cities.log)"
docker compose exec -d backend sh -c \
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
'python warm_cities.py --pace 2 >> /app/logs/warm-cities.log 2>&1' || true
}
# Notify IndexNow (Bing / DuckDuckGo / Yandex) of the site's URLs, but only when
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
# the set of pages actually changed (a new/removed city) — code-only deploys skip.
# Best-effort: never fails the deploy.
ping_indexnow() {
echo "==> Pinging IndexNow (only if the URL set changed)"
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
local base="${THERMOGRAPH_BASE_URL:-https://thermograph.org}"
docker compose exec -T backend python indexnow.py --if-changed "$base" \
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
|| echo "!! IndexNow ping failed (non-fatal)" >&2
}
echo "==> Fetching $BRANCH"
git fetch --prune origin "$BRANCH"
git reset --hard "origin/$BRANCH"
# Re-exec: git reset --hard just rewrote this very file's bytes on disk while
# it's still running. bash reads a script via buffered, byte-offset I/O, so
# anything AFTER this point in the OLD execution can read from the wrong
# offset once the file's size/content changed underneath it -- a classic
# self-modifying-script footgun. Confirmed live: after this PR added ~15
# lines above, one deploy ran with the OLD "Building images" log lines even
# though `git status` showed the checkout correctly at the NEW commit --
# the file changed under a running interpreter, not the checkout. Restart
# fresh from the now-updated file so everything after this line is
# guaranteed self-consistent. Guarded so the second invocation doesn't
# fetch+reset+re-exec forever.
if [ -z "${DEPLOY_SH_REEXECED:-}" ]; then
export DEPLOY_SH_REEXECED=1
exec "$0" "$@"
fi
Decouple Terraform from the app repo; add a GCP host scaffold Content-change pass following the extraction from the app monorepo (this repo now stands alone, sourced via git filter-repo to preserve history): - terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every app-secret Terraform variable (postgres_password, auth_secret, VAPID keys, registry_token, Discord/SMTP creds, ...) and the random_password/random_id generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole source of app secrets, rendered at deploy time by deploy/render-secrets.sh; Terraform renders only a non-secret /etc/thermograph-topology.env (sizing, routing) via the renamed thermograph-topology.env.tftpl template. - hosts gains a required app_image_tag field: the host's own checkout is now this infra repo, not the app repo, so there is no "current commit" to derive an image tag from — every host pins one explicitly. repo_url now points at this repo (private; typically needs an embedded read token). - deploy.sh: IMAGE_TAG is now required from the environment instead of derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which would have silently resolved to the wrong or a nonexistent tag. - New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall only, then feeds its IP into the same thermograph-host module every SSH-managed host already uses — one provisioning path regardless of how a host came to exist. var.gcp_hosts defaults to {}, so no google_* resource is planned and the provider is never invoked without it (verified: plan and validate succeed with no GCP credentials configured). - terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated for the new secrets model, the GCP scaffold, and this repo's own identity. Verified: terraform fmt/validate/init clean; plan succeeds against realistic dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans 6 resources with no live credentials, confirming the composition wires correctly end to end).
2026-07-22 04:46:05 +00:00
# Registry-pull cutover (repo-split Stage 6): pull the app image build-push.yml
# (in the APP repo) already built and pushed, instead of building in place. This
# checkout is thermograph-infra, not the app repo, so there is no "current commit"
# to derive the tag from the way the old same-repo deploy.sh once did -- IMAGE_TAG
# must be supplied explicitly (e.g. by the caller exporting
# IMAGE_TAG="sha-<12 hex>" matching the app-repo commit to run, or a semver tag).
REGISTRY_HOST="${REGISTRY_HOST:-git.thermograph.org}"
IMAGE_PATH="${IMAGE_PATH:-emi/thermograph/app}"
Decouple Terraform from the app repo; add a GCP host scaffold Content-change pass following the extraction from the app monorepo (this repo now stands alone, sourced via git filter-repo to preserve history): - terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every app-secret Terraform variable (postgres_password, auth_secret, VAPID keys, registry_token, Discord/SMTP creds, ...) and the random_password/random_id generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole source of app secrets, rendered at deploy time by deploy/render-secrets.sh; Terraform renders only a non-secret /etc/thermograph-topology.env (sizing, routing) via the renamed thermograph-topology.env.tftpl template. - hosts gains a required app_image_tag field: the host's own checkout is now this infra repo, not the app repo, so there is no "current commit" to derive an image tag from — every host pins one explicitly. repo_url now points at this repo (private; typically needs an embedded read token). - deploy.sh: IMAGE_TAG is now required from the environment instead of derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which would have silently resolved to the wrong or a nonexistent tag. - New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall only, then feeds its IP into the same thermograph-host module every SSH-managed host already uses — one provisioning path regardless of how a host came to exist. var.gcp_hosts defaults to {}, so no google_* resource is planned and the provider is never invoked without it (verified: plan and validate succeed with no GCP credentials configured). - terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated for the new secrets model, the GCP scaffold, and this repo's own identity. Verified: terraform fmt/validate/init clean; plan succeeds against realistic dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans 6 resources with no live credentials, confirming the composition wires correctly end to end).
2026-07-22 04:46:05 +00:00
IMAGE_TAG="${IMAGE_TAG:?set IMAGE_TAG=sha-<12-hex-app-commit> (or a semver tag) -- this checkout can't derive it, it isn't the app repo}"
export IMAGE_TAG REGISTRY_HOST IMAGE_PATH
echo "==> Logging in to the registry ($REGISTRY_HOST)"
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin
echo "==> Pulling images ($IMAGE_TAG)"
# Retry: build-push.yml (triggered by the same push) has no ordering
# guarantee against this deploy -- Forgejo/GitHub Actions `needs:` only
# works between jobs in ONE workflow file, not across two workflows
# triggered by the same event. Confirmed live: a real deploy raced ahead of
# the push and failed with "not found". A bounded retry (~5 min) comfortably
# covers a normal build; a genuine problem (bad tag, registry down) still
# fails loudly after that, just a bit slower to surface.
pull_ok=0
for i in $(seq 1 30); do
if docker compose pull backend frontend; then
pull_ok=1
break
fi
echo " pull attempt $i/30 failed (image may not be pushed yet); retrying in 10s..." >&2
sleep 10
done
if [ "$pull_ok" != 1 ]; then
echo "!! docker compose pull failed after 30 attempts" >&2
exit 1
fi
# Schema migrations run inside the backend container's entrypoint (alembic
# upgrade head) before uvicorn starts, so there's no separate migrate step or
# service restart here — compose owns the process model. frontend has no
# migrations (stateless). --remove-orphans matters whenever the compose
# service topology itself changes (e.g. the old single `app` service ->
# `backend`+`frontend`, repo-split Stage 4): compose only manages containers
# for services CURRENTLY defined in the file, so a plain `up -d` leaves a
# renamed-away service's old container running and squatting its port
# forever -- confirmed live, this is exactly what blocked beta's first
# dual-service deploy ("port is already allocated").
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
echo "==> Starting stack"
docker compose up -d --remove-orphans
echo "==> Health check"
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
url="http://127.0.0.1:${HEALTH_PORT}/"
for i in $(seq 1 30); do
if curl -fsS -o /dev/null "$url"; then
echo "==> OK: $url is serving"
warm_city_archives
ping_indexnow
exit 0
fi
sleep 1
done
echo "!! Health check failed for $url" >&2
Containerize the app and move the databases to PostgreSQL 18 (#220) Run Thermograph as a docker-compose stack (app + Postgres 18) and standardize the data layer on Postgres, while keeping the test suite on SQLite. - accounts/db.py: DSN-driven engines. On Postgres, a per-worker read-write + read-only asyncpg pair (the RO engine pins read-only transactions, used by the pure-GET endpoints) plus a sync psycopg engine for the notifier thread; the SQLite path is preserved for tests/local (selected when THERMOGRAPH_DATABASE_URL is unset). models.py: boolean server_default -> sa.false(). - store.py / metrics.py: dialect-flexible — Postgres UNLOGGED tables via psycopg when configured, else the existing raw-sqlite3 paths byte-for-byte; sync interfaces and every fail-soft contract preserved. - Alembic (backend/alembic/) manages the accounts schema; the container entrypoint runs `alembic upgrade head` before uvicorn (4 workers). migrate_accounts_to_pg.py copies the accounts data SQLite->PG through the ORM (UUID/bool/JSON coerced), skips access_token, and resets identity sequences. - Dockerfile + docker-compose.yml: app image (uvicorn, 4 workers, loopback 8137) and a Postgres 18 db (2 CPUs) running pg_duckdb (deploy/db/) so the parquet climate cache is queryable in-DB via read_parquet('/parquet/cache/*.parquet'). - deploy.sh/thermograph.service rewired to manage the compose stack; env example, Makefile targets (up/down/db-up), and deploy/POSTGRES-MIGRATION.md cutover runbook. Tests stay on SQLite (dialect fallback) — 323 pass. The full Postgres stack was verified via docker compose: alembic migrations, register/login, the RO endpoint, store/metrics round-trips, and the accounts data migration.
2026-07-20 06:28:23 +00:00
docker compose ps || true
docker compose logs --tail=50 backend || true
docker compose logs --tail=50 frontend || true
exit 1