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 ```bash cp .env.example .env # point OLLAMA_BASE_URL/OLLAMA_MODEL at your LLM docker compose up -d # builds the image on first run, then starts # open http://localhost:9876 ``` Compose publishes the UI on `127.0.0.1:9876` only, so a fresh instance is not reachable from your LAN. See [Going beyond localhost](#going-beyond-localhost). ## 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 , 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 The default binding is intentionally local-only. Before exposing MAESTRO to a network: 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. 4. Change the compose port mapping from `127.0.0.1:9876:9876` to the interface you intend to serve. 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) |