# Environment variables Everything an operator can set for the MCP stack, in three parts: 1. [The stack's `.env`](#1-the-stacks-env) - what you write in `.env` next to `mcp-stack.compose.yml`. Split into **required** and **optional**. 2. [What the compose file sets itself](#2-what-the-compose-file-sets-itself) - do not put these in `.env`. 3. [Per-server variables](#3-per-server-variables-running-a-server-by-hand) - only for starting one image by hand with `docker run`, outside Compose. `mcp-stack.env.example` is the template: `cp mcp-stack.env.example .env`. `.env` is ignored by git and holds secrets, so it stays on the machine that runs the stack. > Compose substitutes an unset variable with an empty string and only warns. A "required" variable > below is one whose absence makes the service refuse to start or the stack unusable - not one > Compose itself rejects. ## 1. The stack's `.env` ### Required | Variable | What it is | Notes | |---|---|---| | `CODING_AGENT_MCP_API_KEYS` | Bearer tokens that may call the coding-agent MCP. Comma-separated, optionally `subject=token`. | Each token at least 32 characters. The server refuses to start with none. This is the `key` in the node's catalog entry. | | `DEV_SERVER_MCP_API_KEYS` | Same, for the dev-server MCP. | Use a different token from the other servers. | | `BROWSER_MCP_API_KEYS` | Same, for the browser MCP. | | | `MINIO_MCP_API_KEYS` | Same, for the MinIO MCP. | | | `DEV_SERVER_WORKER_TOKEN` | Shared secret between `dev-server-mcp` and `dev-server-worker`. | At least 32 characters; both refuse to start otherwise. Never leaves the stack, so it is not a catalog value. | | `MCP_WORKSPACE_HOST_PATH` | Absolute path of the host directory mounted as `/workspace`. | Bind-mount source, so empty fails. A dedicated project directory - never a home directory or `/`. | Generate a token with `openssl rand -hex 32`. ### Required in practice | Variable | Default | Why | |---|---|---| | `MCP_UID`, `MCP_GID` | `10001` / `10001` | Keep them. The images run as 10001:10001, and the dev-server's two volumes take their owner from the image, so another value leaves the worker unable to write to them. Give `MCP_WORKSPACE_HOST_PATH` to `10001:10001` instead. This file is read literally, no shell runs over it. | ### Optional **Images** | Variable | Default | Meaning | |---|---|---| | `MCP_IMAGE_TAG` | the hash named in `mcp-stack.compose.yml` | Which build of the five `luciolelii/*` images to run: a commit hash of the `mcps` repository, or `latest`. Bumping it is a deploy. | **MinIO MCP** | Variable | Default | Meaning | |---|---|---| | `MINIO_MCP_INTERNAL_BUCKET_PREFIX` | `exec-` | Every internal-mode bucket is ``. Cannot be empty; at most 27 lowercase letters, digits or `-`. It is the boundary that keeps the API key inside buckets this server made. | | `MINIO_MCP_INTERNAL_BUCKET_EXPIRE_DAYS` | `7` | Lifecycle rule set on an execution's bucket when this server creates it: objects are deleted that many days after they were written. `0` sets none. The empty bucket stays. Needs permission to set a bucket lifecycle; if refused, the session still opens and a warning is logged. | | `MINIO_MCP_ALLOWED_ENDPOINTS` | empty (public addresses) | Comma-separated S3 origins, e.g. `https://s3.example.org`. In external mode the endpoint comes from the node's configuration (`x-minio-endpoint`); empty, any **public** address is reachable and private, loopback and metadata ones are refused at every connection; set, exactly the listed origins, private ones included. It applies to both modes, so when set the internal MinIO's endpoint must be on it. | | `MINIO_MCP_DEFAULT_REGION` | `us-east-1` | Region when a session names none. | | `MINIO_MCP_MAX_OBJECT_BYTES` | `1048576` (1 MiB) | Limit for one read and one write. Ceiling 32 MiB. | | `MINIO_MCP_MAX_CONNECTIONS_PER_SESSION` | `8` | External mode only. Ceiling 64. | The internal MinIO's endpoint and keys are **not** variables here: they are headers of the `minio-mcp-internal` entry in the workflow manager's catalog (`x-minio-endpoint`, `x-minio-access-key`, `x-minio-secret-key`, optionally `x-minio-region`). **PostgreSQL MCP** | Variable | Default | Meaning | |---|---|---| | `POSTGRES_MCP_API_KEYS` | none, **required** | Bearer tokens, as for the other servers; at least 32 characters each. | | `POSTGRES_MCP_INTERNAL_DATABASE_PREFIX` | `exec_` | Every internal-mode database, and its user, is `` with the id's dashes as underscores. Cannot be empty; at most 26 lowercase letters, digits or `_`, starting with a letter. It is the boundary that keeps the API key inside databases this server made. | | `POSTGRES_MCP_ALLOWED_HOSTS` | empty (public addresses) | Comma-separated host names this server may connect to. In external mode the host and port come from the node's configuration (`x-postgres-host`, `x-postgres-port`); empty, any **public** address is reachable and private, loopback and metadata ones are refused; set, exactly the listed hosts, private ones included. For the internal mode it is optional; when set, the platform's PostgreSQL host has to be on it. | | `POSTGRES_MCP_MAX_ROWS` | `1000` | Most rows one query returns, whatever the model asks. Ceiling 10000. | | `POSTGRES_MCP_MAX_RESULT_BYTES` | `1048576` (1 MiB) | Most bytes of rows one statement returns; the rest is cut and the answer says so. Ceiling 16 MiB. | | `POSTGRES_MCP_STATEMENT_TIMEOUT_MS` | `30000` | A statement running longer is stopped by the server. Ceiling 10 minutes. | | `POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION` | `4` | External mode only. Ceiling 32. | The internal PostgreSQL's host and credentials are **not** variables here: they are headers of the `postgres-mcp-internal` entry in the workflow manager's catalog (`x-postgres-host`, `x-postgres-port`, `x-postgres-database`, `x-postgres-user`, `x-postgres-password`, `x-postgres-sslmode`, and `x-postgres-role-secret`). The user has to be able to make databases and users - `CREATEDB` and `CREATEROLE`, and nothing more - and the secret, at least 32 characters, is what the password of each execution's own user is derived from. **It has to be the same as the `executionRoleSecret` of the workflow manager's own `storages.json` entry for that PostgreSQL**, and so does the prefix, or the storage nodes and the models would not see the same database. The databases are dropped when they are older than the entry's `executionDatabaseExpireDays` by the workflow manager, not by this server. **Dev server** | Variable | Default | Meaning | |---|---|---| | `DEV_SERVER_SERVICES_CONFIG` | `./dev-server-mcp/services.example.json` | The services a caller may start. Copy `services.example.json` and edit the copy. Mounted read-only. | | `DEV_SERVER_ALLOWED_COMMANDS` | `node,npm,python3,./mvnw` | What a service may run. `npx` is left out on purpose: it runs an arbitrary package by name. | | `DEV_SERVER_PORT_RANGE` | `5200-5219` | Ports given to per-execution instances. Keep aligned with `BROWSER_MCP_ALLOWED_ORIGINS`. | | `DEV_SERVER_MAX_INSTANCES` | `4` | Concurrent instances. Size by memory: two frontend builds saturate the worker's 2 GB. | | `DEV_SERVER_MAX_WORKSPACE_BYTES` | `536870912` (512 MiB) | Largest workspace copied for an instance. | | `DEV_SERVER_IDLE_TIMEOUT_SECONDS` | `7200` | Idle time after which an instance is stopped (its copy is kept). | | `DEV_SERVER_MAX_LIFETIME_SECONDS` | `86400` | Absolute lifetime; then everything, copy included, is discarded. | | `DEV_SERVER_PREVIEW_BASE_URL` | empty | Public address a person opens a preview at. Empty means no preview proxy and no preview links. | | `DEV_SERVER_PREVIEW_PORT` | `4500` | Port of the preview proxy inside the worker. | | `DEV_SERVER_EGRESS_PROXY` | `http://egress-proxy:3128` | The only route out the worker has, for installs. | | `DEV_SERVER_NO_PROXY` | `localhost,127.0.0.1,dev-server-worker` | Hosts that bypass that proxy. | **Browser** | Variable | Default | Meaning | |---|---|---| | `BROWSER_MCP_ALLOWED_ORIGINS` | `http://dev-server-worker:5173,http://dev-server-worker:5200-5219` | Origins the browser may open, exact match. Keep the range aligned with `DEV_SERVER_PORT_RANGE`, or a preview looks like a broken app. | | `BROWSER_MCP_MAX_SESSIONS` | `8` | Concurrent browser sessions. | **Published ports** (all on loopback, `127.0.0.1`) | Variable | Default | | |---|---|---| | `MCP_GATEWAY_PORT` | `3100` | One port serving every server by path (`/coding-agent/`, `/dev-server/`, `/browser/`, `/minio/`). What the catalog expects. | | `CODING_AGENT_MCP_PORT` | `3101` | Per-server ports, kept for clients configured before the gateway. | | `DEV_SERVER_MCP_PORT` | `3102` | | | `BROWSER_MCP_PORT` | `3103` | | | `MINIO_MCP_PORT` | `3104` | | **Dedicated VM** (with the overlay `mcp-stack.vm.compose.yml`) | Variable | Default | Meaning | |---|---|---| | `MCP_GATEWAY_CADDYFILE` | `./mcp-stack.Caddyfile` | Set to `./mcp-stack.vm.Caddyfile` on the VM: it serves HTTPS. | | `MCP_SITE_ADDRESS` | `:3100` | The public host name Caddy gets a certificate for, e.g. `mcp-stack.example.org`. Required with the VM Caddyfile. | | `MCP_TLS_CONTACT` | empty | Contact e-mail for the certificate authority. Set it on the VM. | | `MCP_BIND_ADDRESS` | `0.0.0.0` | Address ports 80 and 443 bind to. | ## 2. What the compose file sets itself Fixed in `mcp-stack.compose.yml`. They are not read from `.env`, and overriding them there has no effect. | Variable | Set on | Value | |---|---|---| | `CODING_AGENT_MCP_EXECUTION_BACKEND` | coding-agent-mcp | `disabled`, so no command-running tool is offered. | | `DEV_SERVER_WORKER_URL` | dev-server-mcp | `http://dev-server-worker:4000` | | `DEV_SERVER_CONFIG` | dev-server-worker | `/config/services.json` | | `DEV_SERVER_WORKSPACE_ROOT` | dev-server-worker | `/workspace` | | `DEV_SERVER_INSTANCES_ROOT`, `DEV_SERVER_INSTANCE_HOME`, `DEV_SERVER_NPM_CACHE` | dev-server-worker | volumes `/instances`, `/instances/.shared-home`, `/npm-cache` | Container paths, user ids of the browser (`1000:1000`), memory and CPU limits are fixed in the file too. ## 3. Per-server variables (running an image by hand) Only for `docker run` of one image, outside Compose. The stack does not forward these, so they cannot be set through `.env`. Defaults shown are the ones in the code. ### coding-agent-mcp | Variable | Default | | |---|---|---| | `CODING_AGENT_MCP_API_KEYS` | none, **required** over HTTP | Also read from `MCP_API_KEYS` when unset. | | `CODING_AGENT_MCP_ROOTS` | the working directory | Workspace root(s), when no `--root` flag is given. | | `CODING_AGENT_MCP_EXECUTION_BACKEND` | `local` on stdio, `disabled` on HTTP | Whether command execution is offered. | | `CODING_AGENT_MCP_CORS_ORIGINS` | empty | Comma-separated browser origins allowed. | | `CODING_AGENT_MCP_AUDIT_LOG` | none | Path of an audit log file. | | `MCP_DEBUG_REQUESTS` | off | `1`/`true`/`yes`/`on` logs requests. Leave it off in production. | ### dev-server-mcp | Variable | Default | | |---|---|---| | `DEV_SERVER_MCP_API_KEYS` | none, **required** | | | `DEV_SERVER_WORKER_TOKEN` | none, **required** | At least 32 characters. | | `DEV_SERVER_WORKER_URL` | `http://dev-server-worker:4000` | | | `DEV_SERVER_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | | | `DEV_SERVER_MCP_MAX_SESSIONS` | `64` | | | `DEV_SERVER_MCP_SESSION_TTL_SECONDS` | `1800` | | | `DEV_SERVER_MCP_CORS_ORIGINS` | empty | | ### dev-server-worker | Variable | Default | | |---|---|---| | `DEV_SERVER_WORKER_TOKEN` | none, **required** | At least 32 characters. | | `DEV_SERVER_CONFIG` | `/config/services.json` | The services file. | | `DEV_SERVER_WORKSPACE_ROOT` | `/workspace` | | | `DEV_SERVER_ALLOWED_COMMANDS` | none, **required** | The worker refuses to start with it empty, and every service's `command` must be on it. | | `DEV_SERVER_INSTANCES_ROOT` | none | Where per-execution copies live. | | `DEV_SERVER_INSTANCE_HOME` | `/tmp/dev-server` | | | `DEV_SERVER_NPM_CACHE` | none | | | `DEV_SERVER_NPM_REGISTRY` | none | Registry override. | | `DEV_SERVER_PORT_RANGE` | `5200-5219` | Ports for instances: a rising range above 1023. | | `DEV_SERVER_MAX_INSTANCES` | `4` | | | `DEV_SERVER_MAX_WORKSPACE_BYTES` / `_ENTRIES` | 512 MiB / `20000` | | | `DEV_SERVER_IDLE_TIMEOUT_SECONDS` / `_MAX_LIFETIME_SECONDS` | `7200` / `86400` | | | `DEV_SERVER_EGRESS_PROXY` / `_NO_PROXY` | none | Proxy for installs. | | `DEV_SERVER_PREVIEW_BASE_URL` | none | | | `DEV_SERVER_PREVIEW_HOST` / `_PORT` | `0.0.0.0` / `4500` | | | `DEV_SERVER_CHILD_PATH` | `/opt/java/openjdk/bin:/usr/local/bin:/usr/bin:/bin` | `PATH` of started services. | | `JAVA_HOME` | none | Passed to started services. | | `DEV_SERVER_WORKER_HOST` / `_PORT` | `0.0.0.0` / `4000` | | | `DEV_SERVER_WORKER_REQUEST_TIMEOUT_MS` | `620000` | | ### browser-mcp | Variable | Default | | |---|---|---| | `BROWSER_MCP_API_KEYS` | none, **required** | | | `BROWSER_MCP_ALLOWED_ORIGINS` | none, **required** | The server refuses to start with it empty. Exact origins, plus ranges such as `http://host:5200-5219`. | | `BROWSER_MCP_MAX_SESSIONS` | `16` | | | `BROWSER_MCP_DEFAULT_TIMEOUT_MS` | `15000` | | | `BROWSER_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | | | `BROWSER_MCP_SESSION_TTL_SECONDS` | `1800` | | | `BROWSER_MCP_CORS_ORIGINS` | empty | | ### minio-mcp | Variable | Default | | |---|---|---| | `MINIO_MCP_API_KEYS` | none, **required** | | | `MINIO_MCP_INTERNAL_BUCKET_PREFIX` | `exec-` | See above. | | `MINIO_MCP_INTERNAL_BUCKET_EXPIRE_DAYS` | `7` | See above. | | `MINIO_MCP_ALLOWED_ENDPOINTS` | empty (public addresses) | | | `MINIO_MCP_DEFAULT_REGION` | `us-east-1` | | | `MINIO_MCP_MAX_OBJECT_BYTES` | `1048576` | Ceiling 32 MiB. | | `MINIO_MCP_MAX_CONNECTIONS_PER_SESSION` | `8` | Ceiling 64. | | `MINIO_MCP_MAX_SESSIONS` | `32` | | | `MINIO_MCP_SESSION_TTL_SECONDS` | `1800` | | | `MINIO_MCP_REQUEST_MAX_BYTES` | derived from `MAX_OBJECT_BYTES` | HTTP body limit; base64 adds a third. | | `MINIO_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | | | `MINIO_MCP_CORS_ORIGINS` | empty | | ### PostgreSQL MCP | Variable | Default | | |---|---|---| | `POSTGRES_MCP_API_KEYS` | none, **required** | | | `POSTGRES_MCP_INTERNAL_DATABASE_PREFIX` | `exec_` | See above. | | `POSTGRES_MCP_ALLOWED_HOSTS` | empty (public addresses) | See above. | | `POSTGRES_MCP_MAX_ROWS` | `1000` | Ceiling 10000. | | `POSTGRES_MCP_MAX_RESULT_BYTES` | `1048576` | Ceiling 16 MiB. | | `POSTGRES_MCP_STATEMENT_TIMEOUT_MS` | `30000` | | | `POSTGRES_MCP_MAX_CONNECTIONS_PER_SESSION` | `4` | Ceiling 32. | | `POSTGRES_MCP_MAX_SESSIONS` | `16` | | | `POSTGRES_MCP_SESSION_TTL_SECONDS` | `1800` | | | `POSTGRES_MCP_REQUEST_MAX_BYTES` | `1048576` | HTTP body limit. | | `POSTGRES_MCP_HOST` / `_PORT` | `0.0.0.0` / `3000` | | | `POSTGRES_MCP_CORS_ORIGINS` | empty | |