Deployment

Configure alineod, run it as a Docker container, and connect it to OpenSandbox.

alineod is a single Bun process with two SQLite files and a working directory. It holds no in-memory state that can't be rebuilt, so the process — or its container — can be restarted or recreated freely (see Crash recovery). Run exactly one instance per database: restarts are safe, replicas are not (see Scaling).

Configuration

VariableDefaultDescription
ALINEOD_PORT4600HTTP + SSE listen port.
ALINEOD_DB_PATH./data/alineod.dbalineod's own state: the append-only swarm ledger and the agents/handles tables derived from it.
ALINEOD_SDK_LEDGER_PATH./data/alineod-sdk-ledger.dbThe SDK's sandbox ledger (sandbox, exec, and checkpoint events), shared by every agent alineod drives. Turn checkpoints are written next to it.
ALINEOD_WORK_DIR./data/alineod-workSpec files handed to spawn(), and stored agent results.
ALINEOD_PROMPT_INACTIVITY_MS180000If a turn's stream produces nothing for this long, alineod follows the turn by polling instead.
ALINEOD_TURN_MAX_MS1800000How long a polled turn may keep running (paused time excluded) before it's ended with partial output.
ALINEOD_CATCH_UP_POLL_MS2000Interval between state polls while following a turn.
ALINEOD_STATE_PROBE_TIMEOUT_MS5000Bound on one state poll, so an unresponsive agent can't stall alineod.
ALINEOD_RESUME_BRIDGE_TIMEOUT_MS10000After a resume, how long to wait for the agent's harness to answer before restarting it.
ALINEOD_SUBTREE_MEMBER_TIMEOUT_MS30000Bound on one member's pause or resume inside a subtree operation, so one unresponsive sandbox can't stall the sweep (reported as failed: timeout).
ALINEOD_ADMISSION_CONCURRENCYhost vCPUs − 1 (at least 1)How many sandboxes alineod creates or forks at once. Spawns beyond it wait for a slot. See Admission.
ALINEOD_ADMISSION_TIMEOUT_MS600000How long a spawn may wait for a slot before it fails with agent.admission_timeout.
ALINEOD_REATTACH_RETRY_DELAY_MS1000On boot, how long to wait before retrying a failed reattach once — see the restore ladder.
ALINEOD_LEASE_TTL_MS15000How long the instance lease outlives its last heartbeat. See Scaling.

Every variable is declared in apps/alineod/config.ts, and a bad value is rejected at startup rather than silently becoming NaN.

Agent memory has its own ALINEOD_MEMORY_* variables, covered in Agent memory. The memory database is a third SQLite file, and scripts/backup.ts includes it.

OpenSandbox connection settings come from alineo.config.json in the working directory, exactly as for the SDK and CLI. Model-provider keys (NVIDIA_API_KEY, ANTHROPIC_API_KEY, …) are read from alineod's own environment wherever a spec's env references them as ${VAR}.

Fixed limits, set in apps/alineod/config.ts:

  • Socket idle timeout: 255 s (Bun's maximum — its ~10 s default would kill SSE and long-polls).
  • SSE heartbeat: every 10 s.
  • GET /agents/:id/result?wait= is capped at 240 s.

Logging

alineod logs to stderr — never stdout — at info by default:

  • the run and agent lifecycle (run.started, agent.spawned, agent.provisioned, agent.state_changed, agent.ended, handle.settled) — each line carries runId, agentId and the event's seq, so it lines up with the event stream;
  • the crash-recovery pass at boot (rehydrating …, then one line per agent reattached, resumed or lost);
  • problems: a failed, lost or budget-denied agent logs at warn; an unexpected request error logs at error with its method and path.
[alineod] listening url=http://localhost:4600 openapi=/openapi
[alineod] agent.spawned runId=r_b72c6e9c agentId=a_9f89972f seq=4 specName=worker-ocean parentAgentId=a_7fb804cd depth=1 spawnIndex=0
[alineod] agent.ended runId=r_b72c6e9c agentId=a_9f89972f seq=19 outcome=success

The SDK's own progress lines ([agent] …) appear alongside, tagged with the agent's name. Prompts, specs, steer messages and results are never logged — they stay in the ledger.

The format is pretty on a terminal and JSON lines otherwise, so docker logs shows JSON.

VariableMeaning
ALINEO_LOG_LEVELdebug, info (default), warn, error or silent.
ALINEO_LOGPer-component levels, e.g. agent=warn,alineod=debug — agent is the SDK's progress lines; debug adds turn/tool events and bookkeeping.
ALINEO_LOG_FORMATpretty or json.
ALINEO_LOG_FILEWrite to this file instead of stderr.

Docker

The image builds the whole monorepo (alineod depends on the SDK workspace packages), so build from the repo root:

docker build -f apps/alineod/Dockerfile -t alineod .

docker run --rm -p 4600:4600 \
  --add-host host.docker.internal:host-gateway \
  -e ALINEO_SERVER_URL=http://host.docker.internal:8080 \
  -e NVIDIA_API_KEY=nvapi-... \
  -v alineod-data:/data \
  alineod

Or with Compose:

NVIDIA_API_KEY=nvapi-... docker compose -f apps/alineod/docker-compose.yml up --build

The entrypoint writes /data/alineo.config.json from these variables before starting the server:

VariableDescription
ALINEO_SERVER_URLOpenSandbox server URL.
ALINEO_USE_SERVER_PROXYRoute sandbox traffic through the OpenSandbox server (true).
ALINEO_API_KEYOpenSandbox API key, if the server requires one.

All state lives under /data — both ledgers, the work directory, and the checkpoints/ directory of turn checkpoints next to the SDK ledger. Keep it on a volume: it is what a restart, a lost container, or a reboot is recovered from.

Networking

alineod talks to OpenSandbox over HTTP only. It does not need the Docker socket: OpenSandbox creates the agent containers, not alineod.

Server proxy URLs and the eip

In server-proxy mode, OpenSandbox hands back sandbox URLs built from its own configured eip. If that is http://localhost:8080, those URLs won't resolve from inside the alineod container. Either put alineod on the same Docker network as OpenSandbox with a routable eip, or run the container with --network host (the same as running alineod bare on the host).

alineod has also been run against an OpenSandbox server using gVisor (runsc) as its secure container runtime; nothing in alineod depends on the runtime.

gVisor on Docker: set --overlay2=none

By default, gVisor keeps a container's writes in its own filestore (--overlay2=root:self). docker commit, which OpenSandbox uses for snapshots and forks, can't see that filestore. So every spawned child and every restored snapshot comes back with none of the parent's files, and the agent inside can't start. Register runsc with "runtimeArgs": ["--overlay2=none"] in /etc/docker/daemon.json (see OpenSandbox#1831). Even then, /tmp is a tmpfs under gVisor and isn't carried into forks.

Admission

Creating or forking a sandbox is the slow, heavy part of a spawn — a cold provision can take a minute or more. alineod runs at most ALINEOD_ADMISSION_CONCURRENCY of them at once, and holds the rest in a queue, so a coordinator that fans out to twenty workers on a small box gets them one after another instead of starving the machine.

A spawn that has to wait records agent.admission_queued, then agent.admission_granted when it gets its slot. One that waits longer than ALINEOD_ADMISSION_TIMEOUT_MS fails loudly with agent.admission_timeout rather than hanging. The uncontended case records nothing.

Scaling

alineod is a single-node control plane. Run one instance per database. It restarts cleanly, but it does not replicate:

  • SSE fan-out is in-process. A client connected to one instance never sees events written by another.
  • Agents have no owner. On boot, an instance reattaches every live agent in the database, so two instances would both drive the same agents.
  • Notifications are delivered in-process, by the instance that recorded the event.
  • State is local SQLite. Putting the files on a shared network volume for several instances is unsupported: SQLite's locking over network filesystems is unreliable.

This is enforced, not just documented. On startup alineod takes a lease in its database, and heartbeats it every ALINEOD_LEASE_TTL_MS / 3. A second instance against the same database waits one TTL to see whether the holder is still alive, then exits with an error naming the other instance's host and pid.

A holder that died without releasing — kill -9, OOM, power loss — stops heartbeating, so the next boot takes the lease over after at most one TTL. A clean shutdown (SIGTERM, SIGINT) releases it immediately. Under a supervisor such as systemd or Docker's restart: policy, a crash therefore costs up to 15 s of extra startup, not a restart loop.

The agents themselves run in OpenSandbox containers, not in alineod.

The protocol spec

The wire contract is defined once, as Zod schemas in apps/alineod/src/schema.ts. The running daemon serves an interactive OpenAPI document at /openapi, and the schemas can be emitted to files for client generation:

cd apps/alineod
bun run spec    # writes apps/alineod/spec/openapi.json and apps/alineod/spec/events.schema.json