PostgreSQL Setup
PostgreSQL Setup
Migration in progress.
api.legendary-arena.comand its PostgreSQL are moving off Render onto a self-hosted DigitalOcean Ubuntu droplet fronted by Cloudflare, perdocs/PLAN.md. Until decommission, Render stays warm as the rollback target.
Summary
Provisions the self-hosted PostgreSQL 18 instance that backs the app. The database runs on localhost co-located with the server, so there is no cross-provider hop between app and data. Tuning targets the 8 GB droplet.
Mechanics
The owning artifact infra/scripts/30-postgres.ps1
requires root and requires LA_DB_PASSWORD (it throws if unset). It runs idempotently in five stages:
PGDG repo — if the
postgresql-<major>package isn’t already available, it adds the PostgreSQL PGDG apt repo: installs theACCC4CF8signing key and writes/etc/apt/sources.list.d/pgdg.listfor the detected Ubuntu codename.Install PG18 — installs
postgresql-18pluspostgresql-client-18and enables the service.Configure — edits
/etc/postgresql/18/main/postgresql.conf, setting localhost-only listen, the port, scram encryption, and baseline tuning for an 8 GB box:listen_addresses = 'localhost' port = 5432 password_encryption = 'scram-sha-256' shared_buffers = 2GB effective_cache_size = 6GB maintenance_work_mem = 512MB wal_buffers = 16MB max_wal_size = 4GB min_wal_size = 1GBpg_hba scram — ensures
pg_hba.confhasscram-sha-256host lines for127.0.0.1/32and::1/128, then restarts postgres.Create role + db — creates or updates role
la(LOGIN+ password) and databaselaOWNER lausingIF NOT EXISTSguards, then prints theDATABASE_URLhint.
Environment overrides: PG_MAJOR (18), LA_DB_NAME (la), LA_DB_USER (la), LA_DB_PASSWORD (required), LA_DB_PORT (5432). Co-location matches the plan’s “server + DB co-located, no cross-provider hop” (docs/PLAN.md
).
Interactions
- Deploy Server
— consumes the resulting
DATABASE_URLand runs the app’s migrations against this database. - Secrets and Env
—
DATABASE_URL(postgres://la:...@localhost:5432/la) is one of the values in the env contract, stored in/etc/la/.env. - Render-to-DigitalOcean Migration — self-hosting the DB is a core migration goal; backups and restore are handled by separate planned scripts (50/51), not yet present.
Edge Cases
LA_DB_PASSWORDis required — the script throws if it is unset, so no role is ever created with an empty or default password.listen_addresses = 'localhost'means NO remote DB connections are accepted; this is by design because the app is co-located on the same box.- The 8 GB tuning block assumes the 8 GB droplet — revisit
shared_buffers/effective_cache_sizeif you step down to a 4 GB box perdocs/PLAN.md. - During the Phase 3 dress rehearsal you restore a COPY of prod and never point the build at the live prod DB (
docs/PLAN.md). scram-sha-256requires clients that support it; older drivers negotiatingmd5will fail to authenticate.
Execute
# Provision PostgreSQL 18 (root required; password is mandatory)
sudo LA_DB_PASSWORD='change-me' pwsh -File infra/scripts/30-postgres.ps1
# With non-default name / user / port
sudo LA_DB_NAME='la' LA_DB_USER='la' LA_DB_PORT='5432' \
LA_DB_PASSWORD='change-me' pwsh -File infra/scripts/30-postgres.ps1
Verify
# Role la exists
sudo -u postgres psql -c '\du'
# Expected: a row listing role "la" with the Login attribute
# Server is accepting connections on localhost
pg_isready -h localhost
# Expected: localhost:5432 - accepting connections
# Database la exists
sudo -u postgres psql -l | grep la
# Expected: a line for database "la" owned by la
References
infra/scripts/30-postgres.ps1— owning provisioning script.docs/PLAN.md— migration plan, co-location and 8 GB box facts, Phase 3 dress-rehearsal rule.content/data/postgres-setup.md— source operator page.- Deploy Server
— downstream consumer of
DATABASE_URL. - Secrets and Env
— where
DATABASE_URLlives in the env contract.