The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftDeployment

Kubernetes & Helm

Deploying the CWA stack to Kubernetes using Helm — values configuration, secrets management, migration Jobs, and rolling updates.

CWA runs well on Kubernetes. Each service maps cleanly to a Deployment, and the stateless PHP and Nuxt containers make rolling updates straightforward.

The Helm chart lives in the template repository at helm/cwa/ — it is included when you generate a project from the components-web-app template. There is no separate Helm registry; you own the chart and modify it as needed.

What the Helm Chart Deploys

ResourceTypeDescription
phpDeploymentFrankenPHP serving the Symfony API
nuxtDeploymentNode SSR server
mercureDeploymentCaddy + Mercure hub
php, nuxt, mercureServiceInternal cluster services
php, nuxt, mercureIngressExternal routing with TLS

Database is typically a managed external service (Cloud SQL, RDS, etc.) rather than an in-cluster StatefulSet for production.

Minimum values.yaml

php:
    image:
        repository: ghcr.io/your-org/app-php
        tag: latest
    env:
        DATABASE_URL: "postgresql://user:pass@cloud-sql/app"
        MERCURE_URL: "http://mercure/.well-known/mercure"
        MERCURE_PUBLIC_URL: "https://mercure.example.com/.well-known/mercure"
    envFrom:
        - secretRef:
            name: cwa-secrets

nuxt:
    image:
        repository: ghcr.io/your-org/app-nuxt
        tag: latest
    env:
        NUXT_PUBLIC_CWA_API_URL: "http://php-service"
        NUXT_PUBLIC_CWA_API_URL_BROWSER: "https://api.example.com"
    envFrom:
        - secretRef:
            name: cwa-secrets

mercure:
    image:
        repository: dunglas/mercure
        tag: latest
    envFrom:
        - secretRef:
            name: cwa-secrets

ingress:
    enabled: true
    annotations:
        cert-manager.io/cluster-issuer: letsencrypt
    hosts:
        api: api.example.com
        www: www.example.com
        mercure: mercure.example.com

Managing Secrets

Never hardcode secrets in values.yaml. Create a Kubernetes Secret:

kubectl create secret generic cwa-secrets \
    --from-literal=JWT_PASSPHRASE=your_jwt_passphrase \
    --from-literal=MERCURE_JWT_SECRET=your_mercure_secret \
    --from-literal=DATABASE_URL="postgresql://..." \
    --from-literal=APP_SECRET=your_app_secret

For production use External Secrets Operator to sync from AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault.

JWT Keys as Kubernetes Secrets

Generate JWT keys locally, then create a Secret:

kubectl create secret generic jwt-keys \
    --from-file=private.pem=config/jwt/private.pem \
    --from-file=public.pem=config/jwt/public.pem

Mount into the PHP pod as a volume in values.yaml:

php:
    volumes:
        - name: jwt-keys
          secret:
            secretName: jwt-keys
    volumeMounts:
        - name: jwt-keys
          mountPath: /var/www/html/config/jwt
          readOnly: true

Running Migrations

Run migrations as a Kubernetes Job before updating the PHP Deployment. Use a Helm pre-upgrade hook:

# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
    name: "{{ .Release.Name }}-migrations-{{ .Release.Revision }}"
    annotations:
        "helm.sh/hook": pre-upgrade,pre-install
        "helm.sh/hook-weight": "-1"
        "helm.sh/hook-delete-policy": before-hook-creation
spec:
    template:
        spec:
            restartPolicy: Never
            containers:
                - name: migrations
                  image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag }}"
                  command: ["bin/console", "doctrine:migrations:migrate", "--no-interaction"]
                  envFrom:
                      - secretRef:
                          name: cwa-secrets

This ensures migrations complete before any new pod receives traffic — no race conditions from parallel pod starts.

Ingress with TLS

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    annotations:
        cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
    tls:
        - hosts: [api.example.com]
          secretName: api-tls
        - hosts: [www.example.com]
          secretName: www-tls
    rules:
        - host: api.example.com
          http:
              paths:
                  - path: /
                    backend:
                        service:
                            name: php-service
                            port:
                                number: 80
        - host: www.example.com
          http:
              paths:
                  - path: /
                    backend:
                        service:
                            name: nuxt-service
                            port:
                                number: 3000

Resource Requests and Limits (Starting Point)

php:
    resources:
        requests:
            cpu: 100m
            memory: 256Mi
        limits:
            memory: 512Mi

nuxt:
    resources:
        requests:
            cpu: 50m
            memory: 128Mi
        limits:
            memory: 256Mi

Tune based on your traffic profile. FrankenPHP is efficient; Nuxt SSR memory usage grows with concurrent requests.

Health Checks

The PHP (FrankenPHP) pod uses a TCP socket for liveness — it just checks the port is listening — and /_api/_/site_config_parameters.jsonld for readiness, which confirms Symfony is fully booted and the database is reachable.

The Nuxt pod uses /_cwa/healthcheck for readiness. This endpoint is a server route provided by the @cwa/nuxt module (server/routes/_cwa/cwa-healthcheck.get.ts) — it returns 200 when the Nuxt server is running.

php:
    startupProbe:
        tcpSocket:
            port: 80
        failureThreshold: 30
        periodSeconds: 10
    livenessProbe:
        tcpSocket:
            port: 80
        initialDelaySeconds: 5
        periodSeconds: 5
    readinessProbe:
        httpGet:
            path: /_api/_/site_config_parameters.jsonld
            port: 80
        initialDelaySeconds: 5
        periodSeconds: 10

nuxt:
    readinessProbe:
        httpGet:
            path: /_cwa/healthcheck
            port: 3000

Rolling Updates

Both PHP and Nuxt use RollingUpdate strategy. Old pods continue serving until new pods pass their readiness probe, ensuring zero downtime.

strategy:
    type: RollingUpdate
    rollingUpdate:
        maxSurge: 1
        maxUnavailable: 0

Rollback

If a deploy fails:

helm rollback cwa        # revert to previous Helm release
kubectl get pods -w      # watch the rollback progress

Helm tracks release history. The migration Job won't re-run on rollback (it only runs on install/upgrade), so a database rollback requires a separate migration reversal if needed.