CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Deployment & Operations

Moving From One Server to Kubernetes

Move a CWA site from one server with Docker Compose to Kubernetes with the template's pipeline - the media into a Cloud Storage bucket, the database from a final dump taken under maintenance mode, the secrets from .env.prod as CI variables, and the certificates from the staging issuer to letsencrypt-prod.

A site that started on one server can move to Kubernetes with the template's pipeline. The same project deploys both ways, so nothing in the code changes. What moves is the data and the settings:

On one serverOn Kubernetes
The database in the database containerAn external PostgreSQL database (DATABASE_URL)
Media in the media_data volumeA Cloud Storage bucket (GCLOUD_BUCKET), under GCLOUD_PATH_PREFIX
Secrets and hosts in .env.prodCI variables
Caddy's own Let's Encrypt certificatescert-manager, through the ingress
These steps haven't been run on a real site yet. They're put together from the template's scripts and pipeline, like the single-server scripts themselves (a preview). Try the move on staging first, as below, and keep the old server until the new site has run for a few days.

The move has two parts. Prepare the cluster while the old server still serves the site. Then, at a quiet time, close the old site, copy the last of its data and point DNS at the cluster.

Prepare the cluster

Follow Deploying a CWA Site up to, but not including, the production deploy: prepare the cluster, create the namespaces and set the CI variables. Two of them come from the server:

  • The database: an external PostgreSQL database, with DATABASE_URL and CWA_CI_POSTGRES_ENABLED="false" (see CI/CD Variables › Database). It must be PostgreSQL, like the server's.
  • The media bucket: GCLOUD_JSON, GCLOUD_BUCKET, and GCLOUD_PATH_PREFIX if other apps share the bucket (see CI/CD Variables › Media storage). A Kubernetes deploy always stores media in the bucket.

Deploy the same commit the server runs, so both have the same migrations.

The secrets from .env.prod

Copy the values from the server's .env.prod into CI variables, so the new site keeps them:

.env.prodCI variableWhy keep it
JWT_SECRET_KEY, JWT_PASSPHRASEThe same namesUsers stay signed in. A new key pair signs everyone out
APP_SECRETAPP_SECRETWhat Symfony signed with it, such as signed URLs, stays valid
CADDY_MERCURE_JWT_SECRETMERCURE_JWT_SECRETThe real-time cookies browsers hold stay valid
NUXT_APP_SECRETNUXT_APP_SECRETOG image URLs already shared keep their signature
ADMIN_PASSWORDADMIN_PASSWORDThe next fixture load would reset the admin to it
MAILER_DSN, and MAILER_EMAIL if you set itThe same namesThe site's mail relay
SITE_HOSTKUBE_INGRESS_BASE_DOMAINProduction is served at it
SITE_ALIASESKUBE_INGRESS_ALIAS_DOMAINSComma-separated here, not space-separated

Leave JWT_PUBLIC_KEY out: the deploy derives it from the key and its passphrase, and fails if they don't belong together. Copy the key without the double quotes .env.prod puts around it. POSTGRES_PASSWORD isn't needed: the external database has its own.

For each secret that isn't a CI variable, the deploy keeps the release's live value, or generates a new one for a new release. So the first deploy of a site without these variables generates new ones. How to set them, protected and on GitHub as secrets, is in CI/CD Variables › Secrets.

Try it on staging

  1. Point staging.<domain> at the cluster's ingress, and push main. Staging gets a certificate from letsencrypt-staging, which browsers don't trust: that's on purpose (see Deploying a CWA Site › Push main and check staging).
  2. Load a copy of the site's data into the new database, as in Copy the database, from a recent backup. Copy the media too (Copy the media).
  3. Check staging serves the site, with its images, and that you can sign in.
  4. Once staging's certificate was issued, set CWA_CI_CLUSTER_ISSUER=letsencrypt-prod (see Deploying a CWA Site › Switch to trusted certificates). The production deploy then gets a trusted certificate.

On GitLab, staging gets production's variables, so it uses the same external database (see CI/CD › Staging). The final copy below replaces whatever staging wrote there.

Lower the DNS records' TTL a day before the move, for example to 300 seconds, so visitors reach the cluster soon after you change them.

Copy the media

Each backup's <time>-media.tar.gz holds the media_data volume: uploads, and their image variants under cache/. The bucket needs the same files at the same paths, under the site's GCLOUD_PATH_PREFIX. The database stores each file's path, not its URL, so the paths must match.

Unpack an archive and copy it with the Google Cloud CLI. Uploaded originals never change at a path, so they get a year's Cache-Control; variants get 30 days, as the API sets them when it stores a file itself (see Uploadable › Cache-Control on Stored Files):

mkdir media && tar -xzf backups/<time>-media.tar.gz -C media

gcloud storage rsync media gs://<bucket>/<prefix> --recursive \
  --exclude='^cache/' --cache-control='public, max-age=31536000, immutable'
gcloud storage rsync media/cache gs://<bucket>/<prefix>cache --recursive \
  --cache-control='public, max-age=2592000'

Leave <prefix> empty when GCLOUD_PATH_PREFIX is empty, and otherwise write it with its trailing slash, such as site-a/. If the server stored media under a prefix of its own, copy from that folder of the archive.

rsync only copies what changed, so copy once while preparing, and again from the final backup. Don't add --delete-unmatched-destination-objects: it would delete what the new site's staging stored.

Move the site

At a quiet time, ask editors to stop editing, then:

  1. Close the old site. On the server, turn maintenance mode on, so visitors see the maintenance page and nothing changes while you copy:
    docker compose --env-file .env.prod exec php bin/console silverback:api-components:maintenance on
    

    Admins bypass maintenance mode, so make sure nobody signed in keeps editing.
  2. Take a final backup on the server with bin/server/backup.sh, and copy its files off the server.
  3. Copy the media again from the final backup's archive, as in Copy the media.
  4. Copy the database from the final backup's dump, as in Copy the database.
  5. Point DNS at the cluster's ingress: the site's host and every alias. With Cloudflare in front, leave them DNS only until the certificate is issued (see Cloudflare › Each deploy track).
  6. Deploy production (see Deploying a CWA Site › Deploy production). Don't load the fixtures: the admin and the pages came with the database. cert-manager gets the certificate once DNS reaches the ingress. Until then, the ingress serves a temporary one.
  7. Open the site. The dump was taken with maintenance mode on, and the setting is stored in the database, so the new site starts in maintenance mode too. In a ready API pod, flush the cache and turn it off, as restore.sh does on a server:
    kubectl get pods -n <namespace> -l app.kubernetes.io/name=cwa,app.kubernetes.io/instance=<release>
    kubectl exec -n <namespace> <ready pod> -- php bin/console silverback:api-components:purge-http-cache
    kubectl exec -n <namespace> <ready pod> -- php bin/console silverback:api-components:maintenance off
    
  8. Check the site: pages come from the cache, images load from the bucket, and you can sign in with your existing password (see Deploying a CWA Site › Check the site).

Keep the old server running, still in maintenance mode, until DNS has moved everywhere: a visitor whose resolver still has the old address gets the maintenance page rather than an error. Then stop it, and keep its last backup and .env.prod.

Copy the database

Restore the dump into the external database with pg_restore, from anywhere that can reach it:

pg_restore --clean --if-exists --no-owner --dbname='postgresql://<user>:<password>@<host>/<database>' backups/<time>-database.dump

--clean --if-exists drops each table before restoring it, so it replaces a copy you loaded earlier. --no-owner makes the external database's user own everything. Use a pg_restore of the server's PostgreSQL major version or newer (the template's is 16). This is the same command as Migrations and Backups › Restoring a backup.

The first production deploy finds the database already migrated to the commit you deployed, so it has nothing left to migrate.