English | [日本語](docker.ja.md)

# Running MAESTRO with Docker

The fastest way to run MAESTRO is Docker Compose. This guide covers the full
path from `git clone` to a working instance, plus the things that commonly trip
people up (Linux networking, the LLM endpoint, data persistence, the sandbox).

## TL;DR

Works on Linux and on Windows via WSL2 (Docker Desktop's WSL integration).

```bash
docker compose up -d     # builds the image on first run, then starts
# open http://localhost:9876
```

No `.env` or `config.yaml` is required to start: a fresh `docker compose up`
opens a setup wizard in the UI where you point MAESTRO at your LLM. To preset the
endpoint instead, `cp .env.example .env` and set `OLLAMA_BASE_URL`/`OLLAMA_MODEL`
(see [The LLM endpoint](#the-llm-endpoint)).

Compose publishes the UI on `9876` on **all host interfaces**, so a Docker host
is reachable from your LAN out of the box. That also means a fresh instance is
exposed and **unauthenticated** — anyone who can reach the port can run code on
the host via the agent's Bash tool. Read [Going beyond localhost](#going-beyond-localhost)
before leaving it up on a shared network.

### Windows (WSL2)

Run the same command inside a WSL2 distro with Docker Desktop's WSL integration
enabled — nothing else is needed. The headed-browser stack runs entirely inside
the container (no host X server or WSLg), and the repository's `.gitattributes`
keeps the build's shell scripts LF-only, so a Windows checkout (which would
otherwise convert them to CRLF) still builds cleanly.

## What the container is (and is not)

- It runs the **MAESTRO app** (web UI, workers, tools, optional gateway).
- It does **not** run an LLM. You point MAESTRO at an existing
  OpenAI-compatible endpoint (Ollama, vLLM, a hosted gateway, …).
- The image bundles the headed-browser stack (Xvfb / x11vnc / Chromium) and the
  Bash **sandbox** (bubblewrap + a pre-baked Python toolchain), so the Browser
  tab and sandboxed Bash work out of the box.

## The LLM endpoint

`.env.example` defaults to an Ollama running on the **host**:

```ini
OLLAMA_BASE_URL=http://host.docker.internal:11434/v1
OLLAMA_MODEL=qwen3:32b
```

`host.docker.internal` resolves to the host on Docker Desktop (macOS/Windows)
and — because `docker-compose.yml` sets `extra_hosts: host.docker.internal:host-gateway`
— on Linux too. You usually don't need to change anything.

Point it elsewhere when your LLM is not on the Docker host:

```ini
# Ollama / vLLM on another machine (use that machine's IP)
OLLAMA_BASE_URL=http://192.0.2.10:11434/v1
# A hosted OpenAI-compatible gateway
OLLAMA_BASE_URL=https://your-gateway.example.com/v1
```

Make sure the model in `OLLAMA_MODEL` is actually pulled/served by that
endpoint, or task runs will fail at the first LLM call.

## Verify it's running

```bash
docker compose ps                 # STATUS should become healthy
docker compose logs -f maestro    # watch startup; Ctrl-C to stop following
```

The container has a healthcheck that polls `/health`. Once it reports
`healthy`, open <http://localhost:9876>, create a task, and confirm it
progresses. If the LLM endpoint is wrong you'll see connection errors in the
logs as soon as a task starts.

## Data persistence

Two named volumes survive `docker compose down` / image rebuilds:

| Volume | Mount | Holds |
|--------|-------|-------|
| `maestro-data` | `/app/data` | SQLite DB, users, skills, secrets |
| `maestro-workspaces` | `/workspaces` | per-task agent workspaces |

`docker compose down -v` deletes these volumes (and all your data) — omit `-v`
to keep them.

## Configuration

The image ships a runnable `config.yaml` (copied from `config.yaml.example`),
and `.env` overrides the common knobs (LLM endpoint, `PORT`). For most setups,
editing `.env` is all you need.

To manage the full `config.yaml` from the host (and have **Settings UI** edits
persist across container recreation), bind-mount it — uncomment in
`docker-compose.yml`:

```yaml
    volumes:
      - ./config.yaml:/app/config.yaml   # create ./config.yaml first
```

Create the host file before starting (`cp config.yaml.example config.yaml`), or
Docker will create a *directory* at that path. Without the bind-mount, Settings
UI changes live only inside the container and are lost when it is recreated.

## The Bash sandbox in Docker

`safety.bash_sandbox` defaults to `auto`: use bubblewrap when available, fall
back to a hardened allowlist otherwise. The image installs bubblewrap, so the
sandbox is active by default. For multi-user or untrusted workloads set
`safety.bash_sandbox: always` (fail-closed if bubblewrap is unavailable).

bubblewrap needs unprivileged user namespaces. They are enabled on most modern
hosts; if your host or container runtime disables them, `auto` degrades to the
hardened allowlist and logs a warning at startup. See
[operations/bash-sandbox-provisioning.md](operations/bash-sandbox-provisioning.md).

## Build vs. prebuilt image

`docker compose up` **builds the image locally** from the `Dockerfile` (there is
no published image). The first build downloads Chromium and the Python
toolchain, so expect several minutes; later starts reuse the cached image.
Rebuild after pulling new code with `docker compose up -d --build`.

## Going beyond localhost

By default Compose publishes `9876` on all interfaces, so the instance is already
reachable from your LAN — **and unauthenticated**. Before you rely on it on any
shared or untrusted network, harden it:

1. Enable authentication (OAuth, or local accounts).
2. Set `safety.bash_sandbox: always`.
3. Terminate TLS — either MAESTRO's native HTTPS or a reverse proxy in front.

To keep an instance **local-only** instead (e.g. reach it over an SSH tunnel),
pin the compose port mapping back to `127.0.0.1:9876:9876`.

See [SECURITY.md](../SECURITY.md) and
[getting-started.md](getting-started.md) for the full hardening checklist.

## Troubleshooting

| Symptom | Likely cause |
|---------|--------------|
| Tasks fail immediately with a connection error | `OLLAMA_BASE_URL` unreachable from the container, or the model isn't served |
| `host.docker.internal` not found (Linux) | `extra_hosts` removed from compose, or an old Docker without host-gateway — use the host's LAN IP |
| Settings changes vanish after `down`/`up` | `config.yaml` not bind-mounted (see [Configuration](#configuration)) |
| Browser tab / CAPTCHA pool unstable | `/dev/shm` too small — compose sets `shm_size: 1gb`; keep it |
| All data gone after restart | used `docker compose down -v` (the `-v` deletes volumes) |
