Docker Compose
The shipped docker-compose.yml for headless NevoFlux Agent — the headless, oneshot and remote services, environment, volumes, tmpfs, and container hardening, fully explained.
deploy/headless/docker-compose.yml packages the docker run invocations as
reusable, pre-hardened services, so you don't retype the flags. It defines three
services.
| Service | Mode | Use for |
|---|---|---|
headless | long-running service (--daemon --headless --http-addr 0.0.0.0:8080 --a2a-addr 0.0.0.0:8084) | Trusted tasks. Serves the task API on :8080 and A2A on :8084. Started by docker compose up. |
oneshot | one task per container | Untrusted tasks — but see the caveat: its run --task form is not wired up yet. Gated behind the oneshot profile. |
remote | --daemon --remote-control | A phone-driven head with no HTTP surface. Gated behind the remote profile. (v0.3.14) |
All three apply the same container hardening (read_only, cap_drop: ALL,
no-new-privileges, pids / cpu / memory limits). headless and oneshot keep
the data dir on a tmpfs and mount ./out:/work and
base-profiles:/base-profiles:ro; remote keeps its data dir on a persistent
volume instead — see The remote service.
Use it
# 0. one-time: put an API key (and optionally an egress proxy) in the environment
export ANTHROPIC_API_KEY=sk-...
export EGRESS_PROXY=http://egress:3128
# 1. build the image
docker compose build
# 2. service mode (trusted): serve the task API
docker compose up
# curl -X POST localhost:8080/tasks -d '{"task":"open example.com, report title","mode":"browser"}'
# curl localhost:8080/metricsThe headless service, annotated
services:
headless:
build:
context: .
dockerfile: Dockerfile
# Image is DOWNLOADED from GitHub releases (no local compile). Pin versions:
# args: { AGENT_VERSION: v0.3.16, BROWSER_VERSION: 0.3.16 } # default: latest
# To build from locally-staged artifacts instead, set: dockerfile: Dockerfile.local
image: nevoflux/agent:latest
command: ["--daemon", "--headless", "--http-addr", "0.0.0.0:8080",
"--a2a-addr", "0.0.0.0:8084"]
ports:
- "8080:8080" # task API (+ OpenAI /v1): POST /tasks, GET /tasks/:id[/events], /metrics
# - "8081:8081" # dedicated OpenAI (needs --openai-addr)
# - "8082:8082" # dedicated MCP (needs --mcp-addr)
# - "8083:8083" # dedicated ACP (needs --acp-addr)
- "8084:8084" # A2A: /.well-known/agent-card.json, POST /a2a (0.3.0), POST /a2a/v1 (1.0)
# - "6080:6080" # noVNC in a browser: http://localhost:6080/vnc.html (NEVOFLUX_VNC=1)
# - "5900:5900" # raw VNC for native clients (NEVOFLUX_VNC=1). Keep both off in prod.
environment:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:?set ANTHROPIC_API_KEY (or your provider's key)}"
# HTTP_PROXY: "${EGRESS_PROXY}" # route egress through a proxy (also set NO_PROXY below)
# HTTPS_PROXY: "${EGRESS_PROXY}"
# NO_PROXY: "127.0.0.1,localhost" # so the in-process gateway isn't proxied
NEVOFLUX_VNC: "0" # 1 (+ password file) to watch live on :5900 / :6080
# NEVOFLUX_A2A_TOKEN: "" # set to require `Authorization: Bearer` on A2A
# NEVOFLUX_A2A_PUBLIC_URL: "" # externally reachable base, if behind a proxy
# NEVOFLUX_A2A_CONTEXT_IDLE_SECS: "1800"
# NEVOFLUX_A2A_INLINE_MAX_BYTES: "262144"
# NEVOFLUX_WALL_CLOCK_SECS: "600" # A2A streams don't cancel on disconnect; budget them
RUST_LOG: "info" # log level (stderr → docker compose logs)
# NEVOFLUX_HEADLESS_SCRIPT: /opt/nevoflux/fixed-flow.py # fixed-script mode (no LLM)
# NEVOFLUX_SESSION_MODE: "1" # reuse ONE browser across a trusted task-flow
volumes:
- ./out:/work # drained result.json + debug-bundle + artifacts
- base-profiles:/base-profiles:ro # pre-authenticated login templates (cloned per task)
# - ./fixed-flow.py:/opt/nevoflux/fixed-flow.py:ro
tmpfs:
- /tmp
- /var/nevoflux/data # daemon.port / db — ephemeral
- /home/nevo/.gbrain # GBrain DB (HOME is on the read-only rootfs)
- /home/nevo/.cache # bun / misc caches
read_only: true
cap_drop: ["ALL"]
security_opt:
- "no-new-privileges:true"
# - "seccomp=./seccomp.json" # add a profile once tuned
deploy:
resources:
limits:
cpus: "2.0"
memory: 4g
pids: 512
restart: "no"
# networks: [egress-restricted] # attach to an egress-restricted network
volumes:
base-profiles:
# external: true # reuse an existing named volumeNotes on the config
command—--headlessserves the task API and spawns a browser per task. The task-API port also serves the OpenAI / Anthropic endpoints and A2A;--a2a-addradds a dedicated A2A port, published as8084. To add MCP, ACP or more dedicated ports, add their flags and publish the ports (see interfaces and ports):command: ["--daemon","--headless","--http-addr","0.0.0.0:8080","--a2a-addr","0.0.0.0:8084", "--openai-addr","0.0.0.0:8081","--mcp-addr","0.0.0.0:8082","--acp-addr","0.0.0.0:8083"]- A2A is published, and unauthenticated, by default.
docker compose upexposes:8084with noNEVOFLUX_A2A_TOKEN. Set the token, or remove the port and--a2a-addrif you don't use A2A — remembering that A2A also answers on:8080regardless. (The comment in the shipped file saying A2A "is not served on--http-addr" does not match the v0.3.16 code.) See A2A. - Provider key — the daemon reads the provider-specific var (
ANTHROPIC_API_KEY/OPENAI_API_KEY/ …). It must match the image's baked[llm].provider; for another provider, set its key and rebuild with--build-arg LLM_PROVIDER=…. - Egress proxy — do not set
HTTP_PROXY/HTTPS_PROXYto an empty string (an empty proxy can hang the client). Leave them unset for direct egress, or set them to a real proxy plusNO_PROXY: "127.0.0.1,localhost". pids— set underdeploy.resources.limits; Compose v2 rejects setting bothpids_limitanddeploy.resources.limits.pids.- Networks — Compose can't express an egress allowlist itself. Put
headlesson a network whose egress is restricted (external firewall, a proxy sidecar, or a k8sNetworkPolicy).
The oneshot service
oneshot:
profiles: ["oneshot"] # not started by `docker compose up`
image: nevoflux/agent:latest
build: { context: ., dockerfile: Dockerfile }
environment:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
volumes:
- ./out:/work
- base-profiles:/base-profiles:ro
tmpfs: [/tmp, /var/nevoflux/data, /home/nevo/.gbrain, /home/nevo/.cache]
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
deploy: { resources: { limits: { cpus: "2.0", memory: 4g, pids: 512 } } }It is meant to be invoked per task, passing run --task … args to the entrypoint:
docker compose --profile oneshot run --rm oneshot \
run --task "open example.com and report the title" \
--profile base1 --policy browser-only --wall-clock 300s --token-budget 200kCaveat — still open as of v0.3.16. The
oneshotservice and therun --taskone-shot form ship in the deploy assets, but therunsubcommand is not wired into the agent binary yet (re-checked against v0.3.16) — the arguments are ignored and no task runs; only--daemon --headlessservice mode is active. Until it lands, get per-task isolation by running a freshheadlesscontainer per task (bring it up, submit one task, tear it down), or use session mode withend_sessionfor trusted sequential flows.
The remote service
New in v0.3.14. Drives this container from a phone through nevoflux.app; it serves no HTTP, so it publishes no ports.
remote:
profiles: ["remote"] # `docker compose --profile remote up remote`
image: nevoflux/agent:latest
command: ["--daemon", "--remote-control"]
environment:
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:?set ANTHROPIC_API_KEY (or your provider's key)}"
NEVOFLUX_REMOTE_MODE: "agent" # fixed at startup; the phone can't change it
NEVOFLUX_REMOTE_EXECUTION_TIER: "browser-auto"
RUST_LOG: "info"
volumes:
- remote-data:/var/nevoflux/data # REQUIRED — a real volume, NOT a tmpfs
- ./account-token:/var/nevoflux/data/account-token:ro # nevoflux.app account token
- ./out:/work
tmpfs: [/tmp, /home/nevo/.gbrain, /home/nevo/.cache]
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
volumes:
remote-data: # this head's identity, browser profile and history — deleting it un-pairs every phoneThe data volume holds the channel and its pairing code; on a tmpfs, every restart
would be a new head and the paired phone would stop connecting. Setup, sign-in
options (NEVOFLUX_SERVICE_TOKEN, nevoflux-agent account login), the mode / tier
defaults and failure behaviour are on
Remote control (container).
What to change for your setup
| Where | Change |
|---|---|
environment.ANTHROPIC_API_KEY | Your provider key. Better: leave it empty and inject via HTTP_PROXY so a prompt-injected agent can't read it from env. |
HTTP_PROXY / HTTPS_PROXY (EGRESS_PROXY) | Point at an egress proxy that allowlists the LLM API + your task domains — this is the hard network boundary. |
volumes: base-profiles | Populate this named volume once with pre-authenticated login profiles (a human logs in), or set external: true to reuse one; it's cloned per task. |
volumes: ./out:/work | Host dir mounted at the image's working directory /work. Note that task results go to the task workspace under /tmp/nevoflux-profiles by default, not here — see status / result. |
ports / VNC | Uncomment 6080 / 5900 and set NEVOFLUX_VNC=1 (+ a password file) only to watch a run live; keep off in prod. |
deploy.resources.limits | Tune cpu / memory / pids per host. |
security_opt: seccomp | Add a tuned seccomp profile once available. |
networks | Attach headless to an egress-restricted network. |
--a2a-addr / 8084 / NEVOFLUX_A2A_TOKEN | Set a token, or drop the A2A port if you don't use it. |
Base profiles (login state)
A base profile is a pre-authenticated browser profile a human logs in once.
Each task clones it (never touches the original), so tasks start already logged
in. Populate the base-profiles named volume once, then reference a profile by
name — via the profile field on POST /tasks, or NEVOFLUX_TASK_PROFILE for the
thin interfaces. Keep the mount read-only so tasks can't overwrite a base
credential; drop :ro only if you use session mode's
save_profile to persist a clone back.
Docker image
Pull or build the nevoflux/agent image — build args, run modes, image environment, and the live-view (VNC/noVNC).
Task API & interfaces
The headless HTTP task API — submitting tasks, follow-up turns, per-turn usage, cancellation — plus which interface lives on which port, session and fixed-script modes, and the full CLI-flag and env-var reference.