NevoFlux
Headless (Docker)

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.

ServiceModeUse for
headlesslong-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.
oneshotone task per containerUntrusted tasks — but see the caveat: its run --task form is not wired up yet. Gated behind the oneshot profile.
remote--daemon --remote-controlA 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/metrics

The 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 volume

Notes on the config

  • command — --headless serves the task API and spawns a browser per task. The task-API port also serves the OpenAI / Anthropic endpoints and A2A; --a2a-addr adds a dedicated A2A port, published as 8084. 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 up exposes :8084 with no NEVOFLUX_A2A_TOKEN. Set the token, or remove the port and --a2a-addr if you don't use A2A — remembering that A2A also answers on :8080 regardless. (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_PROXY to an empty string (an empty proxy can hang the client). Leave them unset for direct egress, or set them to a real proxy plus NO_PROXY: "127.0.0.1,localhost".
  • pids — set under deploy.resources.limits; Compose v2 rejects setting both pids_limit and deploy.resources.limits.pids.
  • Networks — Compose can't express an egress allowlist itself. Put headless on a network whose egress is restricted (external firewall, a proxy sidecar, or a k8s NetworkPolicy).

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 200k

Caveat — still open as of v0.3.16. The oneshot service and the run --task one-shot form ship in the deploy assets, but the run subcommand is not wired into the agent binary yet (re-checked against v0.3.16) — the arguments are ignored and no task runs; only --daemon --headless service mode is active. Until it lands, get per-task isolation by running a fresh headless container per task (bring it up, submit one task, tear it down), or use session mode with end_session for 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 phone

The 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

WhereChange
environment.ANTHROPIC_API_KEYYour 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-profilesPopulate 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:/workHost 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 / VNCUncomment 6080 / 5900 and set NEVOFLUX_VNC=1 (+ a password file) only to watch a run live; keep off in prod.
deploy.resources.limitsTune cpu / memory / pids per host.
security_opt: seccompAdd a tuned seccomp profile once available.
networksAttach headless to an egress-restricted network.
--a2a-addr / 8084 / NEVOFLUX_A2A_TOKENSet 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.

On this page