How-to guideAgentic QA environment
Start a self-contained application
Compose owns the application topology. Code Voucher validates it, applies a trusted hardening override, waits for readiness, and addresses services only over an attempt-local network.
A complete topology
This pattern covers a database, an idempotent initialization job, a local external-service substitute, and the browser target. It deliberately publishes no host ports and mounts no host directories.
docker-compose.qa.yml
services:
database:
image: postgres:17-alpine
read_only: true
tmpfs:
- /var/lib/postgresql/data
- /tmp
environment:
POSTGRES_DB: app_qa
POSTGRES_USER: app_qa
POSTGRES_PASSWORD: synthetic-only
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_qa"]
interval: 2s
timeout: 2s
retries: 20
qa-init:
build:
context: .
dockerfile: Dockerfile.qa
target: backend
command: ["./scripts/qa-initialize"]
read_only: true
tmpfs: [/tmp]
environment:
DATABASE_URL: postgres://app_qa:synthetic-only@database/app_qa
APP_ENV: qa
depends_on:
database:
condition: service_healthy
newsletter-stub:
build:
context: ./qa/stubs/newsletter
read_only: true
tmpfs: [/tmp]
environment:
STUB_MODE: deterministic
web:
build:
context: .
dockerfile: Dockerfile.qa
target: frontend
read_only: true
tmpfs: [/tmp]
environment:
DATABASE_URL: postgres://app_qa:synthetic-only@database/app_qa
NEWSLETTER_BASE_URL: http://newsletter-stub:8080
APP_ENV: qa
depends_on:
qa-init:
condition: service_completed_successfully
newsletter-stub:
condition: service_startedReplace image tags with approved digest-pinned images before relying on this pattern outside dogfood. Repository-controlled builds still have build-time network access in the current Docker executor.
Initialization contract
- 1
Migrate an empty database
The initializer must work from a clean, attempt-local database and fail on migration errors. - 2
Seed synthetic identities
Create stable users and records with .test addresses. Never copy production data or sessions. - 3
Remain idempotent
Use upserts or reset-and-seed behavior so retries produce the same visible state. - 4
Exit before web starts
The browser target depends on successful completion. A partial seed must prevent readiness.
scripts/qa-initialize
#!/bin/sh
set -eu
python manage.py migrate --noinput
python manage.py seed_qa_data --reset
python manage.py check --deployDefinition of ready
- Every service sets read_only: true and uses tmpfs for writable runtime paths
- No ports, bind mounts, Docker socket, custom networks, privileged mode, secrets, or env_file
- The health endpoint returns success only after migrations, seed data, and required substitutes are ready
- The browser URL names a Compose service, not localhost
- The environment starts without Internet access or production credentials
Next, replace external services and define deterministic browser scenarios.
Validation today: Code Voucher validates both YAML files when QA starts. There is no public local validation CLI yet. Until one ships, run the QA Compose topology locally and review it against this allowlist before committing.