Skip to content
Use Sortie in Docker

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.md tested locally (quick start)
  • API credentials for your agent (for example, ANTHROPIC_API_KEY for Claude Code, GITHUB_TOKEN for Copilot, CODEX_API_KEY for Codex, or provider-specific OpenCode credentials such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_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/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/sortie

Pin to a specific version for reproducible builds:

FROM ghcr.io/sortie-ai/sortie:<version> AS sortie

This 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 a Claude Code image

Claude Code requires Node.js and npm. Its --dangerously-skip-permissions mode refuses to run as root, so the container must use a non-root user.

Create Dockerfile.claude:

FROM ghcr.io/sortie-ai/sortie:latest AS sortie

FROM node:24-slim

# Install Claude Code.
RUN npm install -g @anthropic-ai/claude-code@latest \
    && npm cache clean --force

# Create a non-root user at UID 1000. The 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.claude -t sortie-claude .

Build a Copilot image

GitHub Copilot Coding Agent also requires Node.js. The same pattern applies, with a different npm package:

Create Dockerfile.copilot:

FROM ghcr.io/sortie-ai/sortie:latest AS sortie

FROM node:24-slim

RUN npm install -g @github/copilot@latest \
    && npm cache clean --force

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.copilot -t sortie-copilot .

Build a Codex image

The Codex CLI is a statically linked Rust binary with no runtime dependencies. No Node.js or npm is required.

Create Dockerfile.codex:

FROM ghcr.io/sortie-ai/sortie:latest AS sortie

FROM debian:bookworm-slim

# Install git and the download tools Codex needs.
RUN apt-get update && apt-get install -y --no-install-recommends \
    git ca-certificates curl wget && \
    rm -rf /var/lib/apt/lists/*

# Install Codex CLI for the current Debian architecture.
RUN set -eux; \
    debian_arch="$(dpkg --print-architecture)"; \
    case "${debian_arch}" in \
    amd64) codex_arch="x86_64-unknown-linux-musl" ;; \
    arm64) codex_arch="aarch64-unknown-linux-musl" ;; \
    *) echo "unsupported Codex architecture: ${debian_arch}" >&2; exit 1 ;; \
    esac; \
    curl -fsSL "https://github.com/openai/codex/releases/latest/download/codex-${codex_arch}.tar.gz" \
    | tar -xz -C /usr/local/bin codex; \
    chmod +x /usr/local/bin/codex

RUN 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.codex -t sortie-codex .

Build an OpenCode image

OpenCode requires Node.js, npm, and git. Authentication is provider-specific. In unattended runs, forward the provider variables that match your selected OpenCode model.

Create Dockerfile.opencode:

FROM ghcr.io/sortie-ai/sortie:latest AS sortie

FROM node:24-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    git wget && \
    rm -rf /var/lib/apt/lists/*

RUN npm install -g opencode-ai@latest && npm cache clean --force

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.opencode -t sortie-opencode .

Run the container

Sortie needs two paths at runtime, plus credentials for both the agent and the tracker passed as environment variables:

PathPurposeMount type
Workspace rootAgent working directories for each issueRead-write volume
WORKFLOW.mdWorkflow configuration fileRead-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:

AgentVariable
Claude CodeANTHROPIC_API_KEY
CopilotGITHUB_TOKEN (or GH_TOKEN, or COPILOT_GITHUB_TOKEN)
CodexCODEX_API_KEY
OpenCodeProvider-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:

TrackerVariables
GitHub IssuesSORTIE_GITHUB_TOKEN, SORTIE_GITHUB_PROJECT
JiraSORTIE_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.md

Claude 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.md

Copilot 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.md

Codex 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.md

OpenCode 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.md

If 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:

FlagPurpose
--rmRemove the container on exit
--initInject 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:...:roMount the workflow file as read-only
-p 7678:7678Expose 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.md

Or 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.md

Handle 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 sortie

Add a health check

Sortie exposes two health endpoints:

EndpointPurpose
/readyzReadiness — checks database, preflight, and workflow state. Returns HTTP 503 if any subsystem is unhealthy.
/livezLiveness — 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 1

The 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: json

With 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,"running":2,"retrying":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

The pattern is the same for any agent:

  1. Start from the distroless image as a named stage.
  2. Pick a base image that provides your agent’s runtime (Node.js, Python, etc.).
  3. Install the agent.
  4. Create a non-root user.
  5. Copy the Sortie binary from the named stage.
  6. Set the entrypoint to Sortie.

Example skeleton for a Python-based agent:

FROM ghcr.io/sortie-ai/sortie:latest AS sortie

FROM python:3.12-slim

RUN pip install --no-cache-dir your-agent-package

RUN useradd --create-home --shell /bin/bash --uid 1000 sortie
COPY --from=sortie /usr/bin/sortie /usr/bin/sortie

USER sortie
WORKDIR /home/sortie

ENTRYPOINT ["/usr/bin/sortie", "--host", "0.0.0.0"]

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: healthy

Troubleshooting

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.

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:

FileAgentBase Image
claude-code.DockerfileClaude Codenode:24-slim
copilot.DockerfileGitHub Copilotnode:24-slim
codex.DockerfileCodexdebian:bookworm-slim
opencode.DockerfileOpenCodenode:24-slim

If a section in this guide becomes outdated, check those files for the current recommended configuration.

Was this page helpful?