Project Structure

How the .dokkimi/ folder is organized.

The .dokkimi/ folder

The only requirement is that a .dokkimi/ folder exists at your repo root. You can organize files inside however you want — there are no required subfolder conventions. All **/*.json and **/*.yaml / **/*.yml files inside .dokkimi/ are scanned. You can use JSON or YAML interchangeably for definitions and shared fragments.

A common convention:

.dokkimi/
├── config.yaml            # Project-level config (optional)
├── definitions/           # Runnable definition files
├── shared/                # Shared item fragments (referenced via $ref)
└── init-files/            # Database init scripts (SQL, JS)

Two file types

Dokkimi distinguishes file types by their JSON shape, not by folder or naming convention:

Runnable definition

Has top-level name (string) + items (array):

{
  "name": "my-test-suite",
  "config": { "timeoutSeconds": 300 },
  "variables": { ... },
  "items": [ ... ],
  "tests": [ ... ]
}

Shared fragment

Any JSON file that does NOT have both name + items. Typically a single item object:

{
  "type": "DATABASE",
  "name": "postgres-db",
  "database": "postgres"
}

Fragments are not runnable on their own — they exist to be referenced via $ref from runnable definitions.

Definition config block

The optional config object in a definition holds run-level settings:

{
  "name": "my-test-suite",
  "config": {
    "timeoutSeconds": 300,
    "browser": {
      "version": "148.0.7778.56"
    }
  },
  "items": [ ... ],
  "tests": [ ... ]
}
FieldTypeRequiredDefaultDescription
timeoutSecondsintegerNo300Timeout for the entire run in seconds
browser.versionstringNo"148.0.7778.56"Chrome version tag. Changing this requires regenerating visual baselines.

The config block is entirely optional — omit it to use defaults. The browser block is ignored when the definition contains no UI actions.

Config file

The .dokkimi/ folder can contain a project-level config file: config.yaml, config.yml, or config.json (first match wins). This file is not a definition — it's loaded separately by name.

dokkimi: 0.1.0
env:
  REGISTRY: ghcr.io/dokkimi
  IMAGE_TAG: v1.2.3
  STRIPE_TEST_KEY: sk_test_abc123
FieldTypeDescription
dokkimistringTarget Dokkimi version. The CLI warns if your installed version is older.
envobjectFlat string → string map of build-time values. Keys must be alphanumeric + underscores.

Build-time interpolation: ${{VAR}}

Any string value in a definition or shared fragment can reference a config env variable with ${{VAR}}. These are resolved at build time by the definition resolver, before the definition is sent to Control Tower.

{
  "type": "SERVICE",
  "name": "my-service",
  "image": "${{REGISTRY}}/my-service:${{IMAGE_TAG}}",
  "port": 3000,
  "healthCheck": "/health"
}

After resolution, the image becomes ghcr.io/dokkimi/my-service:v1.2.3.

${{VAR}} vs {{VAR}}${{VAR}} always resolves from config.yaml env only. {{VAR}} resolves at build time in item fields (from a map seeded with config.yaml env, then definition-level variables merged on top) and at runtime in test steps (from test variables, loops, and extraction). For keys not overridden by a definition variable, {{FOO}} and ${{FOO}} resolve to the same value. If a ${{VAR}} reference doesn't match any key in env, the resolver errors. Unresolved {{VAR}} in items is also an error.

Staged bootup order

By default, all items boot in parallel. If a service expects its dependencies to be running at startup (e.g., a database with no reconnection logic), use the stage property to control deployment order.

Each item accepts an optional stage field (non-negative integer, defaults to 0). Items deploy in stage order — stage N+1 containers don't start until all stage N items are healthy.

items:
  - $ref: ../shared/postgres.yaml
    stage: 0
  - $ref: ../shared/redis.yaml
    stage: 0
  - $ref: ../shared/lago-api.yaml
    stage: 1

Stage 0 (Postgres + Redis) boots and becomes healthy. Only then does stage 1 (lago-api) deploy.

Isolation model

Each runnable definition file creates its own isolated Docker namespace — a dedicated network with its own services, databases, browser, and DNS. No state is shared between definitions. When a run finishes, the entire namespace is destroyed.

Tests within a single definition share the same namespace intentionally. This lets you write multi-step workflows where test 2 depends on state that test 1 created — for example, creating a user in one test and verifying their data persists in the next. Tests within a definition execute sequentially in order.

For scenarios that need completely independent data, put them in separate definition files. Each file gets its own fresh environment. You can run multiple definitions in parallel with dokkimi run --max-parallel N, and they'll never interfere with each other because each runs in its own isolated namespace.

Test runtime

Dokkimi's orchestration layer adds under a second of overhead. What determines your total runtime is your containers and your tests:

To keep runs fast:

.dokignore

You can create a .dokkimi/.dokignore file to exclude files from cloud/CI test runs. It uses the same syntax as .gitignore:

# Ignore experimental definitions
experiments/

# Ignore a specific file
definitions/broken-test.json

# Ignore all files matching a pattern
**/wip-*.json

Paths are relative to the .dokkimi/ directory. Ignored files are still valid for local runs — .dokignore only affects cloud/CI pipelines.