diff --git a/.forgejo/workflows/backend-deploy-dev.yml b/.forgejo/workflows/backend-deploy-dev.yml index 88227f1..94b247e 100644 --- a/.forgejo/workflows/backend-deploy-dev.yml +++ b/.forgejo/workflows/backend-deploy-dev.yml @@ -70,4 +70,10 @@ jobs: # thermograph-lan is host-native (the runner job runs directly on the # LAN box, not over SSH like beta/prod), so plain env vars on the # command reach deploy-dev.sh directly -- no ssh-action needed here. + # The lake creds ride the Actions S3 secrets: dev renders no vault + # (deploy-dev.sh's no-op secrets path), and compose interpolates + # ${THERMOGRAPH_LAKE_S3_*} straight from the deploy environment. + env: + THERMOGRAPH_LAKE_S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + THERMOGRAPH_LAKE_S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} run: SERVICE=backend BACKEND_IMAGE_TAG=${{ steps.tag.outputs.tag }} bash "$HOME/thermograph-dev/infra/deploy/deploy-dev.sh" diff --git a/infra/deploy/deploy.sh b/infra/deploy/deploy.sh index b4a56ce..b9eda1e 100755 --- a/infra/deploy/deploy.sh +++ b/infra/deploy/deploy.sh @@ -177,11 +177,12 @@ esac export BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG:-local}" export FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG:-local}" -# Which compose services this run pulls/rolls. +# Which compose services this run pulls/rolls. lake runs the backend image +# with a different role, so it rolls with every backend deploy. case "$SERVICE" in - backend) TARGETS=(backend) ;; + backend) TARGETS=(backend lake) ;; frontend) TARGETS=(frontend) ;; - all) TARGETS=(backend frontend) ;; + all) TARGETS=(backend lake frontend) ;; esac # Login only when a token is supplied. The SSH/CI deploy paths (deploy.yml, diff --git a/infra/deploy/secrets/beta.yaml b/infra/deploy/secrets/beta.yaml index d7a74c3..5162a7c 100644 --- a/infra/deploy/secrets/beta.yaml +++ b/infra/deploy/secrets/beta.yaml @@ -16,6 +16,8 @@ THERMOGRAPH_VAPID_PRIVATE_KEY: ENC[AES256_GCM,data:sTtkAbMVUtwvRssCZJa2PWUjlfnYS THERMOGRAPH_VAPID_PUBLIC_KEY: ENC[AES256_GCM,data:SnFw+pwNgZlxMEhMUH/1daMZlRB2QFNJHtL+4C3S1eFcJJdT2uNGkY7Ym9FPfx1V7cwt/3urHg1oVgdDzYaIal2wye4l5HhKBP5PjtQkDW+Pck24kxul,iv:Ku4MhsrAfyA2Y3CqhGIsYKrjhKQ4cqAYjCqjFeEvBWE=,tag:hw42iS+79D7gNf+BPTYM0g==,type:str] THERMOGRAPH_VAPID_CONTACT: ENC[AES256_GCM,data:ox+hHiFMTPZqYwICDSwPwcT13ZDSKiGcAVHfVVMNYQ==,iv:DssV/olPXERpdmXNMmzjVYvhn/0zmuSp2XlTs42pIi0=,tag:4YHXI1BFbGZ6JsdXqJuf3w==,type:str] REGISTRY_TOKEN: ENC[AES256_GCM,data:pC1Zzs56la8qjzWcbkuKyeVHwS5WfIe+mrLm5Sdi/LKYziAvRU5cOA==,iv:57PlJEBdgwNJsfFdNOGqAnKk8l6zpmMSWYFEKS6pn+4=,tag:pPftNHUVU6+CY2nWMaXO6w==,type:str] +THERMOGRAPH_LAKE_S3_ACCESS_KEY: ENC[AES256_GCM,data:ZEQ278P7azMKlYMjUraP6hIqnjymqL90zEbvTNC9iXU=,iv:5nXXZwx7RfqwCMqx5LWtxQy2nQcq+6Yr79ulBi6BOnE=,tag:jXcCCCcz4NsDoIZwa/ji4A==,type:str] +THERMOGRAPH_LAKE_S3_SECRET_KEY: ENC[AES256_GCM,data:p5qCrHWoH1i3smDZZBIO8zMSWWxDARYsTY12AA0E8ao=,iv:PVOEQ0kBFeYaE/FT2UswtSTY7S84U/YOPSbcsXR51f4=,tag:JFwaOwAYInPs199li66U/g==,type:str] sops: age: - enc: | @@ -27,7 +29,7 @@ sops: 0+PQdtDxom72BqrAoj5lhepxW8YWPqQIRXsVS/XYQEQfPJuuuiRMKw== -----END AGE ENCRYPTED FILE----- recipient: age1xx4dzs0dxlwvkv9sjuqzsphl7lfrxannkfken374yu2qvvcte9sqzktqt2 - lastmodified: "2026-07-22T03:17:28Z" - mac: ENC[AES256_GCM,data:npF+ngF4Khj3NJ2PNFcQjQYIrZVpdPM0drk43pAI6HfFaynX8JOqRFZRsH0sCfMoPkXB/ffCJxla1Tt3Mu3zgKTNSWcn+mjzl+dYvlruY1VylHLS+GnTO9zKeQcwbrYzTfZNWzQhm6/mw8KCL3791U/xNHwJc6g5VxRUWl6TBdo=,iv:JLzY8rm33NLG8CqyAlU6vBhlODUiRipKu9daoRvgyzg=,tag:yIsxVZAnw8FD7Bt/x55wMQ==,type:str] + lastmodified: "2026-07-23T22:30:10Z" + mac: ENC[AES256_GCM,data:JWfkQk4Z1ohPha9LID/hvZOycmDy0sUyjiqDdCidfCQEsPx+OwhPatcM31x1ZKFgh3TyYTxrauoSTlIeuwfy7+R9jATDdBBZe9zMqgs6Szu9WUdFiaZzIFpG7SEVw+8IqIyAuqZVSW3k5yHmchcmkNyDekV+1RIF8wxUrOeSCVQ=,iv:6PXUiCnYBiFyNPnCRAs6f7a+RuvWI4jSIMXwuMn7WjA=,tag:y+Ix2m65hQRkSt8FylfKSg==,type:str] unencrypted_suffix: _unencrypted version: 3.13.2 diff --git a/infra/deploy/stack/deploy-stack.sh b/infra/deploy/stack/deploy-stack.sh index 8833aec..e86a125 100755 --- a/infra/deploy/stack/deploy-stack.sh +++ b/infra/deploy/stack/deploy-stack.sh @@ -157,9 +157,16 @@ if [ "$SERVICE" = "all" ] || ! docker service inspect "${STACK_NAME}_web" >/dev/ else case "$SERVICE" in backend) - echo "==> Rolling web + worker to $BACKEND_IMAGE" + echo "==> Rolling web + worker + lake to $BACKEND_IMAGE" docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_web" docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_worker" + # lake ships in the same image; a stack file predating it has no service + # yet — the next SERVICE=all stack deploy creates it, so don't fail here. + if docker service inspect "${STACK_NAME}_lake" >/dev/null 2>&1; then + docker service update --with-registry-auth --detach=false --image "$BACKEND_IMAGE" "${STACK_NAME}_lake" + else + echo " (no ${STACK_NAME}_lake service yet; created on the next full stack deploy)" + fi ;; frontend) echo "==> Rolling frontend to $FRONTEND_IMAGE" diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index c2107fd..b97e0e8 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -40,6 +40,9 @@ services: db: cpus: !reset null deploy: !reset null + lake: + cpus: !reset null + deploy: !reset null # Uncapped memory on dev too (prod ceilings it at 8g). The Postgres/DuckDB # memory *budget* is still the ~8 GB derived by deploy/db/init/20-tuning.sh from # the default DB_MEMORY (raise DB_MEMORY to give dev more); shm_size stays (it's diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 4de4659..7eb20a8 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -124,6 +124,11 @@ services: # (NOT /app/data) so it never shadows the Python `data/` package -- see the # volumes: note below and thermograph-backend paths.py. THERMOGRAPH_SINGLETON_LOCK: /state/notifier.lock + # History reads ask the lake service before any third-party source; if + # the lake has no bucket creds (LAN without them exported) it answers + # 503 and climate.py falls straight through to NASA — an accelerator, + # never a dependency. + THERMOGRAPH_LAKE_URL: http://lake:8141 # Prod secrets live in /etc/thermograph.env: POSTGRES_PASSWORD, # THERMOGRAPH_AUTH_SECRET, THERMOGRAPH_VAPID_PRIVATE_KEY/_PUBLIC_KEY, # THERMOGRAPH_COOKIE_SECURE=1, mail/Discord keys, ... (see @@ -157,6 +162,36 @@ services: - "127.0.0.1:8137:8137" restart: unless-stopped + # The ERA5 lake service: partition-pruned reads over the era5-thermograph + # bucket with a local parquet cache, same backend image with + # THERMOGRAPH_ROLE=lake (the entrypoint starts lake_app — no database, no + # migrations, so no depends_on: db). Bucket creds come from + # /etc/thermograph.env where the vault renders them (beta/prod) or from the + # deploy environment (LAN — deploy-dev.yml injects the Actions S3 secrets); + # without them the service stays healthy and every read falls through to + # NASA. Never published on a host port: only the backend talks to it. + lake: + image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local} + environment: + THERMOGRAPH_ROLE: lake + PORT: 8141 + THERMOGRAPH_SERVICE_ROLE: backend + WORKERS: "1" + THERMOGRAPH_LAKE_CACHE: /state/lake-cache + THERMOGRAPH_LAKE_S3_ACCESS_KEY: ${THERMOGRAPH_LAKE_S3_ACCESS_KEY:-} + THERMOGRAPH_LAKE_S3_SECRET_KEY: ${THERMOGRAPH_LAKE_S3_SECRET_KEY:-} + env_file: + - path: /etc/thermograph.env + required: false + volumes: + - lakecache:/state + cpus: ${LAKE_CPUS:-2} + deploy: + resources: + limits: + cpus: "${LAKE_CPUS:-2}" + restart: unless-stopped + frontend: # Frontend's OWN image, published by thermograph-frontend's build-push.yml # from its own Dockerfile (which starts `uvicorn app:app` directly -- no @@ -187,6 +222,7 @@ volumes: pgdata: {} appdata: {} applogs: {} + lakecache: {} networks: # Pin the default network's subnet + gateway so the host Postfix can rely on a