Operations
Runbook 04 — Platform CMS deployment (Payload)
How
apps/cms(Payload CMS) is deployed, how the platform database is wired, and the real gotchas hit along the way. Update this whenever the pipeline changes.
Architecture
push to Forgejo (main) ──► Coolify clones (HTTPS + token) ──► Nixpacks build (pnpm)
│
▼
Docker container ◄── Traefik (HTTPS, Let's Encrypt) ── cms.soconnective.com
│
▼
Platform PostgreSQL (Coolify-managed, private)
├─ schema `payload` → Payload's tables (auth, admin, collections)
└─ schema `public` → domain tables (organizations/leads/audit_log) with RLS
Components
| Item | Value |
|---|---|
| App | apps/cms — Payload 3.85 + Next.js 15.4 |
| Coolify app | base_directory /apps/cms, build pack nixpacks, port 3000 |
| Domain | https://cms.soconnective.com (Let's Encrypt) |
| Start command | pnpm payload migrate && pnpm start (migrations run on every deploy) |
| Database | Coolify-managed PostgreSQL, private (not publicly exposed) |
| Env (runtime) | DATABASE_URI (internal URL), PAYLOAD_SECRET |
| Schema isolation | Payload uses schemaName: 'payload'; domain stays in public |
Secrets (platform DB credentials,
PAYLOAD_SECRET, app uuid) live in the password manager /fs-secrets, never in git.
First-time provisioning
- Create the platform Postgres (Coolify) — private.
- Create the
payloadschema and the domain schema:CREATE SCHEMA IF NOT EXISTS payload;- Apply
packages/dbSQL migrations + provision theapp_userrole (see ADR 0003).
- Generate and run Payload migrations:
pnpm payload migrate:create initthenpnpm payload migrate(inside the container, against the platform DB). - Seed the first admin (Payload local API) and change the password immediately.
Gotchas (real problems and fixes)
- Coolify mangled the HTTPS git URL to SSH. Creating the app via the API stored
git_repositoryasfeedback/soconnective.gitand the build failed withgit ls-remote ... over ssh. Fix: PATCHgit_repositoryto the fullhttps://feedback:<TOKEN>@git.soconnective.com/feedback/soconnective.git. push: truedoes not run in production. Payload only pushes the schema in dev. Fix: use migrations in production (payload migratein the start command).- TLS /
ERR_CERT_AUTHORITY_INVALID+ HSTS. An old Plesk server (216.250.126.64) still answered forcmswith an expired self-signed certificate. Authoritative DNS (IONOS) already pointscms→ VPS (216.250.119.216); the error was stale DNS cache reaching the old server. Fix: flush DNS / lower TTL / retire the old Plesk server. After DNS pointed correctly, restarting the app made Traefik issue the Let's Encrypt certificate.