aidd ships as a single-process container: the compiled aidd-web binary serves the SPA, API, and
WebSockets on port 3210, launches the sibling aidd CLI binary for runs, and embeds its database
migrations (they run at boot). There is no nginx or supervisord inside the image; TLS, security
headers, and rate limiting belong to an edge proxy (see Reverse proxy).
The image also bakes in the four agent-backend CLIs (claude, codex, opencode, kilo),
pinned via AGENT_CLI_VERSIONS in scripts/docker-image.ts.
The in-process native/ollama backend needs no CLI at all. The container runs as the non-root
user aidd (uid 1000), required because claude --dangerously-skip-permissions refuses to run
as root.
For the standalone (non-Docker) deployment, see deployment.md.
bun run docker:build # build ghcr.io/nomadicdaddy/aidd:{latest,<version>}
bun run smoke:docker # end-to-end smoke against a throwaway container
docker compose up -d # dev compose: state under ./.docker/
docker logs aidd-dev # first boot prints the web API auth token ONCEbun run deploy:docker -- --appdata-root /opt/appdata --projects-root /srv/projects --upThis pre-authors the deploy tree (spernakit methodology) so the container's first-boot bootstrap
is skipped: ${APPDATA_ROOT}/aidd/home/.aidd/config.json with a generated web.authToken,
data/ and logs/ directories, a copy of docker-compose.production.yml, and compose.vars.
Provider keys go in a compose.env file next to the compose file (see
compose.env.example).
Manual equivalent: copy compose.vars.example → compose.vars, edit, then
docker compose -f docker-compose.production.yml --env-file compose.vars up -d.
| Container path | Purpose |
|---|---|
/home/aidd |
~/.aidd/config.json, agent-CLI credentials (~/.claude, ~/.codex, …), ~/.gitconfig, caches |
/app/data |
aidd-panel.db, run data, backups/ (entrypoint pre-start copies, keeps 5) |
/app/logs |
backend.pid, backend logs (path is fixed relative to the binary; rootfs is read-only) |
/projects |
Project working trees aidd manages; must stay path-stable: /projects/... paths persist in the web DB |
On Linux hosts the ${APPDATA_ROOT}/aidd tree must be owned by uid 1000. Docker Desktop
(Windows/macOS) bind mounts handle ownership transparently.
aidd is JSON-only: there are no environment overrides for web settings. The container reads
/home/aidd/.aidd/config.json (the home volume). If none exists, the entrypoint authors one
(hostname 0.0.0.0, allowRemote true, generated authToken, allowedRoots ["/projects"]) and
prints the token once in docker logs. Edit the mounted file and restart to change settings. Do
not change web.port inside the container (the image HEALTHCHECK probes 3210); remap the host
port with AIDD_PORT instead. See configuration.md for all keys.
Container env reaches launched agent CLIs only when allowlisted in
shared/src/subprocess-env.ts; anything else is silently
dropped.
| Backend | API-key auth (compose.env) | Subscription auth |
|---|---|---|
claude-code |
ANTHROPIC_API_KEY |
docker exec -it <container> claude /login (persists in home volume) |
codex |
OPENAI_API_KEY |
docker exec -it <container> codex login |
opencode/kilo |
provider keys per their config | their respective login flows |
native/ollama |
NATIVE_API_KEY/NATIVE_BASE_URL etc. |
n/a (in-process; point NATIVE_BASE_URL at an Ollama host) |
Git identity for run commits: set GIT_USER_NAME/GIT_USER_EMAIL in compose.env (entrypoint
writes ~/.gitconfig on first boot), or manage ~/.gitconfig in the home volume yourself.
Upgrade: bump APP_VERSION in compose.vars, then docker compose --env-file compose.vars pull
and up -d. Volumes (config, credentials, DB, projects) carry over; migrations run at boot.
Rollback: revert APP_VERSION to the previous tag and up -d. Embedded migrations are
forward-only. If the newer version migrated the schema, restore the matching pre-start backup
from data/backups/ (enabled by AIDD_BACKUP_ON_START=1, the default in
docker-compose.production.yml) before starting the older image.
The production compose publishes the panel loopback-only (127.0.0.1:${AIDD_PORT}). For remote
access, front it with a TLS proxy; examples in docker/examples/.
Two auth facts matter at the edge:
- Only direct loopback callers are token-exempt. Any forwarded request (carrying
X-Forwarded-*) must presentAuthorization: Bearer <web.authToken>. The browser UI attaches it automatically after login, but proxy health probes must send it explicitly. - CSP, rate limiting, and security headers are the proxy's job; the container does not duplicate them (divergence from the spernakit template, which ships in-container nginx for this).
- Container restart kills in-flight runs. Detached runs do not outlive the container; on the
next boot the heartbeat watcher reaps orphaned runs to
failed. Drain or wait for runs beforedocker compose stopwhen possible (stop_grace_periodis 30s). - Resource sizing. Each concurrent run spawns a full agent CLI (node process). Size
AIDD_MEM_LIMITtogether withweb.maxConcurrentRuns(default 2); 4g/2cpu is a workable starting point. - Image size is ~3 GB (node runtime + four agent CLIs + compiled binaries + catalogs). The
agent CLIs update frequently; bump
AGENT_CLI_VERSIONSinscripts/docker-image.tsand rebuild to ship newer ones. In-image self-update is disabled (read-only npm layer; the entrypoint also writes~/.claude/settings.jsonwithautoUpdates: falseon first boot). - CPU baseline. The binaries use the
bun-linux-x64-moderntarget (AVX2-class). Very old x64 hosts would need abaselinebuild target added toscripts/lib/standalone/constants.ts.
bun run smoke:docker builds the image, composes it up with throwaway volumes
(.docker/smoke/), and verifies: health, SPA, 401 without token, tokened API, /projects mount
discovery, a full simulated run through the detached spawn chain, and a graceful stop within the
grace period. -- --with-claude additionally launches a real claude-code run (requires
ANTHROPIC_API_KEY, spends tokens) and guards against the claude-as-root refusal regression.
-- --keep leaves everything up for debugging; -- --no-build reuses the existing aidd:dev
image.