Files
zitadel/deploy/compose/tests/smoke.spec.ts
T
bac224c56d chore: improve docker compose template, tests and docs (#11593)
Replaces the single-file `docker-compose.yaml` quickstart with a
production-aware, Traefik-based compose pack in `deploy/compose/`. The
pack covers the full arc from a 2-minute localhost quickstart to a
hardened homelab or semi-production deployment.

### What's in the pack

**Stack**: Traefik (proxy) → ZITADEL API (Go `:8080`) + ZITADEL Login
(Next.js `:3000`) → PostgreSQL

All HTTP/gRPC routing is handled by Traefik via Docker labels — no
manual proxy config needed. The Login V2 UI is enabled by default. Login
URLs are derived automatically from `ZITADEL_DOMAIN`,
`ZITADEL_EXTERNALPORT`, and `ZITADEL_PUBLIC_SCHEME` — no separate URL
variables needed.

**Compose files**

| File | Purpose |
|------|---------|
| `docker-compose.yml` | Base stack — works standalone. Uses explicit
`name: zitadel` network for reliable Traefik service discovery. |
| `docker-compose.mode-letsencrypt.yml` | TLS overlay: ACME HTTP
challenge |
| `docker-compose.mode-external-tls.yml` | TLS overlay: upstream LB/CDN
terminates TLS. Uses `forwardedHeaders.trustedIPs` (configurable via
`TRAEFIK_TRUSTED_IPS`) instead of `insecure=true`. |
| `docker-compose.mode-local-tls.yml` | TLS overlay: self-signed certs
for LAN |
| `docker-compose.prodlike.yml` | Splits init / setup / start for
controlled upgrades |
| `docker-compose.test.yml` | CI overlay: swaps images to locally-built
`:local` tags |

**Optional profiles**: `cache` (Redis), `observability` (OpenTelemetry
Collector)

### Build infra

- New `@zitadel/api:pack` and `@zitadel/login:pack` Nx targets build
local Docker images (`zitadel/zitadel:local`,
`zitadel/zitadel-login:local`) for use in CI and local testing
- `apps/api/Dockerfile` now accepts a `BINARY` build arg so local and
release builds share the same image

### Testing

- New `@zitadel/compose` Nx project with targets: `test-config`
(validates all overlay combinations using `--quiet`), `test-run` (starts
full stack with local images), `test-e2e` (Playwright wiring + protocol
matrix tests through Traefik), `test-full` (end-to-end: build → start →
test → teardown), `stop`
- **`@zitadel/compose` is explicitly excluded from `nx affected` in CI
for now** — the full stack smoke test requires a Docker daemon and
significant resources. The intent is to add a dedicated
`compose_smoke_test` CI job in a follow-up. The targets can be run
locally with `pnpm nx run @zitadel/compose:test-full`.

### Documentation

- **`compose.mdx`**: Complete rewrite with a staged structure (Stage 1
Quickstart → Stage 2 Homelab → Stage 3 Beyond Compose). Documents TLS
modes, profiles, secrets hardening, ExternalDomain/Port/Secure
invariant, upgrades, and the path to Kubernetes
- **New `requirements.mdx`**: Lists supported PostgreSQL versions
(14–18), Redis (standalone), Docker Compose v2.x, and reverse proxy h2c
requirements
- **`reverse_proxy.mdx`**: Added intro covering h2c requirements, TLS
modes table, and Login UI routing split
- **`troubleshooting.mdx`**: New sections for container restarts on
upgrade, FIRSTINSTANCE env vars not taking effect, and diagnosing
unhealthy containers
- **`caddy/index.mdx`**: Known issue and workaround for the `TE:
trailers` header hang
- Removed the old
`apps/docs/content/self-hosting/deploy/docker-compose.yaml` embedded in
the docs

### Breaking change

The old `apps/docs/content/self-hosting/deploy/docker-compose.yaml` file
is deleted. The getting-started docs page
(`/self-hosting/deploy/compose`) now points to the new pack via a `curl
| tar` download command.

---

### Checklist

- [x] `deploy/compose/` smoke test passes end-to-end locally (`pnpm nx
run @zitadel/compose:test-full`)
- [x] Docs build passes (`pnpm nx run @zitadel/docs:build`)
- [ ] Follow-up issue created to add `compose_smoke_test` CI job

---------

Co-authored-by: Mridang Agarwalla <[email protected]>
2026-02-24 16:33:12 -08:00

62 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Compose stack wiring smoke tests
*
* PURPOSE: verify that all compose services are correctly wired together.
* These are INFRASTRUCTURE tests, not login-feature tests.
* They answer: "does the compose stack work end-to-end?"
*
* Test path: browser → Traefik (port 8888) → zitadel-login → zitadel-api → postgres
* A single successful login proves every link in that chain is functioning.
*
* For feature-level login tests (MFA, OIDC flows, IdP, etc.) see:
* apps/login/acceptance/ – run via @zitadel/login:test-acceptance
*
* Credentials are read from SMOKE_TEST_ADMIN_USERNAME / SMOKE_TEST_ADMIN_PASSWORD
* (set in .env.test). The fallback values below match the defaults in .env.test and
* docker-compose.test.yml — they are intentional so the test works out-of-the-box
* without any extra configuration. If you change the seeded admin password, update
* .env.test accordingly.
* The initial admin password is seeded via ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD
* in docker-compose.test.yml.
*/
import { expect, test } from "@playwright/test";
const username =
process.env.SMOKE_TEST_ADMIN_USERNAME ?? "[email protected]";
const password = process.env.SMOKE_TEST_ADMIN_PASSWORD ?? "Password1!";
test.describe("compose stack wiring", () => {
test("admin can complete username/password login (proves Traefik → Login → API → DB chain)", async ({
page,
}) => {
// Step 1 – enter login name.
// Use a long timeout for the first navigation: even though the compose stack
// is healthy, ZITADEL's LoginName page makes server-side gRPC calls to the
// API on first render and can be slow when the instance is still warming up.
await page.goto("/ui/v2/login/loginname");
// The login page renders two inputs with the same test ID (one with autofocus,
// one without — likely an accessibility duplicate). Use .first() to avoid a
// strict-mode violation and target the active, focused input.
const usernameInput = page.getByTestId("username-text-input").first();
await usernameInput.waitFor({ state: "visible", timeout: 120_000 });
await usernameInput.fill(username);
// Wait for the button to be enabled before clicking — the page may still be
// hydrating when the input becomes visible.
await page.getByTestId("submit-button").first().waitFor({ state: "visible" });
await page.getByTestId("submit-button").first().click();
// Step 2 – enter password.
// Wait for the password page to fully render before typing; clicking submit
// above triggers a navigation and the input must be present and interactive.
const passwordInput = page.getByTestId("password-text-input").first();
await passwordInput.waitFor({ state: "visible", timeout: 30_000 });
await passwordInput.fill(password);
await page.getByTestId("submit-button").first().waitFor({ state: "visible" });
await page.getByTestId("submit-button").first().click();
// Step 3 – assert signed-in page (ZITADEL redirects here after a successful
// direct login that has no original auth-request to return to).
await expect(page).toHaveURL(/signedin/, { timeout: 30_000 });
});
});