Migrate an app database to platform CNPG
Канон:
wilix-site/content/one/docs/runbooks/cnpg-migrate-database.md(этот файл).
Cut over a service from external (cloud) PostgreSQL to the in-cluster CNPG cluster. Used for ContextForge, Daytona, and future platform apps sharing {{ release }}-pg.
Related: logical DB provisioning — /one/docs/runbooks/cnpg-provision-db
Production reference
| Item | Value |
|---|---|
| Namespace | wilix-one-platform |
| CNPG cluster | wilix-one-platform-production-pg |
| RW service | wilix-one-platform-production-pg-rw:5432 |
| ContextForge deploy | wilix-one-platform-production-contextforge |
| ContextForge secret | wilix-one-platform-production-contextforge |
| Daytona API deploy | wilix-one-platform-production-daytona-api |
| Daytona CNPG password secret | daytona-cnpg-password |
Grace period (cloud fallback)
After cutover, do not drop the cloud database for 7 days. Keep the old DSN noted in a secure location (password manager / encrypted secret-values history) so you can roll back by patching the app Secret and scaling down/up.
Rollback: scale app to 0 → patch DATABASE_URL back to cloud DSN → scale up → verify.
Checklist
1. Generate app password
Password must not contain single quotes (provision script rejects them):
CF_PASS=$(openssl rand -base64 24 | tr -d "'/")
export CF_PASS
Store in a temporary Kubernetes Secret until secret-values is updated:
kubectl -n wilix-one-platform create secret generic contextforge-cnpg-password \
--from-literal=password="$CF_PASS" \
--dry-run=client -o yaml | kubectl apply -f -
2. Provision logical DB + role
Prefer exec mode when superuser access is disabled:
./scripts/cnpg-provision-db.sh \
--namespace wilix-one-platform \
--cluster wilix-one-platform-production-pg \
--db contextforge \
--role contextforge \
--password-from-env CF_PASS \
--mode exec
3. Scale app to zero
Record previous replica count:
kubectl -n wilix-one-platform get deployment wilix-one-platform-production-contextforge \
-o jsonpath='{.spec.replicas}'
kubectl -n wilix-one-platform scale deployment wilix-one-platform-production-contextforge --replicas=0
kubectl -n wilix-one-platform rollout status deployment/wilix-one-platform-production-contextforge --timeout=120s
4. Dump cloud database
Read current DSN from the live Secret (do not log the password):
CLOUD_DATABASE_URL=$(kubectl -n wilix-one-platform get secret wilix-one-platform-production-contextforge \
-o jsonpath='{.data.DATABASE_URL}' | base64 -d)
If the scheme is postgresql+psycopg, convert for pg_dump:
PG_DUMP_URL="${CLOUD_DATABASE_URL/postgresql+psycopg/postgres}"
pg_dump --format=custom --no-owner --no-acl "$PG_DUMP_URL" -f /tmp/contextforge.dump
5. Restore into CNPG
Preferred: pipe dump via kubectl exec into the primary (primary pod /tmp may be read-only):
CF_PASS=$(kubectl -n wilix-one-platform get secret contextforge-cnpg-password -o jsonpath='{.data.password}' | base64 -d)
PRIMARY=$(kubectl -n wilix-one-platform get pod \
-l "cnpg.io/cluster=wilix-one-platform-production-pg,role=primary" \
-o jsonpath='{.items[0].metadata.name}')
cat /tmp/contextforge.dump | kubectl -n wilix-one-platform exec -i "$PRIMARY" -c postgres -- \
env PGPASSWORD="$CF_PASS" pg_restore --no-owner --role=contextforge \
-h localhost -U contextforge -d contextforge
Alternative: port-forward (kill when done):
kubectl -n wilix-one-platform port-forward svc/wilix-one-platform-production-pg-rw 5433:5432 &
PGPASSWORD="$CF_PASS" pg_restore --no-owner --role=contextforge \
-h 127.0.0.1 -p 5433 -U contextforge -d contextforge /tmp/contextforge.dump
If restore fails on missing extensions, create them as superuser (--mode exec or brief enableSuperuserAccess) then re-run restore. Some pg_restore warnings are OK.
Verify:
kubectl -n wilix-one-platform exec "$PRIMARY" -c postgres -- env PGPASSWORD="$CF_PASS" \
psql -h localhost -U contextforge -d contextforge -c '\dt'
6. Point app at CNPG
Match the DSN scheme the app already uses (ContextForge: postgresql+psycopg):
postgresql+psycopg://contextforge:<CF_PASS>@wilix-one-platform-production-pg-rw:5432/contextforge
Live Secret (immediate cutover):
NEW_URL="postgresql+psycopg://contextforge:${CF_PASS}@wilix-one-platform-production-pg-rw:5432/contextforge"
kubectl -n wilix-one-platform patch secret wilix-one-platform-production-contextforge \
--type merge -p "{\"data\":{\"DATABASE_URL\":\"$(echo -n "$NEW_URL" | base64)\"}}"
Git / werf: encrypt into .helm/secret-values-production.yaml:
contextforge:
envSecret:
DATABASE_URL: <werf-encrypted>
echo -n "$NEW_URL" | werf helm secret encrypt # requires WERF_SECRET_KEY
If WERF_SECRET_KEY is unavailable, live Secret cutover is still valid; update secret-values on the next deploy-capable workstation and remove contextforge-cnpg-password after.
7. Scale up and verify
kubectl -n wilix-one-platform scale deployment wilix-one-platform-production-contextforge --replicas=1
kubectl -n wilix-one-platform rollout status deployment/wilix-one-platform-production-contextforge --timeout=180s
kubectl -n wilix-one-platform get pods | grep contextforge
kubectl -n wilix-one-platform logs deployment/wilix-one-platform-production-contextforge --tail=50
Success indicators:
- Pod
1/1 Running - No
authentication failed/could not connectin logs - Server starts (
Server is ready, database pool warnings are OK) - Bootstrap role errors ("Multiple rows were found") are expected when data was restored from cloud
8. Cleanup (after secret-values encrypted)
kubectl -n wilix-one-platform delete secret contextforge-cnpg-password
rm -f /tmp/contextforge.dump
ContextForge migration notes (2026-07-27)
- Logical DB/role:
contextforgeonwilix-one-platform-production-pg - Cloud source DB:
wilixone_contextforge(Selectel DBaaS) — retained, not dropped - Restore: 71 public tables via stdin
pg_restoreinto primary - Live Secret patched to in-cluster DSN;
.helm/secret-values-production.yamlstill holds encrypted cloud URL untilWERF_SECRET_KEYre-encrypt - Temporary password store: Secret
contextforge-cnpg-passwordin namespacewilix-one-platform
Daytona migration notes (2026-07-27)
- Logical DB/role:
daytonaonwilix-one-platform-production-pg - Source: Bitnami subchart
wilix-one-platform-production-postgresql(useruser, databasedaytona) - Restore: 28 public tables via stdin
pg_restoreinto primary - Live cutover: patch
wilix-one-platform-production-daytona-apienvDB_HOST→…-pg-rw,DB_USERNAME→daytona,DB_PASSWORD→ Secretdaytona-cnpg-password - Helm:
daytona.postgresql.enabled: false,daytona.externalDatabase.*in values-production;existingSecret: daytona-cnpg-password(keyspassword,database-password) - Bitnami StatefulSet/PVC/Secret removed after API verified on CNPG
- Password: Kubernetes Secret
daytona-cnpg-passworduntildaytona.externalDatabase.passwordencrypted in secret-values (WERF_SECRET_KEYunavailable in worktree)
Daytona (Task 6)
Same flow with --db daytona --role daytona, deploy wilix-one-platform-production-daytona-api, and daytona.externalDatabase in values-production (see notes above).
Open follow-ups
- Encrypt DSNs/passwords — when
WERF_SECRET_KEYis available, re-encrypt ContextForgeDATABASE_URLand Daytonadaytona.externalDatabase.passwordinto.helm/secret-values-production.yaml(replace live-only / cloud ciphertext). - Remove temporary Secrets — after secret-values are updated and a deploy succeeds: delete
contextforge-cnpg-passwordanddaytona-cnpg-passwordfrom namespacewilix-one-platform. - Decommission cloud ContextForge DB — after 7-day grace period with no rollback, drop or archive Selectel
wilixone_contextforge(cloud DSN no longer in use).