InitRunner

Docker

Run InitRunner in a container without installing Python or managing dependencies.

Old envelopes still load. Convert them with initrunner doctor --fix PATH. See Envelope Migration. The :latest image ships with all extras pre-installed (EXTRAS="all"), so every provider, feature, and interface works out of the box. Since v2026.8.3 it also includes the compiled Svelte dashboard. The default command is initrunner dashboard --expose --no-open.

Looking for the runtime sandbox? Since v2026.4.16, tool subprocesses run under a pluggable sandbox. See Runtime Sandbox for the overview, Bubblewrap Sandbox for the Linux-native backend, or Docker Sandbox for the container backend.

Tip: Want to skip Docker setup entirely? Cloud Deploy offers one-click deployment to Railway, Render, and Fly.io.

Images

Official images are published to both registries:

RegistryImage
GitHub Container Registryghcr.io/vladkesler/initrunner:latest
Docker Hubvladkesler/initrunner:latest

Both are identical multi-platform images (linux/amd64 and linux/arm64) -- use whichever registry your environment prefers.

Tags

Since v2026.8.9 there are two builds, published to both registries on every release:

TagWhat is in itOn disk
latest, <version>Every extra: all providers, MCP, vector store, search, ingestion, channels, dashboard1.07 GB
slim, <version>-slimCore runtime plus the dashboard: OpenAI and Ollama, every dependency-free tool, triggers, flows, teams, groups, --serve308 MB

Use slim when you know what your roles need and none of it is MCP, vector memory, or ingestion. It also runs about 38 MB lighter, because the MCP client stack loads whether or not a role uses it. A role that needs a missing extra fails at load with the uv pip install line, so switching tags never fails silently.

One catch on slim: ephemeral mode, meaning initrunner run with no role file, turns persistent memory on by default, and that needs the vector extra. Since v2026.8.10 it stops at load and names both ways out: add --no-memory to run without memory, or use latest. Examples on this page that name a role file are unaffected.

The version tags (2026.8.10, 2026.8.10-slim) are immutable. latest and slim move with each release and are never published for a pre-release tag, so pin the version tag in production.

Naming the command

The image's default command is initrunner dashboard --expose --no-open, and the entrypoint runs whatever command it is handed. Your own command replaces that default outright, so every example on this page spells out initrunner before the subcommand:

docker run ... ghcr.io/vladkesler/initrunner:latest run -i              # exec: run: not found
docker run ... ghcr.io/vladkesler/initrunner:latest initrunner run -i   # correct

The same goes for command: in Compose and for command: or args: in a Kubernetes pod spec. Examples on this page carried the short form before v2026.8.10, so if you copied one and it exits with exec: run: not found, this is why.

API keys

The container needs API keys to reach your LLM provider. Three ways to pass them:

# 1. Inline (key exported in your shell)
docker run --rm -it -e OPENAI_API_KEY ...

# 2. From your initrunner setup (recommended if you ran `initrunner setup`)
docker run --rm -it --env-file ~/.initrunner/.env ...

# 3. Mount your entire initrunner config directory
#    The container reads /data/.env automatically (INITRUNNER_HOME=/data).
docker run --rm -it -v ~/.initrunner:/data ...

--env-file injects keys only. It does not copy ~/.initrunner/run.yaml. If that file is absent and more than one provider key is set, auto-detect prefers Anthropic. Pin the model you want with -e INITRUNNER_MODEL=openai:gpt-5.4, or use option 3 so the container sees run.yaml.

Quick Start

One-shot prompt

docker run --rm -e OPENAI_API_KEY \
  -v ./roles:/roles \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run /roles/my-agent.yaml -p "Hello"

Interactive chat

docker run --rm -it -e OPENAI_API_KEY \
  -v ./roles:/roles \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run /roles/my-agent.yaml -i

Cherry-picked tools

docker run --rm -it -e OPENAI_API_KEY \
  -v ./roles:/roles \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run --tools git --tools filesystem

Document ingestion

docker run --rm -it -e OPENAI_API_KEY \
  -v ./docs:/docs \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run --ingest /docs

Web dashboard

This is the image default (CMD). --expose binds 0.0.0.0 and generates an API key if you do not set one; sign in at /login with the key printed in the container logs. /api/health stays unauthenticated.

docker run -d -e OPENAI_API_KEY \
  -v ./roles:/roles \
  -v initrunner-data:/data \
  -p 8100:8100 \
  ghcr.io/vladkesler/initrunner:latest

Open http://localhost:8100 to access the dashboard. Pass initrunner dashboard --expose --no-open --roles-dir /roles if you want an explicit command.

Telegram bot

docker run -d -e OPENAI_API_KEY -e TELEGRAM_BOT_TOKEN \
  -v ./roles:/roles \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run telegram --daemon

telegram here is the bundled starter. Mount your own role and name it instead (initrunner run /roles/bot.yaml --daemon) to set an allowlist and budgets. --bot was removed in v2026.8.11.

API server

docker run -d -e OPENAI_API_KEY \
  -v ./roles:/roles \
  -p 8000:8000 \
  ghcr.io/vladkesler/initrunner:latest \
  initrunner run --serve

The API is available at http://localhost:8000.

Docker Compose

Create a docker-compose.yml:

services:
  initrunner:
    # GHCR (default) — or use vladkesler/initrunner:latest (Docker Hub)
    image: ghcr.io/vladkesler/initrunner:latest
    # build: .   # uncomment to build from source
    ports:
      - "8100:8100"   # Web dashboard
      - "8000:8000"   # API server (if also running --serve)
    volumes:
      - ./roles:/roles
      - initrunner-data:/data
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
      - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
      - INITRUNNER_DASHBOARD_API_KEY=${INITRUNNER_DASHBOARD_API_KEY:-}  # persistent dashboard key
    restart: unless-stopped
    command: ["initrunner", "dashboard", "--expose", "--no-open", "--roles-dir", "/roles"]

volumes:
  initrunner-data:

Start the stack:

docker compose up -d

Policy Engine

To enable agent policy enforcement in Docker, mount your policy directory and set the environment variable:

volumes:
  - ./policies:/data/policies
environment:
  - INITRUNNER_POLICY_DIR=/data/policies

See Agent Policy Engine for policy authoring details.

Sizing the container

Memory limits count RSS, and RSS x time x replicas is the bill. Serving hello-world.yaml, measured with docker stats:

$ docker run -d --name agent -m 256m -e OPENAI_API_KEY -p 8000:8000 \
    ghcr.io/vladkesler/initrunner:slim \
    initrunner run /opt/initrunner/example-roles/hello-world.yaml --serve --host 0.0.0.0

$ docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' agent
agent 127.1MiB / 256MiB

Rules of thumb, all worth re-measuring against your own roles with about 30% headroom:

  • 256m for a plain agent, or a whole group of them. Members share the imported stack, so each extra agent costs under 1 MB.
  • 512m for a role with ingest: or vector memory:, which loads LanceDB when the store opens. The same hello-world shape with vector memory measures 212 MiB idle on :latest, so 256m leaves nothing for an ingestion run.
  • MALLOC_ARENA_MAX=2 is already set in both images. It caps glibc's per-thread malloc arenas, which otherwise inflate RSS well past the live heap in a threaded Python process.

See Memory Footprint for where the memory goes and how to measure it.

A CronJob against a warm server

The expensive pattern in Kubernetes is a CronJob that runs initrunner run on every tick: each invocation pays the full import cost of about 110 MB and several seconds of startup, 1,440 times a day on a per-minute schedule. Keep one warm server and have the CronJob call it:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: hourly-digest
spec:
  schedule: "0 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: call
              image: curlimages/curl:8.11.1
              resources:
                requests: {memory: 16Mi}
                limits: {memory: 32Mi}
              env:
                - name: API_KEY
                  valueFrom:
                    secretKeyRef: {name: desk-api, key: api-key}
              args:
                - -sSf
                - -H
                - "Authorization: Bearer $(API_KEY)"
                - -H
                - "Content-Type: application/json"
                - -d
                - '{"model":"digest","messages":[{"role":"user","content":"write the daily digest"}]}'
                - http://desk:8000/v1/chat/completions

The desk Deployment is the one from Grouped Agents, serving every agent from one process. The CronJob pod is a 16 MB curl container.

If the schedule belongs to the agent rather than to the cluster, a cron trigger with --daemon keeps it all in the one process and needs no CronJob at all.

Building Locally

Build the image from the repository root:

docker build -t initrunner .
docker run --rm initrunner initrunner --version

Customizing extras

The default image includes all extras (EXTRAS="all"). You can narrow it down with a build arg:

docker build --build-arg EXTRAS="dashboard,anthropic" -t initrunner-custom .

EXTRAS="dashboard" is exactly how the published :slim image is built.

Environment Variables

Pass API keys and configuration as environment variables:

VariableDescription
OPENAI_API_KEYOpenAI API key
ANTHROPIC_API_KEYAnthropic API key
GOOGLE_API_KEYGoogle API key
INITRUNNER_HOMEData directory inside the container (defaults to /data)
INITRUNNER_DASHBOARD_API_KEYFixed dashboard API key (persists across container restarts)
INITRUNNER_LOG_LEVELLog level: ERROR, WARNING (default), INFO, DEBUG. The way to raise verbosity in a container, where you cannot add --verbose to the command. Since v2026.8.8. See Logging.
MALLOC_ARENA_MAXCaps glibc's per-thread malloc arenas. Both images set it to 2 since v2026.8.9. Override only if you have measured something better for your workload.

Volumes

Container PathPurpose
/rolesMount your role YAML files here
/dataPersistent state — sessions, memory, vector indexes

Ports

PortService
8000API server (initrunner run --serve)
8100Web dashboard (initrunner dashboard)

Docker Entrypoint

The Docker image uses a custom entrypoint that seeds starter examples into /data/roles/ on first boot. If the directory already contains files, seeding is skipped.

Since v2026.8.9 both images seed hello-world.yaml and code-reviewer.yaml, the two examples that run on any install. Three of the five previously seeded examples needed the search, vector, or audio extras, which is a broken first impression in an image built without them.

This is the same entrypoint used by the Cloud Deploy platforms (Railway, Render, Fly.io). If you want to disable seeding, mount your own role directory at /data/roles/ before starting the container.

Ollama Integration

If Ollama runs on the host machine, the container cannot reach localhost. Use the Docker host gateway address in your role YAML:

model:
  provider: ollama
  base_url: http://host.docker.internal:11434/v1

On this page