Runners
The three runner strategies — docker, compose and custom — what each one generates, how profile fields map onto them, resource limits, networking, sidecar orchestration, and the teardown differences
The runner: field in the profile decides how the container is started.
Comparison
docker | compose | custom | |
|---|---|---|---|
| Use case | one container | agent plus companion services | full control with your own compose file |
| Mechanism | Docker API (dockerode) | generated compose file + docker compose | your compose file + docker compose |
| Config source | profile fields | profile fields plus the compose: block | .heretic/cli/compose.yaml |
| Generated file | none | .heretic/temp/<session>/compose.yaml | none |
| Command override | yes | yes | ignored |
Detached (-d) | yes | yes (up -d) | yes |
| Attach | the agent container | the agent service | the first service reported by compose |
| Build sidecars | yes (dockerode-native) | yes (depends_on: service_healthy) | rejected |
| Requires | Docker daemon + docker CLI (for attach) | Docker Compose v2 | Docker Compose v2 + your file |
runner: docker — a single container
The default. All profile fields apply: image, volumes, env, extra, dind, mcp, secrets, tool_backends.
image: giglabo/claude-heretic:latest
runner: docker
provider: anthropic
Start-up sequence: ping the daemon (Docker is not available. Please ensure Docker is running.), pull the image when it is missing locally (Failed to pull image <image>: …), start the build sidecars if configured, remove any container with the same name, create and start the container, fix ownership of the mounted agent home, then attach (interactive) or return the ID (detached).
Interactive mode shells out to docker attach, which is more reliable for TTYs than an API stream — so the docker CLI must be installed, not just the socket.
Resource limits and other extra: fields
| Field | Type | Example | Notes |
|---|---|---|---|
memory | string | "4g", "512m" | units b, k, m, g; anything else fails with Invalid memory format |
cpus | string | "2.0", "0.5" | fractional values allowed |
shm_size | string | "2g" | size of /dev/shm |
capabilities | array | ["SYS_PTRACE"] | added Linux capabilities |
privileged | boolean | true | full host access — avoid |
network | string | "host", "bridge", "none" | network mode |
ports | array | ["8080:80", "3000:3000"] | host:container; a bare "8080" maps the same port both sides |
user | string | "1000:1000" | uid:gid |
run_as_root | boolean | true | root with HOME=/home/agent |
hostname | string | "my-agent" | container hostname |
labels | object | {team: platform} | applied after heretic's labels, so it can override them |
extra:
memory: "8g"
cpus: "4.0"
shm_size: "2g"
network: host
ports: ["3000:3000", "5173:5173"]
capabilities: [SYS_PTRACE]
privileged: false
extra.network and sidecars
Build sidecars need the agent on their private network. If you pin extra.network, your value wins and a warning is logged that the builders may be unreachable.
runner: compose — agent plus services
Use it when the agent needs a database, a cache, or a local MCP server. The CLI writes .heretic/temp/<session>/compose.yaml and runs:
docker compose -f <file> -p heretic-<agent>-<session> up [-d]
The project name is lowercased with unsupported characters replaced by -. Compose v2 is required: docker compose v2 is not available. …
The agent is always the service named agent — do not reuse that name in compose.services.
image: giglabo/claude-heretic:latest
runner: compose
provider: anthropic
extra:
ports: ["3000:3000"]
compose:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
Structure of the generated file
version: "3.8"
services:
agent: # always "agent"
image: …
environment: … # secrets + env + token injection + transformations
volumes: … # profile volumes + MCP + session mounts
labels:
heretic.managed: "true"
heretic.agent: <name>
heretic.project: <dir>
heretic.session: <session>
stdin_open: true
tty: true
working_dir: …
command: [...] # profile command or the CLI override
network_mode: … # from extra.*
ports: [...]
cap_add: [...]
privileged: …
user: …
hostname: …
deploy:
resources:
limits:
memory: …
cpus: …
shm_size: …
depends_on: # when build sidecars are configured
builder-node:
condition: service_healthy
db: … # your compose.services
builder-node: … # heretic's builder services, merged LAST
networks: … # your compose.networks
volumes: … # your compose.volumes + sidecar cache volumes
Your services are merged first and heretic's builder services last, so heretic wins on a name collision. Named volumes referenced by services — yours and the sidecar caches — are declared at the top level.
The compose: block
| Field | Type | Description |
|---|---|---|
compose.services | object | additional services; any Compose v3.8 service configuration |
compose.networks | object | top-level networks: definitions |
compose.volumes | object | top-level named volume declarations |
The block is deep merged by a local override (objects recurse; arrays and primitives replace), and it is ignored — with a validation warning — when the runner is not compose.
Networking between services
All services share the compose default network and resolve each other by service name — the agent reaches PostgreSQL at db:5432 and Redis at redis:6379 with no extra configuration.
compose:
networks:
internal:
driver: bridge
services:
db:
image: postgres:16
networks: [internal]
A local MCP server as a service
image: giglabo/claude-heretic:latest
runner: compose
provider: anthropic
mcp:
- name: my-local-server
command: npx
args: ["-y", "mcp-remote", "http://mcp-server:8080/sse"]
compose:
services:
mcp-server:
image: my-mcp-server:latest
environment:
API_KEY: "${MY_API_KEY}"
Compose teardown deletes named volumes
The compose runner's own stop() runs docker compose down --volumes, which removes named volumes — including builder caches. heretic-cli stop works through labels instead and keeps them. Use heretic-cli stop when you want the caches to survive.
Build sidecars and SSH
To route npm, pip, mvn, go or cargo into a sibling container or onto a remote host, see Sidecars and SSH Host Access.
runner: custom — your own compose file
Use it when you need the full Compose spec: build contexts, health checks, depends_on conditions, several named volumes, elaborate networking.
You write .heretic/cli/compose.yaml and heretic runs it verbatim with docker compose -f <file> -p heretic-<agent> up — note the project name has no session component. A missing file fails at construction time:
Custom compose file not found: <path>
The custom runner requires a .heretic/cli/compose.yaml file in your project directory.
heretic-cli local-init --compose # starter template
$EDITOR .heretic/cli/compose.yaml
# profile
image: giglabo/claude-heretic:latest # required by validation, unused by this runner
runner: custom
provider: anthropic
secrets:
ANTHROPIC_API_KEY: ~/.heretic/get-anthropic-key.sh
env:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"
MY_DB_PASSWORD: "${MY_DB_PASSWORD}"
What the CLI injects
Nothing is written into your file. Instead these are exported into the docker compose process environment, so ${VAR} references inside the compose file resolve:
- resolved
secrets: - profile
env:values GH_TOKEN,GITHUB_TOKENGH_COPILOT_TOKEN,GITHUB_COPILOT_TOKEN(forprovider: copilot)
Differences from runner: compose
| Feature | compose | custom |
|---|---|---|
| MCP config | generated and mounted | you manage it |
session directory (~/.claude, ~/.copilot) | mounted automatically | you manage it |
onboarding seed (.claude.json) | created | not created |
extra: fields | applied to the agent service | ignored |
compose: block | merged into the generated file | ignored |
| command override | applied | ignored |
| build sidecars | supported | rejected: Build sidecars require the 'docker' or 'compose' runner … |
| attach target | the agent service | the first service reported by compose |
Full example
version: "3.8"
services:
agent:
image: giglabo/claude-heretic:latest
environment:
- ANTHROPIC_API_KEY
- GH_TOKEN
- NODE_ENV=development
volumes:
- ${PWD}:/workspace
- agent-home:/home/agent
working_dir: /workspace
stdin_open: true
tty: true
networks: [app]
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks: [app]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
networks: [app]
networks:
app:
driver: bridge
volumes:
pgdata:
agent-home:
This waits for PostgreSQL to be healthy, persists the database and the agent home across restarts, and keeps everything on a dedicated bridge network.
Choosing a runner
| Scenario | Runner |
|---|---|
| a single agent, nothing else | docker |
| the agent needs a database or cache | compose |
| the agent needs a local MCP server as a service | compose |
| you need build toolchains the image lacks | docker or compose with sidecars |
you need health checks, build contexts or depends_on | custom |
| you already have a compose setup to integrate with | custom |
Known quirks
isRunning()for the compose runner parsesdocker compose ps --format jsonexpecting a JSON array; newer Compose versions emit newline-delimited JSON, and the check then reports "not running" even though the stack is up.heretic-cli ps, which reads labels, is unaffected.- The generated compose file is deleted when the CLI process exits (including on
SIGINT/SIGTERM);heretic-cli stopdoes not need it because it works from labels.
Next Steps
- Sidecars — build toolchains in sibling containers
- SSH Host Access — the remote-host backend
- Agent Profiles — the full schema
- Sessions — parallel instances