# Deployment
URL: /docs/alineod/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](/docs/alineod/concepts/crash-recovery)). Run **exactly one** instance per database: restarts are
safe, replicas are not (see [Scaling](#scaling)).

## Configuration [#configuration]

| Variable                            | Default                        | Description                                                                                                                                            |
| ----------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ALINEOD_PORT`                      | `4600`                         | HTTP + SSE listen port.                                                                                                                                |
| `ALINEOD_DB_PATH`                   | `./data/alineod.db`            | alineod's own state: the append-only swarm ledger and the `agents`/`handles` tables derived from it.                                                   |
| `ALINEOD_SDK_LEDGER_PATH`           | `./data/alineod-sdk-ledger.db` | The 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-work`          | Spec files handed to `spawn()`, and stored agent results.                                                                                              |
| `ALINEOD_PROMPT_INACTIVITY_MS`      | `180000`                       | If a turn's stream produces nothing for this long, alineod follows the turn by polling instead.                                                        |
| `ALINEOD_TURN_MAX_MS`               | `1800000`                      | How long a polled turn may keep running (paused time excluded) before it's ended with partial output.                                                  |
| `ALINEOD_CATCH_UP_POLL_MS`          | `2000`                         | Interval between state polls while following a turn.                                                                                                   |
| `ALINEOD_STATE_PROBE_TIMEOUT_MS`    | `5000`                         | Bound on one state poll, so an unresponsive agent can't stall alineod.                                                                                 |
| `ALINEOD_RESUME_BRIDGE_TIMEOUT_MS`  | `10000`                        | After a resume, how long to wait for the agent's harness to answer before restarting it.                                                               |
| `ALINEOD_SUBTREE_MEMBER_TIMEOUT_MS` | `30000`                        | Bound 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_CONCURRENCY`     | host vCPUs − 1 (at least 1)    | How many sandboxes alineod creates or forks at once. Spawns beyond it wait for a slot. See [Admission](#admission).                                    |
| `ALINEOD_ADMISSION_TIMEOUT_MS`      | `600000`                       | How long a spawn may wait for a slot before it fails with `agent.admission_timeout`.                                                                   |
| `ALINEOD_REATTACH_RETRY_DELAY_MS`   | `1000`                         | On boot, how long to wait before retrying a failed reattach once — see [the restore ladder](/docs/alineod/concepts/crash-recovery#the-restore-ladder). |
| `ALINEOD_LEASE_TTL_MS`              | `15000`                        | How long the instance lease outlives its last heartbeat. See [Scaling](#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](/docs/alineod/concepts/memory#configuration).
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 [#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](/docs/alineod/api-reference/events);
* 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.

| Variable            | Meaning                                                                                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALINEO_LOG_LEVEL`  | `debug`, `info` (default), `warn`, `error` or `silent`.                                                                                     |
| `ALINEO_LOG`        | Per-component levels, e.g. `agent=warn,alineod=debug` — `agent` is the SDK's progress lines; `debug` adds turn/tool events and bookkeeping. |
| `ALINEO_LOG_FORMAT` | `pretty` or `json`.                                                                                                                         |
| `ALINEO_LOG_FILE`   | Write to this file instead of stderr.                                                                                                       |

## Docker [#docker]

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

```bash
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:

```bash
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:

| Variable                  | Description                                                    |
| ------------------------- | -------------------------------------------------------------- |
| `ALINEO_SERVER_URL`       | OpenSandbox server URL.                                        |
| `ALINEO_USE_SERVER_PROXY` | Route sandbox traffic through the OpenSandbox server (`true`). |
| `ALINEO_API_KEY`          | OpenSandbox 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 [#networking]

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

<Callout type="warn" title="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).
</Callout>

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

<Callout type="warn" title="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](https://github.com/opensandbox-group/OpenSandbox/issues/1831)). Even then,
  `/tmp` is a tmpfs under gVisor and isn't carried into forks.
</Callout>

## Admission [#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 [#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-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:

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