Skip to main content

Documentation Index

Fetch the complete documentation index at: https://qwibitai-nanoclaw-8-mintlify-container-config-db-1778268498.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

NanoClaw runs agents in isolated Linux containers to provide security through OS-level process and filesystem isolation. In v2, the container runtime uses a two-database IO model instead of stdin/stdout piping, and the agent-runner runs on Bun instead of Node.js.

Runtime abstraction

All runtime-specific logic lives in src/container-runtime.ts:
  • Docker (default) — cross-platform support (macOS, Linux, Windows via WSL2)
  • Apple Container (macOS only) — lightweight native runtime
The runtime binary is specified by CONTAINER_RUNTIME_BIN:
export const CONTAINER_RUNTIME_BIN = 'docker';

Apple Container vs Docker

Apple Container is Apple’s native virtualization framework (macOS 15+). It runs Linux containers without a VM layer like Docker Desktop.

When to use Apple Container

  • You’re on macOS 15 (Sequoia) or later
  • You want to avoid installing Docker Desktop
  • You want faster container startup

When to stick with Docker

  • You’re on Linux or Windows (WSL2)
  • You need cross-platform parity
  • You’re deploying to a production server

Key differences

DockerApple Container
Binarydockercontainer
Bind mounts-v host:container:ro--mount type=bind,source=...,target=...,readonly
Stop commanddocker stop -t 1 namecontainer stop name
Health checkdocker infocontainer system status
PlatformmacOS, Linux, Windows (WSL2)macOS 15+ only

Switching runtimes

Run the /convert-to-apple-container skill in Claude Code. To revert, use git revert.

Container image

The agent container is built from container/Dockerfile and includes:
  • Node.js 22 — base image runtime
  • Bun (pinned to 1.3.12) — runs agent-runner TypeScript directly (no compilation)
  • Chromium — browser automation via agent-browser
  • Claude Code SDK@anthropic-ai/claude-code installed globally via pnpm
  • tini — PID 1 signal forwarding (ensures outbound.db writes finalize on SIGTERM)
  • pnpm (via corepack) — for global Node CLI installs
  • System toolscurl, git, ca-certificates, unzip
  • Optional CJK fontsfonts-noto-cjk (~200 MB, opt-in via INSTALL_CJK_FONTS=true)

Key design decisions

  • Source is NOT baked in/app/src is a read-only bind mount from the host. Source changes never require an image rebuild.
  • only-built-dependencies allowlist in .npmrc for agent-browser and @anthropic-ai/claude-code
  • Runs as node user (non-root) with /workspace/group as working directory
  • Entrypoint: tini -> entrypoint.sh -> exec bun run /app/src/index.ts

Building the image

./container/build.sh

Per-agent-group images

Agent groups can specify custom packages in their container config (stored in the container_configs DB table). The host builds a derived Docker image:
  • Tag: derived from the checkout-scoped base image and agent group
  • Built on top of nanoclaw-agent-v2-<slug>:latest
  • Adds custom apt and npm packages
  • Resulting image tag is written back to the container_configs.image_tag column

Container configuration storage

Per-agent-group runtime config lives in the container_configs table in the central DB. The legacy groups/<folder>/container.json file is now a materialized view — written by the host at spawn time, read by the container at startup. The container has no idea the DB exists; nothing inside the container changed.

Schema

The container_configs table has one row per agent group, with both scalar and JSON columns:
ColumnTypePurpose
agent_group_idTEXT (PK, FK)References agent_groups.id (cascades on delete)
providerTEXTAgent provider override (claude, opencode, etc.)
modelTEXTModel name (e.g., claude-sonnet-4-6)
effortTEXTReasoning effort hint
image_tagTEXTPersisted Docker image tag for per-group builds
assistant_nameTEXTDisplay name in system prompt
max_messages_per_promptINTEGEROverride for MAX_MESSAGES_PER_PROMPT
skillsTEXT (JSON)"all" or ["skill1", ...]
mcp_serversTEXT (JSON)Record<string, McpServerConfig>
packages_aptTEXT (JSON)string[] of apt packages
packages_npmTEXT (JSON)string[] of npm packages
additional_mountsTEXT (JSON)AdditionalMountConfig[]
cli_scopeTEXTdisabled | group (default) | global — controls in-container ncl access
updated_atTEXTISO timestamp

Backfill

On startup, the host runs a one-time backfill that seeds container_configs rows from any existing groups/<folder>/container.json files (and the legacy agent_groups.agent_provider column). The backfill is idempotent — it skips groups that already have a row.

Provider cascade

Provider resolution simplified from a 3-step cascade to 2 steps:
sessions.agent_provider
  → container_configs.provider
  → 'claude'
The legacy agent_groups.agent_provider column is retained for backwards compat but no longer participates in resolution and is no longer exposed via ncl groups. Configure provider via ncl groups config update --id <group-id> --provider <name>.

Updating config

All writes go through the DB layer. Multi-word verbs accept either spaces (preferred) or dashes — ncl groups config get and ncl groups config-get both work.
  • ncl groups config update — change scalar fields (--provider, --model, --effort, --image-tag, --assistant-name, --max-messages-per-prompt, --cli-scope)
  • ncl groups config add-mcp-server / config remove-mcp-server — manage MCP servers
  • ncl groups config add-package / config remove-package — manage apt/npm packages
  • Self-mod approvals (install_packages, add_mcp_server) — write to DB instead of file
  • ncl groups config get — view current config (open access; others require approval)
Config CLI ops only write to the DB; restart is now a separate command — see “Explicit restart” below. The host helper restartAgentGroupContainers() is invoked by self-mod approvals to apply config changes.

CLI scope

cli_scope controls what an in-container agent can do via ncl:
ValueBehavior
disabledAgent never learns about ncl (the instructions section is excluded from the composed CLAUDE.md). The host CLI dispatcher rejects any cli_request.
group (default)Agent can call groups, sessions, destinations, members only, scoped to its own agent group. --id, --agent_group_id, and --group args are auto-filled. Cross-group reads are rejected post-handler; help output reflects the scope. cli_scope arg is blocked outright.
globalUnrestricted — current behavior. Set automatically for owner agent groups by init-first-agent.
Enforcement is host-side only — no image rebuild or env var needed.

Explicit restart

ncl groups restart --id <group-id> [--rebuild] [--message <text>]
  • Kills running containers for the agent group; without --message, they come back on the next inbound message
  • With --message, an on_wake row is written to inbound.db and the container respawns immediately via the onExit callback
  • --rebuild forces an image rebuild before respawn (useful after package changes); package commands no longer trigger a build implicitly
  • Called from inside a container, --id is auto-filled and only the calling session is restarted
The on_wake flag on messages_in ensures wake messages are delivered only on the new container’s first poll iteration. Without it, the dying container — still in its SIGTERM grace window — could steal the message before exiting. killContainer accepts an onExit callback that fires after the process actually exits, guaranteeing race-free respawn.

Two-database IO model

In v2, all communication between host and container uses two SQLite databases per session. There is no stdin/stdout piping, no IPC files, and no output markers.

inbound.db (host writes, container reads)

TablePurpose
messages_inInbound messages, tasks, system notifications
deliveredTracks delivery outcomes for outbound message IDs
destinationsLive destination map (channels and other agents)
session_routingDefault reply routing (channel_type, platform_id, thread_id)

outbound.db (container writes, host reads)

TablePurpose
messages_outOutbound messages with deliver_after and recurrence
processing_ackTracks which inbound messages the container has processed
session_statePersistent key/value store (e.g., SDK session ID for resume)
container_stateTool-in-flight state for stuck-detection

Cross-mount invariants

Three invariants are critical for correctness:
  1. journal_mode=DELETE — WAL’s mmapped -shm doesn’t refresh across Docker mounts
  2. Host opens-writes-closes per operation — closing invalidates the container’s page cache
  3. One writer per file — DELETE-mode journal unlink isn’t atomic across the mount

Container lifecycle

Spawning containers

Containers are spawned by the spawnContainer function. Wake calls are deduplicated via an in-flight promise map.
1

Materialize container config

The host reads the agent group’s row from the container_configs DB table and writes it as groups/<folder>/container.json. This file is a materialized view — the DB is the source of truth, and the file is regenerated on every spawn so the runner always sees fresh config. Provider contributions are resolved from this config.
2

Build volume mounts

Mounts are built based on the session, agent group, and validated additional mounts.
3

Sync skill symlinks

Skill symlinks at /home/node/.claude/skills/ are updated to point to /app/skills/{name}. These are dangling on the host but valid inside the container.
4

Compose CLAUDE.md

A composed CLAUDE.md is regenerated and mounted read-only.
5

Spawn container

Docker container is spawned with tini as PID 1.
6

Track container state

Session is marked as running in the central DB.

Volume mounts

PathContainer pathModePurpose
Session folder/workspaceRWinbound.db, outbound.db, outbox/, inbox/
Agent group folder/workspace/agentRWWorking files
container.json/workspace/agent/container.jsonROMaterialized from DB at spawn time
Composed CLAUDE.md/workspace/agent/CLAUDE.mdRORegenerated each spawn
Global memory/workspace/globalROShared instructions
Agent-runner source/app/srcROBind mount from host
Container skills/app/skillsROShared skill definitions
Claude SDK state/home/node/.claudeRWSDK state + skill symlinks
Additional mounts/workspace/extra/{name}Per-configValidated against allowlist
Provider mountsVariousPer-providerProvider-contributed

Timeouts and stale detection

Containers have two timeout/detection mechanisms:
  1. Container timeout — maximum runtime before force kill (default: 30 minutes)
  2. Stale detection — host sweep checks .heartbeat mtime and processing_ack age to detect stuck containers

Container shutdown

  • killContainer(sessionId, reason) stops the container via docker stop, falls back to SIGKILL
  • On close/error, the session is marked stopped and typing indicators are cleared

Credential injection

The OneCLI SDK’s applyContainerConfig() configures each container’s network to route through the vault:
const onecliApplied = await onecli.applyContainerConfig(args, {
  addHostMapping: false,
  agent: agentIdentifier,
});
  • Injects HTTPS_PROXY and CA certs into Docker args
  • All container API calls route through the vault
  • No raw API keys are passed via environment variables
  • Each agent group gets its own agentIdentifier for credential scoping

Debugging containers

docker ps --filter name=nanoclaw-
docker logs nanoclaw-{session-id}
docker inspect nanoclaw-{session-id} | jq '.[0].Mounts'
docker exec -it nanoclaw-{session-id} /bin/bash
docker stop -t 1 nanoclaw-{session-id}
Last modified on May 9, 2026