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)). **A fresh container starts with a login.** Compose publishes `9876` on all host interfaces, so a Docker host is reachable from your LAN out of the box — and the agent's Bash tool would hand anyone who reaches it code execution. So a new install that binds a non-loopback interface generates an admin account on first boot and prints the password: ```console $ docker compose logs maestro | grep initial-admin [initial-admin] ... Sign in as admin@localhost / ... ``` The same password is written to `data/.initial-admin-password` (mode `0600`) in case the log has scrolled away. Sign in, then change it under **Settings → Preferences** — the reminder stops and the file is removed once you do. The setup wizard lives behind that login, so sign in first and configure the LLM afterwards. This only happens on a **fresh** install with no `auth` section in `config.yaml`. An existing deployment is never switched to a login behind your back; if it is unauthenticated and reachable, the UI shows a standing warning instead. See [Going beyond localhost](#going-beyond-localhost). Two consequences worth knowing up front: - **Rebuilding the container keeps the login.** `config.yaml` lives in the image layer, so `docker compose up -d --build` resets it while `/app/data` (and your admin account) survives on the named volume. Boot notices the mismatch and turns `auth.local` back on — your existing password keeps working, nothing is regenerated. Everything *else* you saved in Settings is still lost on a rebuild: bind-mount `config.yaml` (writable) or point `AAO_CONFIG` at a path under the data volume to keep it. To turn the login off deliberately, write `auth.local.enabled: false` — an explicit setting is never overwritten. - **A read-only `config.yaml` makes a fresh install refuse to start.** It cannot write the auth setting, and coming up unauthenticated on a reachable interface is the one outcome nobody would notice. Make the file writable, configure auth yourself, or bind loopback. - **Wiping the data volume while keeping `config.yaml` does not lock you out.** `docker compose down -v` leaves a config that says "auth on" with no account behind it; boot notices and mints a fresh admin, printed the same way. If an OAuth provider is configured, it does nothing — you can still sign in there. ### 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 , 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. A fresh install starts with a generated admin login (see [Quick start](#quick-start)), but that is the floor, not a finished setup. Before you rely on it on any shared or untrusted network, harden it: 1. Change the generated password, or switch to OAuth / your own 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) |