How to Use Sortie in Docker
Build a container image that pairs Sortie with your agent of choice. The published Sortie image is distroless and contains only the binary. You copy it into your own image and choose the base OS, runtime, and packages your agent needs.
This guide supports two valid starting points:
- Use the maintained Dockerfiles under
examples/docker/when you want the fastest path. - Create your own Dockerfile from the snippets below when you want to control the image layout yourself.
Prerequisites
- Docker 20.10+ with BuildKit enabled
- A working
WORKFLOW.mdtested locally (quick start) - API credentials for your agent (for example,
ANTHROPIC_API_KEYfor Claude Code,GITHUB_TOKENfor Copilot,CODEX_API_KEYfor Codex,KIRO_API_KEYfor Kiro, or provider-specific OpenCode credentials such asANTHROPIC_API_KEY,OPENAI_API_KEY, orGOOGLE_API_KEY)
Use the maintained example Dockerfiles
If you do not need to author your own Dockerfile, build one of the maintained examples from the repository root:
docker build -f examples/docker/claude-code.Dockerfile -t sortie-claude .
docker build -f examples/docker/copilot.Dockerfile -t sortie-copilot .
docker build -f examples/docker/codex.Dockerfile -t sortie-codex .
docker build -f examples/docker/kiro.Dockerfile -t sortie-kiro .
docker build -f examples/docker/opencode.Dockerfile -t sortie-opencode .The rest of this guide shows how to create equivalent Dockerfiles yourself, then explains how to run, persist, and operate the containers.
Install Sortie into your image
Sortie publishes a distroless image at ghcr.io/sortie-ai/sortie. It contains one file: /usr/bin/sortie. Copy the binary into your own Dockerfile using a multi-stage build:
FROM ghcr.io/sortie-ai/sortie:latest AS sortie
FROM node:24-slim
COPY --from=sortie /usr/bin/sortie /usr/bin/sortiePin to a specific version for reproducible builds:
FROM ghcr.io/sortie-ai/sortie:<version> AS sortieThis pattern keeps Sortie agent-agnostic: it does not dictate your OS, package manager, or runtime environment. You pick the base image your agent requires.
Build an agent image
Sortie’s own image is distroless and holds only the binary, so the image you run is your agent’s runtime with Sortie copied into it. The recipe is the same whichever agent you pick: start from the published image as a named stage, choose a base that provides the agent’s runtime, install the agent, create a non-root user, copy the binary across, and make Sortie the entrypoint.
Create Dockerfile.agent:
FROM ghcr.io/sortie-ai/sortie:latest AS sortie
FROM node:24-slim
# Install your agent CLI here. The maintained example Dockerfiles carry a
# working install step for each supported agent.
# Create a non-root user at UID 1000. A Node base image already has a
# "node" user at that UID - remove it first.
RUN userdel -r node 2>/dev/null; \
useradd --create-home --shell /bin/bash --uid 1000 sortie
COPY --from=sortie /usr/bin/sortie /usr/bin/sortie
USER sortie
WORKDIR /home/sortie
EXPOSE 7678
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO /dev/null http://localhost:7678/readyz || exit 1
ENTRYPOINT ["/usr/bin/sortie", "--host", "0.0.0.0", "--log-format", "json"]Build the image:
docker build -f Dockerfile.agent -t sortie-agent .Only the base image and the install step differ per agent, and how to install an agent is its vendor’s to publish. Take the install step from the maintained example Dockerfiles and drop it into the placeholder above. Three differences change the shape of the image rather than one line of it:
| Agent | What changes |
|---|---|
| Claude Code | Its permission bypass refuses to run as root, so the non-root user is required rather than a hardening choice. |
| Codex | Ships as a self-contained binary and needs no language runtime, so a plain Debian base is enough. |
| Kiro | Ships as a binary dynamically linked against glibc, so it needs a glibc base such as debian:bookworm-slim rather than a musl-based image like Alpine. |
| OpenCode | Authenticates per provider, so the image needs git and the run must forward the provider credentials your model selection uses. |
Run the container
Sortie needs two paths at runtime, plus credentials for both the agent and the tracker passed as environment variables:
| Path | Purpose | Mount type |
|---|---|---|
| Workspace root | Agent working directories for each issue | Read-write volume |
WORKFLOW.md | Workflow configuration file | Read-only bind mount |
Pass environment variables
The container needs credentials for the agent (to run code) and the tracker (to poll issues and report status). Forward them with -e:
Agent credentials:
| Agent | Variable |
|---|---|
| Claude Code | ANTHROPIC_API_KEY |
| Copilot | GITHUB_TOKEN (or GH_TOKEN, or COPILOT_GITHUB_TOKEN) |
| Codex | CODEX_API_KEY |
| Kiro | KIRO_API_KEY |
| OpenCode | Provider-specific variables such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY. Vertex-backed runs typically also need GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, and VERTEX_LOCATION. |
Tracker credentials:
| Tracker | Variables |
|---|---|
| GitHub Issues | SORTIE_GITHUB_TOKEN, SORTIE_GITHUB_PROJECT |
| Jira | SORTIE_JIRA_API_KEY, SORTIE_JIRA_ENDPOINT, SORTIE_JIRA_PROJECT |
| File (local testing) | None — configured in WORKFLOW.md |
For tracker setup details, see How to connect to GitHub Issues or How to connect to Jira.
If your workflow references other services (private package registries, cloud providers, CI systems), forward those variables too. The container inherits nothing from the host environment unless explicitly passed with -e.
Claude Code with GitHub Issues
docker run --rm --init \
-e ANTHROPIC_API_KEY \
-e SORTIE_GITHUB_TOKEN \
-e SORTIE_GITHUB_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-claude /home/sortie/WORKFLOW.mdClaude Code with Jira
docker run --rm --init \
-e ANTHROPIC_API_KEY \
-e SORTIE_JIRA_API_KEY \
-e SORTIE_JIRA_ENDPOINT \
-e SORTIE_JIRA_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-claude /home/sortie/WORKFLOW.mdCopilot with GitHub Issues
docker run --rm --init \
-e GITHUB_TOKEN \
-e SORTIE_GITHUB_TOKEN \
-e SORTIE_GITHUB_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-copilot /home/sortie/WORKFLOW.mdCodex with Jira
docker run --rm --init \
-e CODEX_API_KEY \
-e SORTIE_JIRA_API_KEY \
-e SORTIE_JIRA_ENDPOINT \
-e SORTIE_JIRA_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-codex /home/sortie/WORKFLOW.mdKiro with Jira
docker run --rm --init \
-e KIRO_API_KEY \
-e SORTIE_JIRA_API_KEY \
-e SORTIE_JIRA_ENDPOINT \
-e SORTIE_JIRA_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-kiro /home/sortie/WORKFLOW.mdOpenCode with Jira
docker run --rm --init \
-e ANTHROPIC_API_KEY \
-e SORTIE_JIRA_API_KEY \
-e SORTIE_JIRA_ENDPOINT \
-e SORTIE_JIRA_PROJECT \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-opencode /home/sortie/WORKFLOW.mdIf your OpenCode model uses OpenAI, Google, Vertex, GitLab Duo, or another provider, replace ANTHROPIC_API_KEY with the provider variables required by that model. See the environment variables reference for the supported pass-through variables.
The flags explained:
| Flag | Purpose |
|---|---|
--rm | Remove the container on exit |
--init | Inject an init process (tini) for zombie reaping |
-e <VAR> | Forward an agent or provider credential into the container |
-e SORTIE_* | Forward tracker or Sortie runtime configuration into the container |
-v .../workspaces:... | Mount the workspace root as a read-write volume |
-v .../WORKFLOW.md:...:ro | Mount the workflow file as read-only |
-p 7678:7678 | Expose the HTTP observability server |
Persist the database
Sortie creates a SQLite database (.sortie.db) in the working directory. Without a volume mount, data is lost when the container stops.
To persist it, mount a volume for the working directory:
docker run --rm --init \
-e ANTHROPIC_API_KEY \
-v sortie-data:/home/sortie \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-claude /home/sortie/WORKFLOW.mdOr point the database to a specific path with --db:
docker run --rm --init \
-e ANTHROPIC_API_KEY \
-v sortie-db:/data \
-v "$(pwd)/workspaces:/home/sortie/workspaces" \
-v "$(pwd)/WORKFLOW.md:/home/sortie/WORKFLOW.md:ro" \
-p 7678:7678 \
sortie-claude --db /data/sortie.db /home/sortie/WORKFLOW.mdHandle process reaping
Sortie handles SIGTERM for graceful shutdown, but orphaned grandchild processes — agent subprocesses that outlive their parent — need an init process for zombie reaping.
The --init flag in the docker run examples above handles this. It injects Docker’s built-in tini as PID 1.
On Kubernetes, enable shareProcessNamespace: true in the pod spec instead.
If you need tini baked into the image itself, install it in your Dockerfile:
RUN apt-get update && apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tini", "--", "/usr/bin/sortie", "--host", "0.0.0.0", "--log-format", "json"]Run as non-root
Claude Code enforces a non-root requirement: --dangerously-skip-permissions exits with an error under UID 0. Even for agents without this restriction, running as non-root is a security best practice.
The example Dockerfiles above create a sortie user at UID 1000. On node:*-slim base images, UID 1000 is already claimed by the node user — remove it first with userdel -r node before creating your own.
If your base image has a different UID layout, adjust accordingly:
RUN useradd --create-home --shell /bin/bash --uid 1000 sortie
USER sortieAdd a health check
Sortie exposes two health endpoints:
| Endpoint | Purpose |
|---|---|
/readyz | Readiness — checks database, preflight, and workflow state. Returns HTTP 503 if any subsystem is unhealthy. |
/livez | Liveness — returns HTTP 200 unless the server is draining (graceful shutdown in progress). |
Use /readyz for Docker HEALTHCHECK because it detects real failures (broken database, invalid workflow), not just process liveness:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO /dev/null http://localhost:7678/readyz || exit 1The tool (wget, curl) depends on your base image. node:24-slim includes wget. The distroless image has no shell, so the health check must be defined in your downstream image.
Emit JSON logs for aggregation
Container runtimes route stdout/stderr to log aggregation pipelines (Loki, Datadog, CloudWatch, ELK). These systems expect newline-delimited JSON. Enable JSON log output with --log-format json:
ENTRYPOINT ["/usr/bin/sortie", "--host", "0.0.0.0", "--log-format", "json"]Or set it in the workflow file’s front matter:
logging:
format: jsonWith JSON active, each log line becomes a self-contained JSON object:
{"time":"2026-04-07T14:30:00.000Z","level":"INFO","msg":"tick completed","candidates":3,"dispatched":2,"dispatched_by_rule":0,"dispatched_by_default":2,"dispatched_by_fallback":0,"running":2,"retrying":0,"held_by_blockers":0,"blockers_unresolved":0,"blockers_not_read":0,"blockers_incomplete":0}All structured fields (issue_id, session_id, error, etc.) appear as top-level keys, ready for indexed search in your aggregation system.
The default text format (key=value lines) remains available and is the better choice when reading logs directly in docker logs or a terminal.
Build the distroless image locally
To build the published distroless image from source:
docker build -t sortie .Inject a version string:
docker build --build-arg VERSION=<version> -t sortie .Include the Git revision in OCI labels:
docker build \
--build-arg VERSION=<version> \
--build-arg REVISION=$(git rev-parse HEAD) \
-t sortie .Cross-compile for a different architecture:
docker build --platform linux/arm64 -t sortie:arm64 .The builder stage runs on the host architecture and uses Go’s native cross-compilation — no QEMU emulation needed.
Adapt for a different agent
Nothing in the recipe is specific to Node. An agent distributed as a Python package needs a Python base image and its own install step, and the rest of the Dockerfile is unchanged: the named distroless stage, the non-root user, the copied binary, and the Sortie entrypoint. The same holds when Sortie reaches the agent over SSH rather than running it in the container: the image then carries an ssh client and no agent runtime at all.
Verify the setup
After building and running your image, confirm that everything works:
# Binary executes correctly
docker run --rm --entrypoint /usr/bin/sortie sortie-claude --version
# Container runs as non-root
docker run --rm --entrypoint /usr/bin/id sortie-claude
# Expected: uid=1000(sortie) gid=1000(sortie) ...
# Health check passes (wait ~30s for the first check)
docker inspect --format='{{.State.Health.Status}}' <container-id>
# Expected: healthyTroubleshooting
Claude Code fails with “must not run as root”: The container is running as UID 0. Verify the USER sortie directive is in your Dockerfile and that you’re not overriding it with docker run --user root.
COPY --from fails with “not found”: The image tag in the FROM ghcr.io/sortie-ai/sortie:... line doesn’t exist. Check available tags at the GitHub Container Registry page or use :latest.
Health check reports unhealthy: Docker runs the health check inside the container, so binding to 127.0.0.1 is enough for HEALTHCHECK. --host 0.0.0.0 matters when you also want the host or another container to reach the observability port through -p. An unhealthy /readyz usually means a workflow, preflight, or database problem. Run wget -qO- http://localhost:7678/readyz inside the container to inspect the response.
Workspace files have wrong permissions: The host directory mounted at /home/sortie/workspaces must be writable by UID 1000. Run chown -R 1000:1000 workspaces/ on the host, or use docker run --user $(id -u):$(id -g) if your host UID differs.
SQLite database locked: Two containers are sharing the same database file. Each Sortie instance needs its own .sortie.db. Use separate named volumes or --db paths for each container.
Kiro fails with authentication errors: Kiro requires KIRO_API_KEY. Sortie runs a kiro-cli whoami canary before the first turn and rejects a missing, invalid, or expired key immediately instead of letting headless chat hang on an interactive login prompt. Verify the key with kiro-cli whoami outside the container; the canary only runs in local mode, not over SSH.
OpenCode fails with provider authentication errors: Forward the provider variables that match the selected OpenCode model. For direct providers, this is typically ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY. Vertex-backed runs also need GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, and usually VERTEX_LOCATION. In SSH mode, those variables must exist on the remote host because Sortie forwards only managed OPENCODE_* variables.
Example Dockerfiles
The Dockerfiles in this guide are self-contained — copy them into your project and build directly. The Sortie repository also maintains reference versions that track the latest best practices:
| File | Agent | Base Image |
|---|---|---|
claude-code.Dockerfile | Claude Code | node:24-slim |
copilot.Dockerfile | GitHub Copilot | node:24-slim |
codex.Dockerfile | Codex | debian:bookworm-slim |
kiro.Dockerfile | Kiro | debian:bookworm-slim |
opencode.Dockerfile | OpenCode | node:24-slim |
If a section in this guide becomes outdated, check those files for the current recommended configuration.
Was this page helpful?