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

Deploying a CWA Site

Choose between one server with Docker Compose (a preview) and Kubernetes with Helm, then take a new site to production on Kubernetes step by step, with the template's GitLab or GitHub pipeline.

A project made with create-cwa can run in production in two ways. Pick one first:

One server with Docker ComposeKubernetes with Helm
StatusPreviewSupported
What runs itcompose.prod.yaml on one Linux server, deployed by the template's bin/server/ scriptsThe template's Helm chart, deployed by its GitLab CI or GitHub Actions pipeline
ScalingOne serverThe front end autoscales, and scaled mode runs several API pods
ReadDeploy to One Server, then Docker › Production Docker ComposeThe steps below, then Kubernetes & Helm and CI/CD
One server is a preview. The template's bin/server/ scripts set up, deploy, back up and restore a site on one server, and media is stored on the server, but the scripts haven't run a production site yet. They assume a plain Linux server, such as Debian or Ubuntu, with Docker installed, and no particular hosting provider. See Deploy to One Server, and Moving From One Server to Kubernetes when you outgrow it.

Kubernetes is the supported way to run a site, and the way to scale one. The pipeline builds the images, runs the tests, and deploys to Kubernetes with Helm. The rest of this page takes a new site to production on Kubernetes in order. Each step links to the page that explains it in full.

Set up once
DNS

KUBE_INGRESS_BASE_DOMAIN, staging.… and <branch>-review.… point at the ingress

Your cluster
ingress-nginx

externalTrafficPolicy: Local

cert-manager

letsencrypt-staging
letsencrypt-prod

Production's namespace

production, staging and canary deploy here

A namespace per review branch

optional

Then, in order
  1. 1
    Push main

    Builds the images, runs the tests and deploys staging. On GitHub, set CWA_CI_STAGING_ENABLED to true first

  2. 2
    Check staging

    Its certificate is from letsencrypt-staging, so expect a browser warning

  3. 3
    Switch to trusted certificates

    Set CWA_CI_CLUSTER_ISSUER=letsencrypt-prod

  4. 4
    Deploy production manual

    First deploy only: with CWA_CI_FIXTURES_ENABLED="true", then load the fixtures

Prepare the cluster

The pipeline deploys into a cluster you already run. It doesn't install anything cluster-wide, so set these up once:

  • A Kubernetes cluster. The deploy jobs install kubectl 1.31.0 and Helm 3.19.0 by default. The daily orphan scan's time zone needs Kubernetes 1.27 or later.
  • ingress-nginx. The pipeline writes ingress-nginx annotations, and its rate limit and www redirect settings are ingress-nginx's. Give the controller's Service externalTrafficPolicy: Local, ideally with 2 replicas and a PodDisruptionBudget. Otherwise every visitor looks internal and Caddy's per-visitor rate limit never applies (see Origin Protection › Ingress rate limits).
  • cert-manager with two ClusterIssuers, named exactly letsencrypt-staging and letsencrypt-prod. The pipeline names the issuer in the ingress's cert-manager.io/cluster-issuer annotation, from CWA_CI_CLUSTER_ISSUER, but never creates it. It defaults to letsencrypt-staging.
  • A way for CI to reach the cluster. On GitLab, a GitLab agent whose context you set as KUBE_CONTEXT. On GitHub, a kubeconfig in the KUBECONFIG secret, and its context in the KUBE_CONTEXT secret.
The ClusterIssuer names must match. A ClusterIssuer with any other name is never used, and no certificate is issued. If yours have other names, set CWA_CI_CLUSTER_ISSUER to the name on every deploy.

The issuers use Let's Encrypt's HTTP-01 challenge through ingress-nginx. A minimal pair, following cert-manager's own examples (use your own email address):

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Create the namespaces

The pipeline never creates namespaces. It only deploys into ones that exist, with role bindings for the identity CI deploys with:

  • Production's namespace, which staging and canary share. On GitLab it's <project name>-production unless you set KUBE_NAMESPACE. On GitHub, set its name in the KUBE_NAMESPACE_PRODUCTION secret.
  • One namespace per branch that should get a review app. A branch without one is skipped, and its pipeline shows orange.

How to create them, and the role binding to add, is in CI/CD › Creating a namespace for a branch. The CI identity also needs access to cert-manager's Certificates in production's namespace, so changing hostnames later causes no downtime (see CI/CD › Permissions).

Set the CI variables

The CLI sets up GitLab CI or GitHub Actions when you create the project. Set the few variables a new site needs: the cluster connection, the domain (KUBE_INGRESS_BASE_DOMAIN), an external database, the media bucket, the mailer and the first admin's password. The checklist is CI/CD Variables › Minimum for a new site.

Use an external database for a real site. The in-cluster PostgreSQL keeps no data by default, so it loses everything when its pod is replaced. Set DATABASE_URL and CWA_CI_POSTGRES_ENABLED="false" (see CI/CD Variables › Database).

Point DNS at the ingress

Let's Encrypt's HTTP-01 challenge must reach the cluster's ingress, so point these hostnames at the ingress controller's address:

  • production: KUBE_INGRESS_BASE_DOMAIN, and any KUBE_INGRESS_ALIAS_DOMAINS;
  • staging: staging.<KUBE_INGRESS_BASE_DOMAIN>;
  • review apps: <branch>-review.<KUBE_INGRESS_BASE_DOMAIN>, usually through a wildcard record.

If Cloudflare is in front, leave each record DNS only until its first certificate is issued (see Cloudflare › Each deploy track).

Push main and check staging

A push to main builds the images, runs the tests and deploys staging. On GitHub, staging runs only once you set the CWA_CI_STAGING_ENABLED variable to true (see Pipeline flags).

Staging gets a certificate from letsencrypt-staging, which browsers don't trust, so expect a certificate warning. That's on purpose: if a hostname or the DNS is wrong, the challenge fails against the staging issuer's generous limits instead of Let's Encrypt's production limits. Check that staging serves the site, warning aside.

Switch to trusted certificates

Once the domain points at the cluster and staging's certificate was issued, set CWA_CI_CLUSTER_ISSUER=letsencrypt-prod, and keep it on every production site. On GitLab, a variable scoped to production reaches staging and canary too.

Deploy production

Production is a manual job: production in GitLab's pipeline, or the Production workflow on GitHub, run from the Actions tab. On GitLab it waits for the builds and tests. Both remove staging and canary once production is deployed (see CI/CD › Production). Before later deploys, you can read production's pending migrations, and turn on a backup before they run: see Migrations and Backups.

For the first deploy only, load the fixtures, which create the scaffold pages and the first admin user:

  1. Set CWA_CI_FIXTURES_ENABLED="true".
  2. Deploy production. On GitLab, then start the manual load fixtures production job. On GitHub, the Load fixtures step runs after the deploy by itself.
  3. Remove the variable. On GitHub the step runs after every deploy while it's set.

Fixture loads append and never delete content. See CI/CD › Fixtures on a deploy.

Optional: put Cloudflare in front

Proxy only, with no edge caching, is the recommended start: Cloudflare › Option A. Turn on edge caching later, if you want it, with Cloudflare › Option B.

Check the site

  • The cache warm after the production deploy (on GitLab, in the image variants and warm production job) lists every sitemap page with its status. Any page that isn't 200 is listed under CACHE WARM FAILED (see CI/CD › Warming the cache after a deploy).
  • Pages come from the cache. Request a page twice: the second response's cache-status header shows a Souin hit. Behind Cloudflare's Option A, cf-cache-status is DYNAMIC.
    curl -sI https://www.example.com/ | grep -i -E 'cf-cache-status|cache-status'
    
  • You can sign in. Sign in at /login with ADMIN_USERNAME and ADMIN_PASSWORD. If the deploy or the fixture load warned that the login is admin / admin, set ADMIN_PASSWORD and load the fixtures again: a password you change in the admin goes back to ADMIN_PASSWORD at the next fixture load (see CI/CD Variables › The admin password).
  • Optional: run the performance audit to see what a visitor experiences.
Image placeholder: a cache warm's log after a first production deploy, every page 200, with its time to first byte.

Which page do I need?

I want to…Page
Run the stack locally, or understand the compose filesDocker
Deploy, back up and restore a site on one server (a preview)Deploy to One Server
Move a site from one server to KubernetesMoving From One Server to Kubernetes
Understand what the Helm chart deploys and how it scalesKubernetes & Helm
Understand the pipeline's stages, scripts, fixtures and hostname changesCI/CD
See pending migrations before a production deploy, back the database up, or close the site while it migratesMigrations and Backups
Look up a CI variable, its default, or the Helm value it setsCI/CD Variables
Put Cloudflare in front, with or without edge cachingCloudflare
Keep made-up URLs and request floods from getting past the cacheOrigin Protection
See every cache layer and what clears itCaching
Find out how much traffic the site can takeLoad Testing
Measure what one visitor experiencesPerformance Audit
Test the built site in a browser before every deployBrowser Tests
Speed up a slow functional test suiteFaster Functional Tests
Fix an error or a symptomTroubleshooting
Bring a project up to a newer template releaseReleases