Environment variables
Set them in engine/.env (see engine/.env.example). Docker Compose passes them to the engine container.
| Variable | Default | Meaning |
|---|---|---|
ENGINE_SECRET | required | Shared secret: mooflair's server auth both ways (mooflair → engine routes, engine → /api/internal), live link and file link signing. The engine refuses to start without it. |
ENGINE_PUBLIC_URL | — | Public base URL for live links (the engine's HTTPS domain in production, e.g. https://engine.example.com; http://localhost:4000 in development). |
ENGINE_FILE_URL | ENGINE_PUBLIC_URL | Where run files are served from. In production give it a host of its own (e.g. https://files.example.com, a second reverse-proxy site pointing at the engine), so a file never shares an origin with the engine's API. |
ENGINE_PORT | 4000 | Host port (bound to 127.0.0.1). |
ENGINE_CORS_ORIGIN | http://localhost:3000 | mooflair's origin: the only page allowed to call user routes and open run sockets. |
LIBRARY_PATH | /var/engine/library | Host folder for MongoDB data, NATS users and browser profiles. |
BROWSER_CHANNEL | chrome | chrome on amd64 servers, chromium on Apple Silicon. |
BROWSER_IDLE_MS | 600000 (10 min) | Stop a browser with no run for this long. |
ENGINE_MAX_LANES | 8 | Lanes (parallel branches and items) one run may have at once. |
WORKER_IDLE_MS | 300000 (5 min) | Stop a worker with no run for this long. |
MOOFLAIR_URL | required | mooflair's address as the engine reaches it (http://host.docker.internal:3000 in dev). The engine loads its apps' config and what each run needs from /api/internal, and tells it events and usage. The engine refuses to start without it. |
MOOFLAIR_KEY | — | mooflair's public key (secret.engine.key.public in its library/configuration.json, one line with \n for line breaks). Pages' workspace tokens are checked with it. Empty: pages can't call the engine. |
ENGINE_METRICS_TOKEN | — | Turns on /metrics and is its bearer token. |
DRIVE_NETWORK_SECRET | — | Joins mooflair's Drive as a node (next-drive 8, at MOOFLAIR_URL; it cleans up once a day on its own): mooflair's secret.drive.network from its library/configuration.json. The engine then keeps workspaces' files in library/drive, serves them at /api/drive, and runs the Drive node. Empty: not part of the Drive (Drive nodes fail). |
DRIVE_NODE_ID | — | This engine's name in the Drive (letters, digits, -, _; not local or google). Set it before files land here: an engine without one gets a made-up id kept in library/drive/store.json. |
DRIVE_CAPACITY_GB | — | Most this engine's disk keeps for the Drive; full, new files go to another engine. Empty: no cap. |
Set by Compose (no need to touch): NATS_URL=nats://nats:4222, NATS_CONTAINER=engine-nats, MONGODB_URI=mongodb://mongo:27017/engine, LIBRARY_HOST_PATH. Inside browser containers the engine sets BROWSER_ID, BROWSER_CONFIG and NATS credentials.
Example .env for local development:
ENGINE_SECRET=change-me-to-a-long-random-string
ENGINE_PUBLIC_URL=http://localhost:4000
ENGINE_PORT=4000
ENGINE_CORS_ORIGIN=http://localhost:3000
LIBRARY_PATH=/Users/you/Mooflair/Library/engine
BROWSER_CHANNEL=chromium
Sandbox (browser and worker containers)
- Egress firewall: each container starts as root just long enough to set
iptablesrules (it getsNET_ADMINfor that), then runs asnodewith no capabilities. Its own network (the engine and NATS) stays open; cloud metadata (169.254.0.0/16), the host and private ranges (10/8, 172.16/12, 192.168/16, 100.64/10) are refused. DNS (port 53) stays open.ENGINE_EGRESS_ALLOW(comma CIDRs, IPv4 or IPv6) opens ranges again: a proxy on your LAN, or locally mooflair athost.docker.internal(Docker Desktop:192.168.65.254/32,<its IPv6>/128; seegetent hosts host.docker.internalin the engine container). In production mooflair's run API is reached by its public URL, so nothing needs opening.ENGINE_BROWSER_DNS(comma IPs, e.g.1.1.1.1,8.8.8.8) gives browser and worker containers DNS servers of their own; empty: Docker's, which are your network's. Set it when your network's resolver gives a site only IPv6 addresses and the machine has no IPv6 (Chrome showsDNS_PROBE_FINISHED_NXDOMAINfor it); containers pick it up as they're recreated. - Node libraries in the image:
undici(HTTP proxy and TLS options),xlsx(spreadsheets),sharp(images) andpyodide(Python in the Code node) are installed in the agent image and kept out of each node's bundle. - Commands: the Command node only runs when an administrator allows it (Admin › Configuration › Sandbox), on plans with commands.
Services (docker-compose.yml)
| Service | Image | Notes |
|---|---|---|
engine | built from Dockerfile | Bun server; mounts the library and the Docker socket (it creates browser containers). |
nats | nats:2-alpine | Users from library/nats/users.conf, written by the engine. |
mongo | mongo:7 | Data in library/database. |
browser-patchright | built from docker/patchright/Dockerfile | Profile build: only builds the browser image. |
Commands
Run from engine/:
npm run dev
Starts the engine and NATS with docker compose up --build --watch (source changes sync and restart).
npm run build:browser
Builds the browser image (browser-patchright). Running browser containers are recreated with it by the next reconcile, which ends the runs they were doing.
docker compose up -d --build engine
Rebuilds and restarts only the engine. Queued runs fail; running runs continue in their agents.
npm run deploy
Production: builds the browser image, then starts engine, NATS and MongoDB. HTTPS comes from your reverse proxy (httpd or nginx) in front of 127.0.0.1:4000, with WebSockets passed through for the live view and run sockets.
Other scripts: npm run c:logs:engine (follow engine logs), npm run typecheck, npm run lint.
Changing a shape
When the run context, node state, events or the agent protocol change, change both apps in the same change (mooflair's types/lib/engine/* mirror the engine's types/*) and deploy them together. There is no runtime number and no compatibility layer.
FAQ
Do I need Google Chrome on a Mac?
No. Set BROWSER_CHANNEL=chromium; there is no Google Chrome build for linux/arm64.
Where are the browser profiles?
$LIBRARY_PATH/browser/<browser id>/profile, downloads in …/download.
Why does a site show DNS_PROBE_FINISHED_NXDOMAIN in a browser?
Your network's DNS gave it no IPv4 address (some routers return only IPv6 for www.facebook.com) and the machine can't reach IPv6. Set ENGINE_BROWSER_DNS=1.1.1.1,8.8.8.8 in the engine's .env and restart the engine (docker compose up -d --no-deps engine): browsers then resolve through those servers.