Skip to content

Commit 57085af

Browse files
ZD Studiosclaude
andcommitted
feat: Docker image + compose for the whole nine-project stack
One container runs everything: Python/uv, Node 22/pnpm, Bun, the Claude CLI, ripgrep and yt-dlp, driven by `aios` exactly as on the host. docker compose up -d --build -> http://localhost:8787 Containerising this is more than convenience. AIOS gives every agent full control of the machine it runs on; in a container that machine is a blast radius you can delete. Guardrails and the hub token still apply inside, so it's defence in depth rather than a swap: no-new-privileges, SYS_ADMIN/NET_ADMIN dropped, and the hub binds 0.0.0.0 only because non-loopback requests need AIOS_HUB_TOKEN — which the entrypoint mints on first boot and prints with the URL. State survives rebuilds: .aios (memory, tasks, flows, audit) is a named volume, .env / aios.config.yaml bind-mount from the repo, and ~/.claude mounts through so the claude-code service inherits a host login (or run `aios claude-login` inside). Two bugs found by building it for real: - Installing from package.json alone silently produced EMPTY node_modules for opencode and openclaw — both are workspace monorepos needing full source to resolve, and the `|| true` hid it. The image now lets `aios setup` install, since it already encodes every workaround (--ignore-scripts for native postinstalls, NODE_OPTIONS heap sizing for openclaw's build, uv sync). - The image came out 16.2GB. pnpm's content-addressable store was 4.7GB and the caches ~1GB; because pnpm hardlinks node_modules into the store, dropping it frees the space while the linked files stay intact — verified in-container before baking it in, and done in the same layer so the bytes actually go. Also fixes a gap this exposed: `aios update` mounted LifeOS/OpenUI/AIOS skills but not the caveman, ponytail and ruflo ones that `aios setup` mounts, so updating left those stale. Both paths now mount the same set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 26ae27e commit 57085af

7 files changed

Lines changed: 382 additions & 0 deletions

File tree

.dockerignore

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# The repo carries nine vendored projects; without this the build context is
2+
# gigabytes of node_modules and git history that the image rebuilds anyway.
3+
.git
4+
**/.git
5+
**/node_modules
6+
**/.venv
7+
**/venv
8+
**/__pycache__
9+
**/*.pyc
10+
**/.pytest_cache
11+
**/.mypy_cache
12+
**/.ruff_cache
13+
**/dist
14+
**/build
15+
**/.next
16+
**/.turbo
17+
**/target
18+
19+
# Local state and secrets — mounted at runtime, never baked into the image.
20+
.aios/
21+
.env
22+
*.env
23+
!.env.example
24+
aios.config.yaml
25+
26+
# Host-specific junk
27+
.claude/
28+
.vscode/
29+
.idea/
30+
*.log
31+
*.zip
32+
*.tar.gz
33+
Thumbs.db
34+
.DS_Store
35+
36+
# Docs/media that don't affect the runtime
37+
docs/aios/inventory.md
38+
**/*.gif
39+
**/*.mp4

Dockerfile

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# syntax=docker/dockerfile:1
2+
# ─────────────────────────────────────────────────────────────────────────────
3+
# The AI OS — containerised.
4+
#
5+
# Nine open-source AI projects across three runtimes (Python/uv, Node/pnpm, Bun)
6+
# in one image, driven by `aios`.
7+
#
8+
# docker compose up -d then open http://localhost:8787
9+
#
10+
# Why a container is the *right* place to run this: The AI OS gives every agent
11+
# full control of the machine it runs on — shell, filesystem, network. On your
12+
# laptop that means your laptop. Here it means this container, which is a blast
13+
# radius you can delete. Guardrails and the hub token still apply inside.
14+
# ─────────────────────────────────────────────────────────────────────────────
15+
16+
# ---------- stage 1: toolchains ---------------------------------------------
17+
FROM python:3.12-slim-bookworm AS base
18+
19+
ENV DEBIAN_FRONTEND=noninteractive \
20+
PYTHONUNBUFFERED=1 \
21+
PYTHONDONTWRITEBYTECODE=1 \
22+
NODE_MAJOR=22 \
23+
PNPM_HOME=/usr/local/pnpm \
24+
BUN_INSTALL=/usr/local/bun \
25+
PATH=/usr/local/pnpm:/usr/local/bun/bin:/root/.local/bin:$PATH
26+
27+
# Node 22 (openclaw + Next.js need >=20), plus the build deps native modules want.
28+
RUN apt-get update && apt-get install -y --no-install-recommends \
29+
ca-certificates curl git unzip xz-utils ripgrep procps tini \
30+
build-essential python3-dev \
31+
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
32+
&& apt-get install -y --no-install-recommends nodejs \
33+
&& npm install -g pnpm@9 \
34+
&& curl -fsSL https://bun.sh/install | bash \
35+
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
36+
&& apt-get purge -y --auto-remove \
37+
&& rm -rf /var/lib/apt/lists/* /tmp/*
38+
39+
# Claude Code CLI — powers the claude-code service (subscription auth, no API key).
40+
RUN npm install -g @anthropic-ai/claude-code || \
41+
echo "claude CLI unavailable at build time; install later with: npm i -g @anthropic-ai/claude-code"
42+
43+
# yt-dlp powers YouTube transcripts (the caption endpoint needs a PO token).
44+
RUN pip install --no-cache-dir --upgrade yt-dlp
45+
46+
# ---------- stage 2: runtime ------------------------------------------------
47+
FROM base AS runtime
48+
WORKDIR /aios
49+
50+
LABEL org.opencontainers.image.title="The AI OS" \
51+
org.opencontainers.image.description="Nine open-source AI projects. One operating system." \
52+
org.opencontainers.image.source="https://github.com/ZDStudios/AIOS" \
53+
org.opencontainers.image.licenses="MIT"
54+
55+
COPY . /aios
56+
57+
# `aios setup` does the installing rather than a hand-written RUN, because it
58+
# already encodes every per-project workaround: --ignore-scripts for the native
59+
# postinstalls that need VS Build Tools, NODE_OPTIONS heap sizing for openclaw's
60+
# build, and uv sync for the Python projects. Installing from package.json alone
61+
# silently produces empty node_modules — opencode and openclaw are workspace
62+
# monorepos and need their full source present to resolve.
63+
RUN set -eux; \
64+
cp -n aios.config.example.yaml aios.config.yaml 2>/dev/null || true; \
65+
chmod +x aios docker/entrypoint.sh 2>/dev/null || true; \
66+
mkdir -p /aios/.aios; \
67+
python aios.py setup --non-interactive --skip-keys --skip-tools --skip-wire \
68+
|| echo "setup reported warnings — see 'docker compose exec aios aios doctor'"; \
69+
# Same layer as the install, or the image keeps the deleted bytes anyway.
70+
# pnpm hardlinks node_modules into its content-addressable store, so dropping
71+
# the store frees ~4.7GB while the linked files stay intact (verified).
72+
rm -rf /usr/local/pnpm/store /root/.cache /root/.npm /tmp/* /var/tmp/*; \
73+
find /aios -type d -name ".git" -prune -exec rm -rf {} + 2>/dev/null || true; \
74+
apt-get purge -y --auto-remove build-essential python3-dev 2>/dev/null || true; \
75+
rm -rf /var/lib/apt/lists/*
76+
77+
# Login shells (docker exec ... bash) re-read /etc/profile and would otherwise
78+
# lose the toolchain PATH baked into ENV.
79+
RUN printf 'export PATH=/usr/local/pnpm:/usr/local/bun/bin:/root/.local/bin:$PATH\n' \
80+
> /etc/profile.d/aios-path.sh
81+
82+
ENV AIOS_ROOT=/aios \
83+
AIOS_HUB_HOST=0.0.0.0 \
84+
AIOS_IN_DOCKER=1 \
85+
AIOS_NO_UPDATE_CHECK=1
86+
87+
# 8787 hub · 4096 opencode · 9119 hermes · 18789 openclaw
88+
# 4788 crewai · 8000 claude-code · 8791/8792 embed proxies
89+
EXPOSE 8787 4096 9119 18789 4788 8000 8791 8792
90+
91+
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=5 \
92+
CMD curl -fsS http://127.0.0.1:8787/health || exit 1
93+
94+
# tini reaps the process trees aios spawns, so stopping the container doesn't
95+
# strand bun/node/uv children as zombies.
96+
ENTRYPOINT ["/usr/bin/tini", "--", "/aios/docker/entrypoint.sh"]
97+
CMD ["start"]

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ curl -fsSL https://raw.githubusercontent.com/ZDStudios/AIOS/main/install.sh | ba
6666
irm https://raw.githubusercontent.com/ZDStudios/AIOS/main/install.ps1 | iex
6767
```
6868

69+
**Docker** — no toolchains on your machine, and the agents' full control is scoped to a container you can delete:
70+
71+
```bash
72+
git clone https://github.com/ZDStudios/AIOS.git && cd AIOS
73+
cp .env.example .env # add a model key, or use your Claude subscription
74+
docker compose up -d --build # then open http://localhost:8787
75+
```
76+
77+
Full guide: **[docs/DOCKER.md](docs/DOCKER.md)**.
78+
6979
The installer **asks for root once, up front** (sudo on Linux/macOS, a UAC prompt on Windows) — because
7080
the agents get full control of the machine and setup registers a start-on-boot service and a global
7181
`aios` command. It then clones the repo to `~/AIOS`, installs the toolchains (**uv, bun, pnpm, Node**)

aios.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,8 @@ def cmd_update(args):
15931593
mount_openui(cfg)
15941594
if cfg_get(cfg, "skills.mount", True):
15951595
mount_aios_skills(cfg)
1596+
mount_agent_modes(cfg) # caveman + ponytail — same set setup mounts
1597+
mount_ruflo(cfg)
15961598
# Restart any services that are currently running so the new code takes effect.
15971599
specs = service_specs(cfg)
15981600
running = [s for s in specs if (read_pid(s) and pid_alive(read_pid(s)["pid"]))]

docker-compose.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# The AI OS — one container, nine AI projects.
2+
#
3+
# docker compose up -d --build build and start
4+
# docker compose logs -f watch it come up (first boot takes a while)
5+
# docker compose exec aios aios status
6+
# docker compose exec aios aios claude-login use your Claude subscription
7+
#
8+
# Then open http://localhost:8787 — the token is printed in the logs.
9+
10+
services:
11+
aios:
12+
build:
13+
context: .
14+
dockerfile: Dockerfile
15+
image: ghcr.io/zdstudios/aios:latest
16+
container_name: aios
17+
restart: unless-stopped
18+
ports:
19+
- "8787:8787" # ★ Control Room (the only one you normally need)
20+
- "4096:4096" # opencode
21+
- "9119:9119" # hermes
22+
- "18789:18789" # openclaw gateway (+ openclaw-os dashboard)
23+
- "4788:4788" # CrewAI
24+
- "8000:8000" # claude-code API
25+
- "8791:8791" # openclaw embed proxy
26+
- "8792:8792" # hermes embed proxy
27+
environment:
28+
# One provider key powers every agent. Leave blank and use your Claude
29+
# subscription instead (docker compose exec aios aios claude-login).
30+
AIOS_LLM_PROVIDER: "${AIOS_LLM_PROVIDER:-openrouter}"
31+
AIOS_LLM_API_KEY: "${AIOS_LLM_API_KEY:-}"
32+
AIOS_DEFAULT_MODEL: "${AIOS_DEFAULT_MODEL:-anthropic/claude-opus-4.6}"
33+
# Raises the GitHub API limit for supervised updates (60/hr -> 5000/hr).
34+
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
35+
AIOS_HUB_HOST: "0.0.0.0"
36+
volumes:
37+
# Durable state: memory, tasks, flows, audit log, pidfiles, logs.
38+
- aios-state:/aios/.aios
39+
# Secrets + config on the host, so rebuilds don't lose them.
40+
- ./.env:/aios/.env
41+
- ./aios.config.yaml:/aios/aios.config.yaml
42+
# Your Claude CLI login, so the claude-code service is authenticated.
43+
# Comment this out if you'd rather log in inside the container.
44+
- ${HOME:-~}/.claude:/root/.claude
45+
# Agents get full control of this container. That's the point — and the
46+
# container boundary is what makes it a reasonable thing to hand them.
47+
# These caps are dropped anyway; nothing AIOS runs needs them.
48+
cap_drop:
49+
- SYS_ADMIN
50+
- NET_ADMIN
51+
security_opt:
52+
- no-new-privileges:true
53+
healthcheck:
54+
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8787/health"]
55+
interval: 30s
56+
timeout: 5s
57+
start_period: 180s
58+
retries: 5
59+
stop_grace_period: 30s
60+
61+
volumes:
62+
aios-state:

docker/entrypoint.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# The AI OS container entrypoint.
3+
#
4+
# start (default) render config, bring the stack up, stay in the foreground
5+
# setup re-run setup (deps/build/wire) then exit
6+
# shell a shell inside the container
7+
# <anything else> passed straight to `aios`
8+
set -euo pipefail
9+
10+
cd /aios
11+
export PATH="/usr/local/pnpm:/usr/local/bun/bin:/root/.local/bin:$PATH"
12+
13+
c(){ printf '\033[1;36m%s\033[0m\n' "$*"; }
14+
ok(){ printf '\033[1;32mOK\033[0m %s\n' "$*"; }
15+
warn(){ printf '\033[1;33m!\033[0m %s\n' "$*"; }
16+
17+
# .aios/ and .env come from volumes, so a fresh container inherits your state.
18+
mkdir -p /aios/.aios
19+
[ -f /aios/aios.config.yaml ] || cp aios.config.example.yaml /aios/aios.config.yaml
20+
21+
# The hub binds 0.0.0.0 so the port publish works. That is only safe because
22+
# non-loopback requests need the token — mint it before anything starts.
23+
if [ ! -f /aios/.env ] || ! grep -q '^AIOS_HUB_TOKEN=' /aios/.env 2>/dev/null; then
24+
python aios.py token >/dev/null 2>&1 || true
25+
fi
26+
TOKEN="$(grep -m1 '^AIOS_HUB_TOKEN=' /aios/.env 2>/dev/null | cut -d= -f2- || true)"
27+
28+
case "${1:-start}" in
29+
start)
30+
c "The AI OS — starting in Docker"
31+
if [ -z "${AIOS_LLM_API_KEY:-}" ] && ! grep -q '^AIOS_LLM_API_KEY=.\+' /aios/.env 2>/dev/null; then
32+
warn "no model API key set — the stack will run, but agents can't answer."
33+
warn "Set one in .env / compose env, or use your Claude subscription:"
34+
warn " docker compose exec aios aios claude-login"
35+
fi
36+
37+
# Render .env + config into every project, mount skills, apply exec policy.
38+
python aios.py setup --non-interactive --skip-tools --skip-install \
39+
|| warn "setup finished with warnings (run: docker compose exec aios aios doctor)"
40+
41+
python aios.py start all --timeout 180 || warn "some services did not report healthy"
42+
python aios.py status || true
43+
44+
echo
45+
ok "Control Room: http://localhost:8787/${TOKEN:+?token=$TOKEN}"
46+
[ -n "$TOKEN" ] && echo " hub token: $TOKEN"
47+
echo
48+
49+
# PID 1 must stay alive for the container to stay up. Watch the hub and
50+
# surface its log, so `docker logs` shows something useful.
51+
touch /aios/.aios/logs/hub.log 2>/dev/null || mkdir -p /aios/.aios/logs
52+
tail -n 0 -F /aios/.aios/logs/*.log 2>/dev/null &
53+
while true; do
54+
if ! curl -fsS http://127.0.0.1:8787/health >/dev/null 2>&1; then
55+
warn "hub stopped responding — restarting it"
56+
python aios.py restart hub || true
57+
fi
58+
sleep 20
59+
done
60+
;;
61+
setup) shift; exec python aios.py setup --non-interactive "$@" ;;
62+
shell) exec /bin/bash ;;
63+
*) exec python aios.py "$@" ;;
64+
esac

0 commit comments

Comments
 (0)