Step 7 — A database¶
About 30 minutes. Branch: step-7.
The one new idea: you declare a need, and the platform provisions it.
Two lines¶
That is your entire side of the arrangement. On the next deploy the platform creates a Postgres database, creates a user scoped to it, and injects into every container of this app:
You never choose a password, never see one, and never put one in the repository.
The connection budget¶
The database is shared. The platform adds up what every app has declared and refuses a deploy that would overcommit it.
That makes this number load-bearing in an unusual way: it is not a limit imposed on you, it is your declaration of intent, and it is what stops one app's bad afternoon from becoming everyone's. Declare what you will actually hold.
Migrations run in the start command¶
There is no "run this once before the deploy" hook. Your container has to be able
to bring itself up from nothing — so backend/entrypoint.sh waits for the
database, migrates, and then becomes the server:
echo "waiting for the database…"
# … bounded retry …
echo "running migrations…"
python -m app.migrate
echo "starting uvicorn…"
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
Three properties make that safe, and all three are worth copying:
- Bounded wait. The database may still be coming up. Retry a limited number of times, then fail loudly. Never loop forever — a container that hangs looks identical to one that is slow.
- Idempotent. Every replica runs this, and every redeploy runs it again.
app/migrate.pyrecords what it has applied and takes an advisory lock, so two replicas starting together take turns instead of colliding. execat the end. It replaces the shell with uvicorn, so signals reach the server directly and the platform can stop it cleanly.
Deploy and look¶
Now --type container has something in it — your app's own output:
Redeploy and watch it say 0 applied. That is idempotency, demonstrated.
mecca debug¶
One dump: configured variables, recent errors, per-service health, resolved URLs, and — the useful part — which environment variables the container actually received. When something you configured is not happening, this is where an absence becomes visible.
If it didn't work¶
| Symptom | Try | Usually |
|---|---|---|
DATABASE_URL is not set |
mecca debug tally |
database.enabled missing or misspelled — see step 4 |
| Deploy fails after 60s | mecca logs tally --type container |
The wait loop gave up; read what it printed |
| A migration failed | same | It ran and the SQL is wrong. The transaction rolled back; fix and redeploy |
| Deploy refused before it started | mecca queue diagnose <id> |
The connection budget. Lower max_connections |