--- title: Self Hosted Docker description: "Run the whole Executor server in one Docker container over a SQLite file: API, MCP, auth, code execution, and the web UI." --- The self-hosted image is the entire Executor server in a single container: the typed API, the MCP server, authentication, QuickJS code execution, and the web console, all in one process over a libSQL (SQLite) file. There is no external database, worker, or proxy to run. ## Run it Using the published image: ```bash docker run -d \ --name executor-selfhost \ -p 4788:4788 \ -v executor-data:/data \ ghcr.io/usefulsoftwareco/executor-selfhost:latest ``` Or build from a clone of the [repo](https://github.com/UsefulSoftwareCo/executor), from `apps/host-selfhost`: ```bash docker compose up -d --build ``` No configuration is required. Open [http://localhost:4788](http://localhost:4788); the first person to create an account becomes the owner. After that, people join through single-use invite links you mint from the **Admin** page, and open signup is closed. ## Storage Everything that must persist (the SQLite database and the generated encryption keys) lives in the data directory, mounted at **`/data`** in the container. Always mount a volume there so it survives restarts and upgrades: ```bash -v executor-data:/data # named volume (what compose uses) # or a host path: -v /srv/executor:/data ``` Back it up by snapshotting that volume (or copying `/data`, primarily `data.db`). `EXECUTOR_DATA_DIR` (default `/data`) sets the directory, and `EXECUTOR_DB_PATH` (default `/data.db`) sets the database file. ## Environment variables Everything is optional: a bare run boots a working instance. The defaults below are the container defaults. | Variable | Default | Purpose | | ----------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- | | `PORT` | `4788` | HTTP port the server listens on. | | `EXECUTOR_HOST` | `0.0.0.0` | Bind address. The image binds all interfaces. | | `EXECUTOR_DATA_DIR` | `/data` | Directory holding the database and generated keys. | | `EXECUTOR_DB_PATH` | `/data.db` | SQLite database file. | | `EXECUTOR_WEB_BASE_URL` | auto (`http://localhost:4788`) | Public URL browsers use. Required behind a domain or TLS (see below). | | `EXECUTOR_TRUSTED_ORIGINS` | unset | Comma-separated browser aliases allowed to authenticate without changing the public URL. | | `BETTER_AUTH_SECRET` | generated, persisted in `/data` | Session secret (32+ chars). Rotating it signs everyone out. | | `EXECUTOR_SECRET_KEY` | generated, persisted in `/data` | Master key encrypting stored secrets. Set it to manage it yourself. | | `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` | unset | Pre-create the admin headlessly (with the password below); skips browser first-run. | | `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` | unset | Password for the bootstrap admin. | | `EXECUTOR_BOOTSTRAP_ADMIN_NAME` | `Admin` | Display name for the bootstrap admin. | | `EXECUTOR_ORG_NAME` | `Default` | Display name of the single org every user joins. | | `EXECUTOR_ORG_SLUG` | `default` | URL slug for that org. | | `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandboxed code to reach loopback / private addresses. Keep off unless you trust the code. | | `EXECUTOR_TRUSTED_PROXY_HEADER` | unset | Header your reverse proxy sets to the real client IP, e.g. `cf-connecting-ip`. Set with the next. | | `EXECUTOR_TRUSTED_PROXIES` | unset | Comma-separated IPs or CIDR ranges the proxy connects from. Set with the previous. | | `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` | `false` | Turn off sign-in rate limiting. Only when a proxy or WAF in front of Executor limits instead. | Tracing is configured separately, and off unless you turn it on — see [Tracing](/hosted/tracing). ## Behind a domain or TLS Set `EXECUTOR_WEB_BASE_URL` to the exact public URL you load in the browser (scheme, host, and port). If it does not match, browser logins are rejected with an invalid-origin error. ```bash docker run -d \ -p 4788:4788 \ -v executor-data:/data \ -e EXECUTOR_WEB_BASE_URL=https://executor.example.com \ ghcr.io/usefulsoftwareco/executor-selfhost:latest ``` For a headless deploy (CI or infra-as-code), set `EXECUTOR_BOOTSTRAP_ADMIN_EMAIL` and `EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD` to create the admin without the browser setup screen. ### Additional browser aliases Keep `EXECUTOR_WEB_BASE_URL` pinned to the canonical public HTTPS origin used for OAuth callbacks and generated links. If the same instance is intentionally available through additional browser addresses, allow those exact origins separately: ```bash -e EXECUTOR_WEB_BASE_URL=https://executor.example.com \ -e EXECUTOR_TRUSTED_ORIGINS=http://executor.home.arpa:4788,http://192.0.2.10:4788 ``` Only cookie-authenticated browser requests use this allowlist. OAuth callbacks, MCP metadata, approval links, and other absolute URLs remain pinned to `EXECUTOR_WEB_BASE_URL`. Origins are never inferred from request headers. ### Behind a reverse proxy Sign-in attempts are rate-limited per client IP (three per ten seconds). When browsers reach the container directly, the IP is the connecting address and nothing needs configuring. Behind Caddy, nginx, Cloudflare, or another reverse proxy, every connection comes from the proxy. If Executor runs behind a reverse proxy, set both variables, otherwise all users share one sign-in bucket. Tell Executor which header the proxy sets to the real client IP, and which addresses the proxy connects from: ```bash -e EXECUTOR_TRUSTED_PROXY_HEADER=x-real-ip \ -e EXECUTOR_TRUSTED_PROXIES=172.18.0.2 ``` Set both together. The header is only honoured on connections from one of the listed addresses, so a client that reaches the container directly cannot spoof it. List only the proxy's own addresses (its IP, or the compose network it shares with Executor, such as `172.18.0.0/24`), never a range that also contains your users: every listed address counts as a proxy hop, and if every hop is trusted Better Auth finds no client and falls back to one shared bucket. Use `cf-connecting-ip` with Cloudflare's published IP ranges, `x-real-ip` for nginx (`proxy_set_header X-Real-IP $remote_addr;`), or `x-forwarded-for` for Caddy. A malformed entry refuses to boot rather than silently pooling every user into one bucket. Executor never trusts `x-forwarded-for` or a similar header on its own, because any client could set it. When an auth request carries one of those headers and no trusted proxy is configured, the server logs one warning naming these two variables. On Linux, Docker's default iptables NAT preserves the client IP on a published port, so a container reached directly needs nothing. Docker Desktop (macOS and Windows) routes published ports through Docker's userland proxy, as does loopback traffic from the Docker host itself, so every connection arrives from one internal address; there, put a reverse proxy on the same Docker network in front of Executor and configure it as above. ## Connect an agent The server exposes a streamable-HTTP MCP endpoint at `/mcp`. Point your client at `http://localhost:4788/mcp` (or your public URL). See [MCP Proxy](/mcp-proxy) for how that endpoint exposes your integrations.