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

Docker

The Docker Compose setup for local development and production — services, environment variables, volumes, and build workflow.

The CWA template repository ships a complete Docker Compose stack. Clone the template and you have a fully wired local environment — PHP, Nuxt, Mercure, PostgreSQL, and a reverse proxy — ready in minutes.

Services

ServiceImageRole
phpFrankenPHPSymfony API (HTTP + Mercure publisher)
nuxtNode 22Nuxt SSR application
databasePostgreSQL 16Primary database
mercureCaddy + Mercure moduleReal-time hub
traefikTraefikReverse proxy routing

Environment Variables

Create .env.local alongside .envnever commit .env.local:

# Database
DATABASE_URL=postgresql://app:secret@database:5432/app?serverVersion=16&charset=utf8

# JWT Authentication
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your_jwt_passphrase
JWT_COOKIE_SAMESITE=strict

# Mercure
MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=https://mercure.localhost/.well-known/mercure
MERCURE_JWT_SECRET=your_mercure_secret

# Nuxt (server-side API URL = internal Docker network; browser URL = public)
NUXT_PUBLIC_CWA_API_URL=http://php
NUXT_PUBLIC_CWA_API_URL_BROWSER=https://api.localhost

# Email
MAILER_DSN=smtp://localhost:1025

The two CWA API URL variables are intentionally different in Docker Compose: NUXT_PUBLIC_CWA_API_URL is the internal Docker hostname (Nuxt → PHP on the Docker network), while NUXT_PUBLIC_CWA_API_URL_BROWSER is the public-facing URL clients use from their browsers.

Starting the Stack

# Start all services in the background
docker compose up -d

# Run database migrations
docker compose exec php bin/console doctrine:migrations:migrate

# Create your first admin user
docker compose exec php bin/console silverback:api-components:user:create

# Load fixtures (optional)
docker compose exec php bin/console doctrine:fixtures:load

Development Workflow

docker-compose.override.yml mounts source directories into the containers:

  • PHP: your api/ directory is mounted; PHP changes are reflected immediately (no build step)
  • Nuxt: app/ is mounted; Vite HMR updates the browser on save
  • Xdebug: configured in the override file; connect via your IDE on port 9003

JWT Key Generation

Generate JWT keys once per environment before first start:

docker compose exec php bash -c "
    mkdir -p config/jwt
    openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
    openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
"

The passphrase you use must match JWT_PASSPHRASE in .env.local.

Building for Production

Multi-stage Dockerfiles produce lean production images:

# Build PHP image
docker build --target runner -t ghcr.io/your-org/app-php:latest ./api

# Build Nuxt image
docker build --target runner -t ghcr.io/your-org/app-nuxt:latest ./app

PHP Dockerfile stages:

  1. deps — install Composer dependencies
  2. builder — copy app, remove dev dependencies
  3. runner — FrankenPHP with production config

Nuxt Dockerfile stages:

  1. depspnpm install
  2. builderpnpm build (outputs .output/)
  3. runner — copies .output/, runs node .output/server/index.mjs

Migrations are not run at image build time. Run them as a separate step in your deploy process before starting the new containers.

Production Docker Compose

For production, remove the docker-compose.override.yml and use the base docker-compose.yml with your production environment file:

docker compose --env-file .env.production up -d

Tag images with the git SHA for immutable, rollback-capable deploys:

docker build -t ghcr.io/your-org/app-php:$GIT_SHA ./api
docker push ghcr.io/your-org/app-php:$GIT_SHA

Common Gotchas

NUXT_PUBLIC_CWA_API_URL vs NUXT_PUBLIC_CWA_API_URL_BROWSER: Must be different when your API is on an internal Docker hostname. The server-side URL uses the Docker service name; the browser URL must be the public domain.

JWT keys: Generate once and mount as a secret — never bake private keys into the image.

Database migrations on restart: Don't run migrations in the container CMD. Race conditions occur when multiple pods start simultaneously. Run them as a pre-deploy Job.

Mercure cookie SameSite: Set JWT_COOKIE_SAMESITE=none and Secure: true if your API and front-end are on different subdomains. On the same domain, strict is safe.