Skip to content

Commit 730756f

Browse files
jkyberneeesclaude
andcommitted
docs: add runnable docker/ examples for the Compose guide
Ship the files referenced by DOCKER_COMPOSE_USER_GUIDE.md as a ready-to-run docker/ folder: multi-stage Dockerfile, a docker-compose.yml with four profiles (restricted, godmode, telegram-restricted, telegram-godmode), the two permission policy files, a .env.example, and a folder README. Add a root .dockerignore and gitignore docker/.env + docker/workspace scratch. Validated with `docker compose config` across all four profiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c850766 commit 730756f

9 files changed

Lines changed: 327 additions & 0 deletions

File tree

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Keep the build context (repo root) small for `docker build`.
2+
.git
3+
bin/
4+
*.test
5+
coverage.out
6+
odek-bin
7+
.plans/
8+
.hermes/
9+
docker/.env
10+
docker/workspace/
11+
benchmark/

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ odek-bin
77
.hermes/
88
notify-channel-proposal.md
99
internal/mcpclient/testdata/fakeserver
10+
11+
# Docker Compose examples — local secrets and agent scratch dir
12+
docker/.env
13+
docker/workspace/*
14+
!docker/workspace/.gitkeep

docker/.env.example

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copy this file to `.env` and fill in your values.
2+
# cp .env.example .env
3+
# DO NOT COMMIT .env — it holds secrets.
4+
5+
# ── Model provider ───────────────────────────────────────────────────────
6+
# API key (ODEK_API_KEY wins; DEEPSEEK_API_KEY / OPENAI_API_KEY are fallbacks)
7+
ODEK_API_KEY=sk-your-key-here
8+
9+
# Model + provider endpoint (pick one)
10+
ODEK_MODEL=deepseek-v4-flash
11+
ODEK_BASE_URL=https://api.deepseek.com/v1
12+
13+
# OpenAI:
14+
# ODEK_MODEL=gpt-4o
15+
# ODEK_BASE_URL=https://api.openai.com/v1
16+
17+
# Anthropic:
18+
# ODEK_MODEL=claude-opus-4-1
19+
# ODEK_BASE_URL=https://api.anthropic.com/v1
20+
21+
# We run unsandboxed on purpose (the container IS the boundary), so silence
22+
# the "running without --sandbox" startup warning that run/repl print.
23+
ODEK_SUPPRESS_SANDBOX_WARNING=1
24+
25+
# ── Telegram bot (only for the telegram-* profiles) ──────────────────────
26+
# Get a token from @BotFather; restrict to YOUR chat/user id.
27+
# ODEK_TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token
28+
# ODEK_TELEGRAM_ALLOWED_CHATS=11111111 # comma-separated chat IDs
29+
# ODEK_TELEGRAM_ALLOWED_USERS=11111111 # comma-separated user IDs (optional)
30+
# ODEK_TELEGRAM_DAILY_TOKEN_BUDGET=2000000 # optional cost cap; 0/unset = unlimited
31+
# ODEK_TELEGRAM_SESSION_TTL_HOURS=24 # optional
32+
# ODEK_TELEGRAM_HEALTH_ADDR=0.0.0.0:9090 # optional GET /health endpoint

docker/Dockerfile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# syntax=docker/dockerfile:1
2+
#
3+
# Build context MUST be the repository root (see docker-compose.yml:
4+
# build: { context: .., dockerfile: docker/Dockerfile }).
5+
6+
# ---- build stage ----
7+
FROM golang:1.25-alpine AS build
8+
WORKDIR /src
9+
10+
# Cache modules first
11+
COPY go.mod go.sum ./
12+
RUN go mod download
13+
14+
# Build the static binary (mirrors the Makefile `build` target)
15+
COPY . .
16+
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /out/odek ./cmd/odek
17+
18+
# ---- runtime stage ----
19+
FROM alpine:latest
20+
# Tooling the agent commonly needs inside the sandbox container.
21+
# Trim or extend this list to taste.
22+
RUN apk add --no-cache ca-certificates git bash coreutils curl jq
23+
24+
# Run as a non-root user — defense in depth even inside the container.
25+
# Pre-create ~/.odek owned by the user so a mounted named volume (used for
26+
# Telegram session state) inherits uid 1000 ownership and is writable.
27+
RUN adduser -D -u 1000 odek \
28+
&& mkdir -p /home/odek/.odek /workspace \
29+
&& chown -R odek:odek /home/odek/.odek /workspace
30+
31+
COPY --from=build /out/odek /usr/local/bin/odek
32+
33+
# Docker does NOT set $HOME from USER, but Odek resolves ~/.odek via $HOME.
34+
# Set it explicitly so config.json, sessions, and the Telegram lock land in
35+
# /home/odek/.odek (where the volume and config bind mounts are).
36+
ENV HOME=/home/odek
37+
USER odek
38+
WORKDIR /workspace
39+
40+
ENTRYPOINT ["odek"]

docker/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Odek — Docker Compose examples
2+
3+
Ready-to-run Compose setup for Odek in two permission profiles:
4+
5+
| Profile | Meaning | Use for |
6+
| --- | --- | --- |
7+
| **Restricted** (default) | Commands are risk-classified; destructive ones denied, the rest require approval. | Day-to-day use, untrusted tasks, human-in-the-loop. |
8+
| **Godmode** (all permissions) | "YOLO" mode — every risk class auto-allowed (except a hardcoded blocklist like fork bombs). No prompts. | Sealed, throwaway containers / CI. |
9+
10+
> **Why no `--sandbox`?** Odek's own `--sandbox` flag spawns *nested* Docker
11+
> containers. Here the Compose container **is** the sandbox, so commands run
12+
> directly inside it. The profile controls *what the agent may do inside that
13+
> boundary*. (`serve` defaults sandbox on, so its command passes `--no-sandbox`;
14+
> `run`/`repl`/`telegram` are unsandboxed by default.)
15+
16+
For the full walkthrough, threat model, and tuning, see
17+
[`../DOCKER_COMPOSE_USER_GUIDE.md`](../DOCKER_COMPOSE_USER_GUIDE.md).
18+
19+
## Files
20+
21+
```
22+
docker/
23+
├── Dockerfile # multi-stage build of the odek binary
24+
├── docker-compose.yml # 4 services across 4 profiles
25+
├── config.restricted.json # Restricted permission policy
26+
├── config.godmode.json # Godmode (YOLO) permission policy
27+
├── .env.example # copy to .env, add your API key
28+
└── workspace/ # the dir the agent works in (mounted in)
29+
```
30+
31+
## Quick start
32+
33+
All commands are run **from this `docker/` directory** so relative paths and
34+
`.env` resolve correctly:
35+
36+
```bash
37+
cd docker
38+
cp .env.example .env # then edit .env: set ODEK_API_KEY (+ model/base URL)
39+
```
40+
41+
### Restricted (recommended)
42+
43+
Interactive Web UI with approval prompts.
44+
45+
```bash
46+
docker compose --profile restricted up --build
47+
```
48+
49+
Open <http://127.0.0.1:8080>, type a task. When the agent attempts a higher-risk
50+
command (network, install, code execution) an **approval modal** appears — approve
51+
or deny. Destructive commands are rejected automatically. Stop with `Ctrl-C`, then
52+
`docker compose --profile restricted down`.
53+
54+
Prefer a terminal REPL (approvals come from the TTY, so `-it` is required):
55+
56+
```bash
57+
docker compose run --rm -it odek-restricted repl
58+
```
59+
60+
### Godmode (all permissions)
61+
62+
No prompts. Best for sealed, disposable containers. One-shot task:
63+
64+
```bash
65+
docker compose --profile godmode run --rm odek-godmode \
66+
run "Create build.sh, make it executable, and run it."
67+
```
68+
69+
The trailing `run "<task>"` overrides the service's default `serve` command. The
70+
service sets `network_mode: none`, so nothing leaves the container — remove that
71+
line in `docker-compose.yml` if the task genuinely needs network egress.
72+
73+
### Telegram bot
74+
75+
Drive the agent from a Telegram chat. Outbound long-polling — **no inbound ports
76+
needed**. Approvals (Restricted) arrive as inline `[Approve] [Deny] [Trust]`
77+
keyboards.
78+
79+
1. Create a bot with **@BotFather**, copy the token.
80+
2. In `.env`, set `ODEK_TELEGRAM_BOT_TOKEN` and **always** restrict access with
81+
`ODEK_TELEGRAM_ALLOWED_CHATS` (and/or `_USERS`) — a bot token is a public
82+
endpoint.
83+
3. Start one of:
84+
85+
```bash
86+
docker compose --profile telegram-restricted up --build -d # approvals in chat
87+
docker compose --profile telegram-godmode up --build -d # no prompts
88+
```
89+
90+
Message your bot `/start`. Sessions persist in the `odek-tg-state` volume.
91+
92+
> **Only run one Telegram profile at a time per token** — Telegram allows a single
93+
> long-poller per bot (a second gets `409 Conflict`). Create a second bot via
94+
> @BotFather if you want both.
95+
96+
## Verify the profiles differ
97+
98+
- **Restricted**: ask it to `rm -rf` everything in `/workspace` → denied, never runs.
99+
- **Godmode**: the same request executes without a prompt (use a throwaway `workspace/`).
100+
101+
Print the active policy mounted in a container:
102+
103+
```bash
104+
docker compose --profile restricted run --rm --entrypoint cat \
105+
odek-restricted /home/odek/.odek/config.json
106+
```
107+
108+
## Tuning
109+
110+
Edit `config.restricted.json`. Precedence (highest first): `allowlist` (exact
111+
match) → `denylist` (prefix) → per-class `classes` → global `action` → built-in
112+
defaults. The `blocked` class (fork bombs, etc.) is always denied. Recreate the
113+
container after editing (`... up` again) since the config is mounted at startup.
114+
115+
```jsonc
116+
{
117+
"dangerous": {
118+
"action": "prompt",
119+
"allowlist": ["go test ./...", "npm test"], // always allowed
120+
"denylist": ["git push --force"], // always blocked
121+
"classes": { "network_egress": "allow" } // loosen one class
122+
}
123+
}
124+
```
125+
126+
## Security notes
127+
128+
- Container runs as **non-root** (uid 1000). Keep it that way.
129+
- Mount only what the agent needs (`./workspace`). Never mount `/`, `$HOME`, SSH
130+
keys, cloud creds, or `/var/run/docker.sock`.
131+
- Keep the Web UI on `127.0.0.1`; front it with an authenticated reverse proxy for
132+
remote access.
133+
- Prefer `network_mode: none` (or a scoped network), especially for Godmode.
134+
- `.env` and `workspace/` are gitignored — never commit secrets or scratch files.

docker/config.godmode.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sandbox": false,
3+
"dangerous": {
4+
"action": "allow",
5+
"non_interactive": "allow"
6+
}
7+
}

docker/config.restricted.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"sandbox": false,
3+
"dangerous": {
4+
"action": "prompt",
5+
"non_interactive": "deny",
6+
"classes": {
7+
"destructive": "deny",
8+
"system_write": "prompt",
9+
"network_egress": "prompt",
10+
"code_execution": "prompt",
11+
"install": "prompt",
12+
"local_write": "allow"
13+
},
14+
"allowlist": [],
15+
"denylist": ["rm -rf /"]
16+
}
17+
}

docker/docker-compose.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Odek in Docker Compose — Restricted (default) and Godmode (all permissions).
2+
#
3+
# Run from this directory (`cd docker`) so the relative paths and `.env`
4+
# resolve correctly. Build context is the repo root (one level up).
5+
#
6+
# Profiles (opt into exactly one at a time):
7+
# restricted Web UI + approval prompts (recommended)
8+
# godmode No prompts; sealed container
9+
# telegram-restricted Telegram bot; approvals via inline keyboards
10+
# telegram-godmode Telegram bot; no prompts
11+
#
12+
# Examples:
13+
# docker compose --profile restricted up --build
14+
# docker compose --profile godmode run --rm odek-godmode run "create build.sh and run it"
15+
# docker compose --profile telegram-restricted up --build -d
16+
17+
services:
18+
# ── Restricted (default) — interactive Web UI with approval prompts ──
19+
odek-restricted:
20+
profiles: ["restricted"]
21+
build:
22+
context: ..
23+
dockerfile: docker/Dockerfile
24+
image: odek:local
25+
env_file: .env
26+
command: ["serve", "--addr", "0.0.0.0:8080", "--no-sandbox"]
27+
ports:
28+
- "127.0.0.1:8080:8080" # Web UI, bound to localhost only
29+
volumes:
30+
- ./workspace:/workspace
31+
- ./config.restricted.json:/home/odek/.odek/config.json:ro
32+
restart: "no"
33+
34+
# ── Godmode (all permissions) — non-interactive, sealed container ──
35+
odek-godmode:
36+
profiles: ["godmode"]
37+
build:
38+
context: ..
39+
dockerfile: docker/Dockerfile
40+
image: odek:local
41+
env_file: .env
42+
# No published ports. No network unless you need it (remove network_mode).
43+
network_mode: "none"
44+
command: ["serve", "--addr", "0.0.0.0:8080", "--no-sandbox"]
45+
volumes:
46+
- ./workspace:/workspace
47+
- ./config.godmode.json:/home/odek/.odek/config.json:ro
48+
restart: "no"
49+
50+
# ── Telegram bot — Restricted (approvals via inline keyboards) ──
51+
odek-telegram-restricted:
52+
profiles: ["telegram-restricted"]
53+
build:
54+
context: ..
55+
dockerfile: docker/Dockerfile
56+
image: odek:local
57+
env_file: .env
58+
command: ["telegram"]
59+
volumes:
60+
- ./workspace:/workspace
61+
- ./config.restricted.json:/home/odek/.odek/config.json:ro
62+
- odek-tg-state:/home/odek/.odek
63+
restart: unless-stopped
64+
65+
# ── Telegram bot — Godmode (no prompts; sealed container) ──
66+
odek-telegram-godmode:
67+
profiles: ["telegram-godmode"]
68+
build:
69+
context: ..
70+
dockerfile: docker/Dockerfile
71+
image: odek:local
72+
env_file: .env
73+
command: ["telegram"]
74+
volumes:
75+
- ./workspace:/workspace
76+
- ./config.godmode.json:/home/odek/.odek/config.json:ro
77+
- odek-tg-state:/home/odek/.odek
78+
restart: unless-stopped
79+
80+
volumes:
81+
odek-tg-state:

docker/workspace/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)