Skip to content

Backend deploy

Phase-7 deploy path. Pre-conditions, action checklist, rollback. Updated 2026-05-11.

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.

#CheckCommand
1scw authenticated as account owner (IAM mutations)scw config show
2tea authenticated (Gitea CI secrets)tea login list
3secret CLI on PATHcommand -v secret
4Dev 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'
5pgvector verified on dev RDBalready done (Phase 1)
6All integration tests greenEROLD_RUN_WORKER_TESTS=1 pytest tests/integration/
7Docker image builds + smoke tests local containercovered by §“Local image smoke test” below
8Gitea repo erold/erold-backend existstea repos list | grep erold-backend — if missing, create: tea repos create --name erold-backend
9DNS for api.erold.dev is owned + you can add a CNAMEcheck Squarespace/Gandi/your registrar

1. Provision prod RDB (~€25/month, recurring)

Section titled “1. Provision prod RDB (~€25/month, recurring)”
Terminal window
cd erold-backend/infra
EROLD_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)”
Terminal window
openssl rand -hex 64 | secret set erold.prod.jwt-signing-key
echo 'sk-YOUR_REAL_OPENAI_KEY' | secret set erold.prod.openai-api-key

3. Provision Object Storage + Secret Manager (~€3/month)

Section titled “3. Provision Object Storage + Secret Manager (~€3/month)”
Terminal window
SCW_DEFAULT_PROJECT_ID="$(cat .project-id)" \
EROLD_PROJECT_ID="$(cat .project-id)" \
bash scripts/05-storage.sh
EROLD_PROJECT_ID="$(cat .project-id)" bash scripts/06-secret-manager.sh
Terminal window
secret get erold.ci.access-key | tea repos secrets create --repo erold/erold-backend --name SCW_CI_ACCESS_KEY
secret get erold.ci.secret-key | tea repos secrets create --repo erold/erold-backend --name SCW_CI_SECRET_KEY
cat infra/.project-id | tea repos secrets create --repo erold/erold-backend --name SCW_PROJECT_ID

5. Provision Container Registry + Serverless Container (~€15/month, recurring)

Section titled “5. Provision Container Registry + Serverless Container (~€15/month, recurring)”
Terminal window
EROLD_PROJECT_ID="$(cat .project-id)" bash scripts/07-registry.sh
EROLD_PROJECT_ID="$(cat .project-id)" bash scripts/08-container.sh

Container starts with a Scaleway hello-world placeholder image. The first git tag v* push replaces it.

Get the prod Serverless Container hostname:

Terminal window
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.

Terminal window
cd erold-backend
git init -b main && git add . && git commit -m "Initial Phase 7 deploy"
git remote add origin git@<gitea-host>:erold/erold-backend.git
git push -u origin main # triggers staging.yml
git tag v1.0.0 && git push --tags # triggers deploy.yml → prod

After the deploy workflow completes (≈ 5 min):

Terminal window
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'
# 200

If 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)”
Terminal window
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 -w
MY_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 & verify
sleep 8 && curl -fs http://127.0.0.1:18000/health
docker 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-par
scw rdb acl delete "${MY_IP}" instance-id="$(cat infra/.rdb-dev-id)" region=fr-par

Manual rollback path — no auto-rollback by design (rollback bugs are worse than the deploy bug 80% of the time; humans need to choose).

Terminal window
# 1. Find the previous good image tag
scw 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 tag
scw 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. Verify
curl -fs https://api.erold.dev/health | jq '.sha'
  • git tag v1.0.0 && git push --tags deploys prod with zero manual steps
  • infra/VERIFY.md checklist (13 commands) all green against prod
  • curl https://api.erold.dev/health returns 200 with the tagged SHA
  • Cross-tenant integration test passes against prod
  • Plugin pointing at https://api.erold.dev/api/v1 ingests events from a test session and they appear in fragment search within 3 s
  • Plugin outbox daemon currently POSTs to legacy /tenants/{t}/log — migrate to /v1/events/batch to 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_TESTS is intentionally gated — runs a real OpenAI lag benchmark. Wire into a nightly Gitea job once a budget cap is set.