DevStack Guide
On this page
The CRAIG devstack is a Docker Compose environment that runs every piece of infrastructure and every application service on your own machine. It gives you a complete, self-contained development and testing environment: ephemeral data, pre-configured authentication, and realistic seed data.
Prerequisites
-
Docker Desktop (Windows/macOS) or Docker Engine (Linux)
-
Docker Compose v2 (included with Docker Desktop)
-
At least 8 GB of RAM allocated to Docker
|
On Windows, use Docker Desktop with the WSL 2 backend for best performance. |
Quick Start
cargo xtask dev start
The start command:
-
Builds all Docker images
-
Starts infrastructure (PostgreSQL, RabbitMQ, Keycloak, Garage)
-
Starts CRAIG services (rules, cases, placement, exchange, financial, reporting, security, intake, web)
-
Initializes the Garage S3-compatible object store (idempotent)
-
Waits for all services to pass health checks (up to 5 minutes)
-
Runs the
craig-seedone-shot container to load mock data
Once complete you will see:
==> CRAIG devstack is ready.
Open http://localhost:8080 to access the web UI. Log in with one of the test user accounts.
Command Reference
The cargo xtask dev command supports the following subcommands.
Running with no arguments or help prints usage information.
start
Build images, start all containers, initialize Garage, and wait for health checks.
cargo xtask dev start
This is the primary command for bringing up the devstack. If the images are already cached, subsequent starts are faster: only changed layers are rebuilt.
stop
Stop all containers and remove them.
Data volumes (Garage, PostgreSQL) are preserved, so a subsequent start will resume with existing data.
cargo xtask dev stop
reload
Rebuild containers and restart without wiping data. Useful after making code changes: the new binaries are compiled into fresh Docker images, but databases and object storage keep their data.
cargo xtask dev reload
This is equivalent to stop followed by start.
restart
Wipe all data and start fresh.
Removes all containers, volumes, and orphan resources, then performs a full start including Garage initialization and seed data loading.
cargo xtask dev restart
Use this when you want a completely clean environment (e.g., after schema changes or to reset seed data).
reseed
Wipe data volumes and restart the stack with cached images.
This is faster than restart because the built images are kept: only the containers are recreated and craig-seed re-run.
The pre-push hook uses it between validate and e2e, because the integration tests that validate runs leave data behind and the e2e suite needs a clean seed.
cargo xtask dev reseed
Typical duration: 30–60 seconds versus ~20 minutes for a full restart.
restart-service
Restart a single CRAIG application service without touching the rest of the stack.
Data volumes are preserved; the command polls the service’s /healthz endpoint and waits for a healthy response before returning.
Used by the E2E cookie-lifecycle suite to prove sessions survive a web-tier restart (ADR-013).
cargo xtask dev restart-service craig-web
The service name is validated against the CRAIG application services list (craig-rules, craig-cases, craig-placement, craig-exchange, craig-financial, craig-reporting, craig-security, craig-intake, craig-composition, craig-web, mock-server — CRAIG_SERVICES in xtask/src/docker.rs) to prevent arbitrary docker compose restart invocations.
clean
Stop everything and wipe all data without restarting.
Like restart but without the subsequent start — leaves the environment fully torn down.
cargo xtask dev clean --confirm
status
Display a formatted table showing the state, health, ports, and uptime of every service.
cargo xtask dev status
Example output:
CRAIG Devstack Status
=====================
Infrastructure:
SERVICE STATE HEALTH PORT(S) UPTIME
------- ----- ------ ------- ------
postgres running healthy 5432 47 minutes
postgres-test running healthy 5433 47 minutes
rabbitmq running healthy 5672, 15672 47 minutes
keycloak running healthy 8180 47 minutes
garage running healthy 3900, 3903 47 minutes
Services:
SERVICE STATE HEALTH PORT(S) UPTIME
------- ----- ------ ------- ------
craig-rules running healthy 8001 47 minutes
craig-cases running healthy 8002 47 minutes
craig-placement running healthy 8003 47 minutes
craig-exchange running healthy 8004 47 minutes
craig-financial running healthy 8005 47 minutes
craig-reporting running healthy 8006 47 minutes
craig-security running healthy 8007 47 minutes
craig-intake running healthy 8008 47 minutes
craig-web running healthy 8080 10 minutes
mock-server running healthy 9090 47 minutes
One-shot:
SERVICE STATE HEALTH PORT(S) UPTIME
------- ----- ------ ------- ------
craig-seed exited - - Exited (0)
Volumes:
craig_garage-data
craig_garage-meta
If nothing is running, it prints CRAIG devstack is not running (no containers found).
logs
Follow container logs in real time. With no argument, follows all services. With a service name, follows only that service.
# All services
cargo xtask dev logs
# Single service
cargo xtask dev logs craig-rules
Press Ctrl+C to stop following.
If you pass an invalid service name, the script prints an error with the list of available services.
e2e
Run the Playwright end-to-end test suite against the running devstack. If the devstack is not running, it is started automatically.
# Run the full E2E suite (the Playwright report gives live counts;
# they drift too fast to pin here)
cargo xtask e2e
# Run only tests matching "rule" in the name
cargo xtask e2e --grep "rule"
# Run with a visible browser window
cargo xtask e2e --headed
# Step through tests interactively
cargo xtask e2e --debug
Any extra arguments are forwarded directly to npx playwright test inside the container.
On failure, a hint is printed with the path to the HTML report:
Hint: open tests/e2e/playwright-report/index.html for the full report
Services and Ports
Infrastructure
| Service | Image | Port(s) | Purpose |
|---|---|---|---|
PostgreSQL 18 |
|
5432 |
Database server; each CRAIG service gets its own database |
PostgreSQL 18 (test plane) |
|
5433 |
|
RabbitMQ 4.2 |
|
5672, 15672 |
Message broker (speaking AMQP, the standard message-queue protocol) plus its management UI |
Keycloak 26.5 |
|
8180 |
OpenID Connect (OIDC) identity provider; imports the |
Garage |
|
3900, 3903 |
S3-compatible object storage for file attachments |
CRAIG Services
| Service | Port | Database | Purpose |
|---|---|---|---|
craig-rules |
8001 |
|
Rules and policy engine (rule sets in the JSON Decision Model, JDM) |
craig-cases |
8002 |
|
Case management (referrals, investigations, cases, plans) |
craig-placement |
8003 |
|
Placement and foster care (homes, placements, matching) |
craig-exchange |
8004 |
|
Data exchange and ICPC (partners, agreements, transactions) |
craig-financial |
8005 |
|
Financial management (payments, rate tables, claims) |
craig-reporting |
8006 |
|
Reporting and analytics (AFCARS, NCANDS, ad-hoc reports) |
craig-security |
8007 |
|
Security, audit logging, compliance reviews, NIST controls |
craig-intake |
8008 |
— (stateless edge per ADR-017; the pre-created |
Public intake: the anonymous child abuse/neglect reporting API |
craig-composition |
8009 |
|
Composition layer engine: arranges plugin surfaces (ADR-035) |
craig-web |
8080 |
— |
Web UI, built as a backend-for-frontend (BFF): it proxies to the backend services and handles authentication itself, so the browser never talks to them directly |
mock-server |
9090 |
— |
A registry-driven stand-in for the 10 typed partner systems, so |
One-Shot Containers
| Container | Purpose |
|---|---|
craig-seed |
Generates and loads seed data into all databases once the services are healthy, using the |
craig-e2e |
Playwright browser tests. Only runs on demand via |
Databases
PostgreSQL is pre-configured with nine CRAIG databases:
CREATE DATABASE craig_rules;
CREATE DATABASE craig_cases;
CREATE DATABASE craig_placement;
CREATE DATABASE craig_financial;
CREATE DATABASE craig_exchange;
CREATE DATABASE craig_reporting;
CREATE DATABASE craig_security;
CREATE DATABASE craig_intake;
CREATE DATABASE craig_composition;
craig_intake is pre-created but never populated, because the intake edge is stateless per
ADR-017. The databases are defined in
devstack/postgres/init.sql, which runs on first startup and also creates the
multibackend-profile IdP databases (authentik, zitadel) and the per-service
migration-owner/runtime role pairs
(#1310).
Schema application happens in the per-service craig-<svc>-migrate one-shot gates
(ADR-063); serving boot only verifies the schema.
Connection credentials: craig / craig (user/password), configured in docker-compose.yml.
Test Users (Keycloak)
The devstack imports a pre-configured craig realm with seven test users:
| Username | Password | Roles |
|---|---|---|
|
|
admin |
|
|
caseworker, supervisor |
|
|
caseworker, eligibility_worker |
|
|
readonly |
|
|
supervisor, county_director |
|
|
supervisor, regional_director |
|
|
admin, state_office |
The Keycloak admin console is available at http://localhost:8180 (login: admin / admin).
Two clients are configured:
-
craig-api: a public client with direct access grants (the resource-owner password credentials flow, ROPC), used by the CLI and integration tests -
craig-ui: a public client using the authorization-code flow with PKCE (Proof Key for Code Exchange), used by the web UI
| Token lifetime is 30 minutes. Long-running test sessions may hit 401 errors if tokens expire. |
Object Storage (Garage)
Garage provides S3-compatible object storage for file attachments (case documents, exchange files).
The start command initializes Garage automatically:
-
Assigns the single node to a data center zone
-
Applies the cluster layout
-
Imports a static API key (
GKdeadbeefdeadbeefdeadbeef) -
Creates the
craig-devbucket -
Grants read/write/owner permissions to the key
This initialization is idempotent: it skips if the bucket already exists.
Configuration
Environment Variables
Services are configured via CRAIG_<SERVICE>__* environment variables defined in docker-compose.yml.
Key variables:
| Variable | Purpose |
|---|---|
|
Keycloak’s public URL (default |
|
Full issuer URL for JWT validation. Override for CI environments where |
|
External URL for craig-web. Used by the OIDC redirect URI and e2e test base URL. |
Split Keycloak URLs
Services use two Keycloak URLs:
-
OIDC_ISSUERis the public URL (for examplehttp://host.docker.internal:8180/realms/craig) that appears in JWTissclaims. Tests and browsers on the host use it. -
OIDC_INTERNAL_URLis the internal Docker URL (for examplehttp://keycloak:8080/realms/craig) that containers use to fetch JWKS signing keys from each other.
This split is necessary because host.docker.internal resolves on the host but not always between containers, while keycloak:8080 resolves between containers but not on the host. (On Linux, host-side resolution itself needs a one-time /etc/hosts mapping — see Local Development.)
Dockerfiles (Not Bind Mounts)
Configuration files (RabbitMQ definitions.json, Keycloak realm export, Postgres init script, Garage config) are baked into images via COPY in their respective Dockerfiles under devstack/.
This avoids a Docker Desktop on Windows bug where bind-mounted files are sometimes mounted as directories.
Development Workflow
Making Code Changes
After modifying Rust source code, you must rebuild the Docker containers for changes to take effect:
cargo xtask dev reload
This rebuilds all images (Docker layer caching makes unchanged services fast) and restarts containers while preserving database data.
Running Integration Tests
Integration tests run on the host against the devstack services. Make sure to rebuild containers first if you’ve changed code:
# Rebuild and restart
cargo xtask dev reload
# Run all tests
cargo nextest run --workspace --locked --profile integration
Running E2E Tests
The Playwright e2e test suite runs in a Docker container against the web UI:
# Run the full suite (starts devstack if needed)
cargo xtask e2e
# Filter by test name
cargo xtask e2e --grep "dashboard"
# Debug a specific test interactively
cargo xtask e2e --debug --grep "login"
Connecting the CLI
The CRAIG CLI connects to devstack services via a profile configuration.
Create or update ~/.config/craig/profiles.toml (Linux/macOS) or %APPDATA%\craig\profiles.toml (Windows):
[devstack]
oidc_internal_url = "http://localhost:8180/realms/craig"
keycloak_client_id = "craig-api"
rules_url = "http://localhost:8001"
cases_url = "http://localhost:8002"
placement_url = "http://localhost:8003"
exchange_url = "http://localhost:8004"
financial_url = "http://localhost:8005"
reporting_url = "http://localhost:8006"
security_url = "http://localhost:8007"
intake_url = "http://localhost:8008"
Then use:
craig --profile devstack rules sets list
Troubleshooting
Service Fails to Start
Check the service logs for errors:
cargo xtask dev logs craig-rules
If a service fails its health check, the start command will time out after 5 minutes and print the last 20 log lines automatically.
Port Conflicts
If you see errors about ports already in use, another process is binding to one of the devstack ports. Check with:
# Linux/macOS
lsof -i :8080
# Windows (PowerShell)
Get-NetTCPConnection -LocalPort 8080
Common conflicts: another web server on 8080, a local PostgreSQL on 5432, or a previous devstack that wasn’t properly stopped.
Use cargo xtask dev clean --confirm to remove any leftover containers and volumes.
RabbitMQ Authentication Errors
The RabbitMQ user is defined in devstack/rabbitmq/definitions.json with a SHA-256 password hash — not via the RABBITMQ_DEFAULT_USER environment variable.
This is because the custom rabbitmq.conf (which loads definitions.json) overrides the Docker entrypoint’s generated config.
If you see AMQP authentication failures, verify that definitions.json contains a valid user entry.
Keycloak Token Expiry
Access tokens expire after 30 minutes. If integration tests or CLI commands start returning 401 errors after running for a while, re-authenticate:
craig --profile devstack auth login
Windows-Specific Issues
-
Git Bash path mangling: The xtask sets
MSYS_NO_PATHCONV=1to prevent Git Bash from converting/garagepaths to Windows paths. -
File bind-mount bug: Docker Desktop on Windows sometimes mounts files as directories. The devstack avoids this by baking config files into images via
COPYin Dockerfiles. Do not switch to bind mounts. -
Line endings: The
craig-seedcontainer runs a shell script that must have Unix line endings. The Dockerfile’scraig-seedstage runssed -i 's/\r$//'to strip Windows carriage returns.
File Layout
| Path | Purpose |
|---|---|
|
Service definitions, ports, environment variables, health checks |
|
Cargo xtask: the cross-platform home of |
|
PostgreSQL Dockerfile and |
|
RabbitMQ Dockerfile, |
|
Keycloak Dockerfile and |
|
Garage Dockerfile and |
|
|
|
Rust binary that generates deterministic, scalable seed SQL. Controlled via |
|
Multi-stage build for all CRAIG Rust services and tools |
|
Playwright test suite — Dockerfile, specs, page objects, fixtures |
CI/CD
The GitLab CI pipeline (.gitlab-ci.yml) has its own devstack setup that does not use cargo xtask dev.
It runs Docker-in-Docker (dind: a sidecar service container runs a second Docker daemon, which the CI job drives over the network) with environment variable overrides:
-
KC_HOSTNAME=http://docker:8180(instead ofhttp://host.docker.internal:8180) -
OIDC_ISSUER=http://docker:8180/realms/craig -
WEB_EXTERNAL_URL=http://docker:8080
The CI pipeline boots the full devstack inline, waits for health, and runs the e2e tests with npx playwright test directly (not via the Docker container).
See .gitlab-ci.yml for the full configuration.