Skip to documentation
Browse documentation
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_started

Replace 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. 1

    Migrate an empty database

    The initializer must work from a clean, attempt-local database and fail on migration errors.
  2. 2

    Seed synthetic identities

    Create stable users and records with .test addresses. Never copy production data or sessions.
  3. 3

    Remain idempotent

    Use upserts or reset-and-seed behavior so retries produce the same visible state.
  4. 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 --deploy

Definition 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.