Installation
Prerequisites
- Docker Desktop (or Docker Engine + Compose)
- Node 22.13 or later and pnpm 11 (for running the CLI and the Nuxt app locally)
PHP, Composer, and all API dependencies run inside Docker — nothing else to install. The Nuxt app's own requirements, including Pinia 4, come in through pnpm install. If you're upgrading an existing app, check the module requirements — the module now needs @pinia/nuxt ^1.0.2, and Nuxt won't boot on an older copy.
1. Scaffold your project
Run the interactive CLI:
pnpm create cwa my-project
The CLI will ask four questions:
| Prompt | Options |
|---|---|
| Project name | The directory to create |
| CI/CD pipeline | GitHub Actions, GitLab CI, or none |
| Features | Navigation links, HTML content editor, image uploads, blog, nested pages, forms (multiselect) |
| Include fixtures? | Scaffolds sample content so the site works out of the box |
After answering, the CLI downloads the template, strips unused feature code from nuxt.config.ts, and generates a README.md tailored to your choices. It then offers to run docker compose up -d and pnpm install in one step — accept both.
create-cwa has the same version as the template release it installs. create-cwa2.0.0-alpha.1 downloads the template, and its cwa-manifest.json of feature options, from the template's v2.0.0-alpha.1 tag, so the same CLI version always installs the same template. Template changes reach new projects only in a new release.--ref:pnpm create cwa my-project -- --ref main
create-cwa0.x versions always installed the template's main branch.2. Generate JWT keys
Authentication uses JWT tokens signed with an RSA key pair. The keys are gitignored, so every environment generates its own:
docker compose exec php bin/console lexik:jwt:generate-keypair
That writes api/config/jwt/private.pem and api/config/jwt/public.pem, encrypted with the JWT_PASSPHRASE from api/.env. The template ships a working passphrase so this step needs no configuration in local development.
docker compose exec php php -r "print bin2hex(random_bytes(26));", add it to api/.env.local as JWT_PASSPHRASE=<your passphrase>, then regenerate the pair with bin/console lexik:jwt:generate-keypair --overwrite. The passphrase and the keys must always match — change one without the other and every login fails silently.Prefer to do it by hand? Run these inside the container (docker compose exec php sh) instead. Both commands prompt for a passphrase — enter the JWT_PASSPHRASE value each time:
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
exit
3. Load fixtures
If you selected Include fixtures during setup, load them once the API container is healthy:
docker compose exec php bin/console doctrine:fixtures:load
This creates a default admin account (admin / admin) and seeds the site structure — layout, pages, and example components.
4. Open the site
There is no dev server to start. docker compose up -d already runs the Nuxt dev server inside the app container, and Caddy proxies it:
| URL | What's there |
|---|---|
https://localhost | Your Nuxt application |
https://localhost/_api | API (JSON-LD) |
https://localhost/_cwa | CWA admin panel |
caddy_data volume, so copy it out with docker compose cp php:/data/caddy/pki/authorities/local/root.crt . and add root.crt to your system trust store.http://localhost:3001 if you ever need to bypass the proxy. Don't run pnpm dev on your host at the same time — that starts a second Nuxt on port 3000 that https://localhost never reaches.Log in at https://localhost/login with admin / admin. Click Edit to enter edit mode and see the inline CMS.
https://localhost with the fixture content loaded — the first thing a reader should see workingWhat Docker handles automatically
When the php container starts, the entrypoint:
- Runs
composer installifvendor/is empty - Waits for PostgreSQL to be ready
- Runs pending database migrations
- Runs
ANALYZEto refresh PostgreSQL query statistics (keeps dev pages fast — see the Docker gotchas)
You never need to run these manually.
CI/CD
If you chose GitHub Actions during setup, three workflows are pre-wired in .github/workflows/:
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml | Every push | Build + test; deploy a review environment for every branch except main |
production.yml | Manual | Deploy canary or production |
cleanup.yml | PR closed | Tear down review environment |
Images push to GHCR. The workflows use the same bin/devops/ shell scripts as the GitLab CI option. Required secrets and variables are documented inline in each workflow file — read those.
publish-create-cwa.yml workflow, which publishes the CLI. create-cwa leaves it out of new projects. A project created before 21 September 2026 may still have it. It only fires on create-cwa/v* tags, so it never runs for you, but you can delete it: rm .github/workflows/publish-create-cwa.yml.Manual setup (alternative)
If you prefer to create a GitHub repository first and clone from there:
With the GitHub CLI:
gh repo create my-website \
--template="components-web-app/components-web-app" \
--private --clone
Or generate from GitHub:
Then follow steps 2–4 above. You'll configure features manually rather than through the CLI.
packages/ directory used for the create-cwa CLI itself. Delete it from your project after cloning — it's not needed in your app.rm -rf packages/
What's next
- Your First Layout — create the Vue file for your layout
- Your First Page Template — define page regions with CwaComponentGroup
- Your First Component — build a PHP entity and matching Vue component