Backend deploy
Phase-7 deploy path. Pre-conditions, action checklist, rollback. Updated 2026-05-11.
What this deploys
Section titled “What this deploys”A Python FastAPI service on Scaleway Serverless Container at
https://api.erold.dev, backed by Managed RDB Postgres 17 (private
endpoint) and Object Storage. Production cost ≈ €43/month (RDB
db-gp-xs €25 + Container min_scale=1 €14 + buckets €2 + SM €1 +
registry €1). See infra/COSTS.md.
Pre-conditions
Section titled “Pre-conditions”| # | Check | Command |
|---|---|---|
| 1 | scw authenticated as account owner (IAM mutations) | scw config show |
| 2 | tea authenticated (Gitea CI secrets) | tea login list |
| 3 | secret CLI on PATH | command -v secret |
| 4 | Dev project + dev RDB already up (Phase 1-6) | cat infra/.project-id && scw rdb instance list region=fr-par -o json | jq -r '.[] | select(.tags[]?=="project=erold") | .name' |
| 5 | pgvector verified on dev RDB | already done (Phase 1) |
| 6 | All integration tests green | EROLD_RUN_WORKER_TESTS=1 pytest tests/integration/ |
| 7 | Docker image builds + smoke tests local container | covered by §“Local image smoke test” below |
| 8 | Gitea repo erold/erold-backend exists | tea repos list | grep erold-backend — if missing, create: tea repos create --name erold-backend |
| 9 | DNS for api.erold.dev is owned + you can add a CNAME | check Squarespace/Gandi/your registrar |
Step-by-step
Section titled “Step-by-step”1. Provision prod RDB (~€25/month, recurring)
Section titled “1. Provision prod RDB (~€25/month, recurring)”cd erold-backend/infraEROLD_PROJECT_ID="$(cat .project-id)" bash scripts/03-rdb.sh# (no EROLD_SKIP_PROD_RDB flag this time — provisions erold-db-prod)Wait until status=ready. Verifies private endpoint, writes
erold.prod.database-url to Keychain.
2. Operator credentials (one-time, no cost)
Section titled “2. Operator credentials (one-time, no cost)”openssl rand -hex 64 | secret set erold.prod.jwt-signing-keyecho 'sk-YOUR_REAL_OPENAI_KEY' | secret set erold.prod.openai-api-key3. Provision Object Storage + Secret Manager (~€3/month)
Section titled “3. Provision Object Storage + Secret Manager (~€3/month)”SCW_DEFAULT_PROJECT_ID="$(cat .project-id)" \ EROLD_PROJECT_ID="$(cat .project-id)" \ bash scripts/05-storage.shEROLD_PROJECT_ID="$(cat .project-id)" bash scripts/06-secret-manager.sh4. Mirror CI keys to Gitea
Section titled “4. Mirror CI keys to Gitea”secret get erold.ci.access-key | tea repos secrets create --repo erold/erold-backend --name SCW_CI_ACCESS_KEYsecret get erold.ci.secret-key | tea repos secrets create --repo erold/erold-backend --name SCW_CI_SECRET_KEYcat infra/.project-id | tea repos secrets create --repo erold/erold-backend --name SCW_PROJECT_ID5. Provision Container Registry + Serverless Container (~€15/month, recurring)
Section titled “5. Provision Container Registry + Serverless Container (~€15/month, recurring)”EROLD_PROJECT_ID="$(cat .project-id)" bash scripts/07-registry.shEROLD_PROJECT_ID="$(cat .project-id)" bash scripts/08-container.shContainer starts with a Scaleway hello-world placeholder image. The
first git tag v* push replaces it.
6. DNS
Section titled “6. DNS”Get the prod Serverless Container hostname:
scw container container get "$(cat infra/.container-prod-id)" region=fr-par -o json | jq -r '.domain_name'Add a CNAME at your DNS host: api.erold.dev CNAME <that-hostname>.
TTL 300. Scaleway issues a Let’s Encrypt cert automatically once the
CNAME resolves.
7. Push the repo to Gitea + tag a release
Section titled “7. Push the repo to Gitea + tag a release”cd erold-backendgit init -b main && git add . && git commit -m "Initial Phase 7 deploy"git remote add origin git@<gitea-host>:erold/erold-backend.gitgit push -u origin main # triggers staging.ymlgit tag v1.0.0 && git push --tags # triggers deploy.yml → prod8. Verify
Section titled “8. Verify”After the deploy workflow completes (≈ 5 min):
curl -fs https://api.erold.dev/health | jq '.'# {"status":"ok","sha":"v1.0.0"}curl -fs https://api.erold.dev/health -w '\n%{http_code}\n'# 200If sha does not match the tag, redeploy did not complete — inspect
scw container container get $(cat infra/.container-prod-id) region=fr-par.
Local image smoke test (every PR, before merge)
Section titled “Local image smoke test (every PR, before merge)”docker build --build-arg GIT_SHA="$(git rev-parse --short HEAD)" -t erold-api:local .# Temporarily open dev RDB:scw rdb endpoint create "$(cat infra/.rdb-dev-id)" load-balancer=true region=fr-par -wMY_IP="$(curl -s https://api.ipify.org)/32"scw rdb acl add "${MY_IP}" instance-id="$(cat infra/.rdb-dev-id)" region=fr-par description="local-smoke" -w
LB="$(scw rdb instance get "$(cat infra/.rdb-dev-id)" region=fr-par -o json | jq -r '.endpoints[]|select(.load_balancer!=null)|"\(.ip):\(.port)"')"PW="$(secret get erold.dev.database-url | sed -E 's#postgresql://[^:]+:([^@]+)@.*#\1#')"
docker run --rm -d --name erold-smoke -p 18000:8000 \ -e DATABASE_URL="postgresql://erold_app:${PW}@${LB}/rdb?sslmode=require" \ -e JWT_SIGNING_KEY="local-stub" \ -e OPENAI_API_KEY="sk-stub-for-tests" \ -e APP_ENV=dev -e PORT=8000 \ erold-api:local
# Wait & verifysleep 8 && curl -fs http://127.0.0.1:18000/healthdocker rm -f erold-smoke
# Always close the temporary opening:scw rdb endpoint list "$(cat infra/.rdb-dev-id)" region=fr-par -o json \ | jq -r '.[]|select(.load_balancer!=null)|.id' \ | xargs -I {} scw rdb endpoint delete {} instance-id="$(cat infra/.rdb-dev-id)" region=fr-parscw rdb acl delete "${MY_IP}" instance-id="$(cat infra/.rdb-dev-id)" region=fr-parRollback
Section titled “Rollback”Manual rollback path — no auto-rollback by design (rollback bugs are worse than the deploy bug 80% of the time; humans need to choose).
# 1. Find the previous good image tagscw container container get "$(cat infra/.container-prod-id)" region=fr-par -o json | jq -r '.registry_image'# rg.fr-par.scw.cloud/erold/api:v1.0.1 (current bad)
# 2. Update to the previous tagscw container container update "$(cat infra/.container-prod-id)" \ region=fr-par \ registry-image=rg.fr-par.scw.cloud/erold/api:v1.0.0 \ -w
# 3. Verifycurl -fs https://api.erold.dev/health | jq '.sha'Phase-7 exit criteria
Section titled “Phase-7 exit criteria”-
git tag v1.0.0 && git push --tagsdeploys prod with zero manual steps -
infra/VERIFY.mdchecklist (13 commands) all green against prod -
curl https://api.erold.dev/healthreturns 200 with the tagged SHA - Cross-tenant integration test passes against prod
- Plugin pointing at
https://api.erold.dev/api/v1ingests events from a test session and they appear in fragment search within 3 s
Open items / known limitations
Section titled “Open items / known limitations”- Plugin outbox daemon currently POSTs to legacy
/tenants/{t}/log— migrate to/v1/events/batchto use the Phase-4 dedup_hash window. Task #25 in the project tracker. - 227 pyright type-annotation cleanup + 154 ruff stylistic — not blocking deploy but should land before v1.1. Task #19.
EROLD_RUN_REAL_EMBEDDER_TESTSis intentionally gated — runs a real OpenAI lag benchmark. Wire into a nightly Gitea job once a budget cap is set.