WILIX Logo

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

ItemValue
Namespacewilix-one-platform
CNPG clusterwilix-one-platform-production-pg
RW servicewilix-one-platform-production-pg-rw:5432
ContextForge deploywilix-one-platform-production-contextforge
ContextForge secretwilix-one-platform-production-contextforge
Daytona API deploywilix-one-platform-production-daytona-api
Daytona CNPG password secretdaytona-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 connect in 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: contextforge on wilix-one-platform-production-pg
  • Cloud source DB: wilixone_contextforge (Selectel DBaaS) — retained, not dropped
  • Restore: 71 public tables via stdin pg_restore into primary
  • Live Secret patched to in-cluster DSN; .helm/secret-values-production.yaml still holds encrypted cloud URL until WERF_SECRET_KEY re-encrypt
  • Temporary password store: Secret contextforge-cnpg-password in namespace wilix-one-platform

Daytona migration notes (2026-07-27)

  • Logical DB/role: daytona on wilix-one-platform-production-pg
  • Source: Bitnami subchart wilix-one-platform-production-postgresql (user user, database daytona)
  • Restore: 28 public tables via stdin pg_restore into primary
  • Live cutover: patch wilix-one-platform-production-daytona-api env DB_HOST…-pg-rw, DB_USERNAMEdaytona, DB_PASSWORD → Secret daytona-cnpg-password
  • Helm: daytona.postgresql.enabled: false, daytona.externalDatabase.* in values-production; existingSecret: daytona-cnpg-password (keys password, database-password)
  • Bitnami StatefulSet/PVC/Secret removed after API verified on CNPG
  • Password: Kubernetes Secret daytona-cnpg-password until daytona.externalDatabase.password encrypted in secret-values (WERF_SECRET_KEY unavailable 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_KEY is available, re-encrypt ContextForge DATABASE_URL and Daytona daytona.externalDatabase.password into .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-password and daytona-cnpg-password from namespace wilix-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).
WILIX Logo
© 2025 WILIX · С любовью к пользователям
ООО "ВИЛИКС" ИНН: 2308266448 ОГРН: 1192375046819