Reference · Deployment

One binary. Nine shapes. Pick the one your infrastructure already has.

A single static Go binary, CGO_ENABLED=0, no runtime dependencies. Deploying it is copying a file and deciding what listens where.

The artifact

One static binary

CGO_ENABLED=0, no glibc requirement, no installer.

The image

FROM scratch

Binary and CA certificates. Nothing else in the container.

Default listener

~/.ion/engine.sock

A unix socket in a directory created 0700.

Idle cost

One process, or none

Job and pipeline shapes run nothing between items.

01 / The constant

Four parts, and the relationship never changes.

Every topology below rearranges where these run. None of them changes what talks to what.

Your applicationyours
The engineours
The modelnot yours
Your tool codeyours

Solid blue is the engine, the only part Ion ships. Dashed is the model, which is not ours and not in your host. The two grey boxes are yours.

02 / Topologies

Worked topologies

A sample, not an inventory. Filter by what you actually have, or compare two shapes side by side.

TOPOLOGY
RUNS AS
REACH FOR IT WHEN
IDLE COST
One machineA process, or a serviceOne person, one workstation, or a single serverOne idle processA managed servicelaunchd or systemdIt must survive reboots and be managed centrallyOne idle processSplit hostsA daemon on a portClients are elsewhere, or the work the tools do belongs on one hostOne idle processA containerAn image you buildYou already ship containersOne idle containerKubernetes sidecarA sidecar, or a jobYou have a cluster and want it beside your APIOne pod, or nothingA queue-driven jobA container per itemWork arrives as events and needs judgment, not a transformNothingA build pipelineOne step in a jobTrying it on real work without deploying anythingNothingA staged workflowOne run per stageSeveral steps each need different judgmentNothingBehind your own clientA daemon your app drivesYou are building the interface people useOne idle process

Where the pieces sit

One machine

One idle process

One host

Your client
The engine
Your extensions
Local socket

Everything on one box, talking over a unix socket. This is the shape you develop against and the shape a small operation can run in production without apology.

LISTENER · unix socketSTATE · ~/.ion on the host

A row is a starting point, not a commitment. Turn on comparison to hold two shapes side by side.

03 / Manifests

The files you would actually write.

Five configurations you can copy as they are.

The shipped image. Two stages, scratch base, healthcheck wired to ion health.

FROM golang:1.26-alpine AS build
RUN apk add --no-cache ca-certificates
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /ion ./cmd/ion/

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /ion /ion
ENV HOME=/data
VOLUME ["/data"]
ENTRYPOINT ["/ion"]
CMD ["serve"]
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD ["/ion", "health"]

04 / Listeners

Paths, ports, and one thing to know before you open a port.

SETTING
DEFAULT
OVERRIDE
Data directory
~/.ion
ION_DATA_DIR
Unix socket
ION_DATA_DIR/engine.sock
ION_SOCKET_PATH
Windows listener
127.0.0.1:21017
ION_SOCKET_PATH
PID file
ION_DATA_DIR/engine.pid
ION_PID_PATH
Exit sentinel
ION_DATA_DIR/engine.exit
ION_EXIT_PATH

ION_SOCKET_PATH, ION_PID_PATH and ION_EXIT_PATH take precedence over ION_DATA_DIR. Give ION_SOCKET_PATH a host:port value and the engine listens on TCP instead of a unix socket.

The TCP listener is unauthenticated

That listener has no authentication. There is no token, no handshake, no client certificate check. The engine accepts a connection, reads a command and runs it. The listener is plaintext, and there is no TLS option on it. The address is used exactly as given, so a bare :PORT binds every IPv4 interface. The engine does not restrict the bind, and it does not warn. The unix socket path gets a filesystem boundary because the engine creates ~/.ion with mode 0700. TCP has no equivalent, so the boundary has to come from the network around it.

Bind

Bind 127.0.0.1 and reach it through an SSH tunnel.

Network

Keep the host on an isolated network segment.

Firewall

Restrict the port with host firewall or security-group rules.

Proxy

Put a TLS or mutual-TLS terminating proxy in front of it.

These are controls outside the engine. None of them adds authentication to the listener itself.

05 / Host

What the host has to provide.

Published binaries cover Linux amd64, macOS amd64 and arm64, and Windows amd64. The published container image is linux/amd64 only.

macOS

Seatbelt

Built in. sandbox-exec wraps the Bash subprocess. Profiles are coarse-grained and not currently customisable.

Linux

bubblewrap

bwrap must be installed and on PATH, and the kernel must allow unprivileged user namespaces. Some hardened kernels do not.

Windows

No implementation

The binary runs and listens on 127.0.0.1:21017. There is no OS sandbox. A policy that requires one blocks the session.

WORKLOAD
MEMORY
CPU
Single short session
32Mi request, 128Mi limit
50m
Multi-session daemon
64Mi request, 256Mi limit
100m
Heavy tool use, large file reads
128Mi request, 512Mi limit
200m

Network egress is the cost driver, not the engine: provider API calls dominate bandwidth and latency. If policy requires the sandbox, a session on a platform without support does not start. No disk-capacity requirement is documented.

06 / State

What is on disk, and what you back up.

What ION_DATA_DIR moves

Point it at a per-instance root and conversations, the scheduler and the runtime files follow it. That is what lets several engines run on one host without colliding on paths or sharing sessions.

conversations/scheduler/engine.sockengine.pidengine.exit

What stays under ~/.ion

ION_DATA_DIR scopes instance state, not identity. Credentials, tokens, user config, extensions and the operational log stay at the home path, so instances on one host share one credential store and one log. Enterprise config has its own override, ION_ENTERPRISE_CONFIG.

config.jsonengine.jsonlcredentials.enccredentials.keymcp-tokens.jsonmcp-clients.jsonsession-bindings.jsonextensions/agents/install_id

Backup boundary

Back up conversations/. Add session-bindings.json if the same session keys must resume their conversations. Credential and MCP token files hold secrets, so back them up only if the process is approved to hold secrets.

conversations/session-bindings.json

Discarding the conversation store loses history, branches, stored attachments, persisted tool results, conversation memory and dispatch history. Sockets and PID files are runtime files, not backup data.

07 / Operating it

Health, logs, and what a restart costs.

Health

ion health is the image HEALTHCHECK. It connects to the configured socket, sends a health command, prints JSON, and exits 0 when the engine answers ok. It exits 1 when it cannot connect, gets an error, or gets no health data. It needs a running engine, not a session.

The response carries engine version, start time, uptime, session count and socket path. For an HTTP liveness probe, bridge HTTP to the socket in a sidecar.

Logs, and where they go

Operational logging is on by default and local: NDJSON at ~/.ion/engine.jsonl. Downstream egress is opt-in through logging.egressTargets: http POSTs batches to logging.egressEndpoint, otel exports OTLP log records. Empty is the default and means local file only.

Telemetry is disabled by default. Enabled with no target it writes ~/.ion/telemetry.jsonl.

One line of engine.jsonl. Every surface writes this shape.ts · level · component · msg · fields, always present
{"ts":"2026-08-19T22:04:05.123456789Z","level":"INFO","component":"engine",
 "tag":"session.dispatch","msg":"tool call completed",
 "session_id":"tab-9f2c","conversation_id":"1780093348767-c1c03e998388",
 "trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7",
 "fields":{"tool":"Bash","turn":3,"duration_ms":842,"status":"ok","cost_usd":0.014}}

msg is a constant, data-free clause so it stays groupable; all variable context goes in fields. Correlation IDs stay top level and are not interchangeable: conversation_id is durable across restarts, trace_id is scoped to one prompt-to-completion run, session_id to one client connection. Omitted, never empty, when out of scope. Source: observability/log-schema.md.

What a restart costs

Less than you would expect. The conversation store is written as the work happens, so a restart cannot lose what already occurred. What a restart interrupts is live identity, and the engine journals enough to continue the root run when recovery is enabled.

Durable

Everything already written

Conversation trees are appended to disk as the work happens, so history, branches, tool results and dispatch records are on disk before a restart can take them. A session binding maps a session key back to its conversation, and the engine picks it up again.

Durable

A client disconnecting

Sessions stay active when the client goes away. Disconnect is not a stop.

Resumes when enabled

The interrupted root run

Before dispatching a recoverable run the engine journals it into the conversation record. On restart it increments the attempt count and continues from the durable checkpoint, without replaying the user prompt. Off unless enabled: engine.json runRecovery, start_session, or an extension override, with maxAttempts defaulting to 2.

Not resumed

In-flight dispatches and streams

The dispatch registry is process memory. A child dispatch that was running when the process died is marked lost on the next start and reported as an event with its child conversation id; the engine does not resurrect it. Provider streams and running tool executions end with the process.

Depends on the signal

The clean exit path

On SIGINT, SIGTERM or SIGHUP the engine writes an exit breadcrumb and flushes active conversations. SIGKILL skips that path. Recovery is exact about durable history and conservative about unknown effects: it does not promise exactly-once execution, and a resumed run is instructed to inspect state before it retries anything.

08 / Worth knowing

Six details that change how you deploy it.

  • ION_SOCKET_PATH takes a host and port as well as a path. Give it host:port and the engine listens on TCP; the CLI and your clients connect the same way either way.
  • ION_DATA_DIR scopes per-instance state, so several engines can run on one host without colliding. It moves conversations, the scheduler and the runtime files; credentials, user config and the operational log stay under ~/.ion.
  • ion prompt --output json is the whole integration story for a pipeline. One command, structured output, exit code you can branch on.
  • A row in that table is a starting point, not a commitment. Nothing coordinates these choices, so nothing constrains them.
  • ion upgrade replaces the binary in place after a SHA-256 checksum check, and refuses to upgrade a development build. Conversations are read forward by a newer engine; reading them with an older engine after a downgrade is not documented.
  • Only the latest release of each component gets security fixes. There is no backport to an older release, so staying current is the update path.

Next: the same pieces, assembled and running

In practice