Deployment
Running Minara Agent in Docker and other production environments
Minara Agent is designed to deploy as a single container with one mounted volume for state. This page explains the Docker image, the common deployment shapes, and the knobs you'll want to tune in production.
The Dockerfile
Dockerfile is a multi-stage build:
- Stage 1:
builder.node:24-slimpluspython3 / make / g++for compilingbetter-sqlite3's native addon. Installs deps, type-checks, and emitsdist/viatsc. - Stage 2:
runtime.node:24-slimwith just the builtdist/,package.json, andnode_modules/copied from the builder. Drops to the non-rootnodeuser and exposes port8080.
The final image does not contain the TypeScript source, the test suite, or the build toolchain. It's roughly 300 MB uncompressed depending on your pnpm lockfile.
The data directory
MINARA_DATA_DIR=/dataAll mutable state lives under this single directory:
/data
├── minara.db ← SQLite (WAL)
├── minara.db-wal
├── minara.db-shm
├── logs/ ← daily rotated NDJSON
├── sandbox/files/ ← sandboxed user files
├── workspace/ ← SOUL.md / AGENTS.md / etc
├── env ← persisted `minara model use` / `minara gateway add`
└── auth/ ← encrypted auth profilesMount /data as a volume. Everything else in the container is
immutable. A docker run --rm with no volume still works for
ephemeral experiments, but loses state on exit.
Common shapes
1. Interactive REPL in a container
docker run --rm -it \
-v minara-data:/data \
-e ANTHROPIC_API_KEY=sk-... \
-e MINARA_API_KEY=... \
minaraThe default CMD runs dist/gateway/cli.js, which drops into
the REPL. Useful for local testing against a production-shape
image.
2. HTTP gateway service
docker run -d --name minara \
-v minara-data:/data \
-p 8080:8080 \
-e ANTHROPIC_API_KEY=sk-... \
-e MINARA_API_KEY=... \
-e MINARA_BIND_ADDR=0.0.0.0:8080 \
minara \
node dist/gateway/server.jsOverride CMD with node dist/gateway/server.js to run the HTTP
gateway instead of the REPL. The health check in the Dockerfile
already probes /healthz on port 8080, so Docker/Kubernetes can
use it directly.
3. Both CLI and gateway on the same data dir
Run the gateway as a long-lived service and open a one-shot REPL against the same data dir for interactive debugging:
docker run --rm -it \
-v minara-data:/data \
-e ANTHROPIC_API_KEY=sk-... \
minara \
node dist/gateway/cli.jsBoth processes read/write the same SQLite file. WAL mode means concurrent reads don't block, but two processes writing at once is still a bad idea. Treat the REPL as a read-mostly debugger when attaching to a running gateway.
4. Autopilot-only deploy
docker run -d \
-v minara-data:/data \
-e ANTHROPIC_API_KEY=sk-... \
-e MINARA_API_KEY=... \
-e MINARA_AUTOPILOT_ENABLED=true \
-e MINARA_DAILY_CAP_USD=500 \
-e MINARA_AUTOPILOT_CAP_USD=50 \
minara \
node dist/gateway/server.jsThe HTTP gateway boots the autopilot runtime on startup when autopilot is enabled. The caps enforce hard ceilings the LLM cannot talk its way past.
Environment variables
Minimum required:
ANTHROPIC_API_KEY(orOPENROUTER_API_KEY, etc.) for the LLM provider.MINARA_API_KEYfor the Minara trading backend, unless you use the interactive device-login flow.
Strongly recommended in production:
MINARA_DAILY_CAP_USD: hard daily spend ceiling.MINARA_PER_TX_MAX_USD: hard per-transaction ceiling.MINARA_BIND_ADDR: bind address for the gateway.MINARA_DATA_DIR=/data: pin the data directory explicitly (avoids surprises if the image's default changes).LOG_LEVEL=info:debugis safe but multiplies log volume roughly 4×.
See Environment Variables for the full inventory.
Secrets
Never bake secrets into the image. The Dockerfile contains nothing you can't publish; all credentials come in at runtime via env vars or bind-mounted files.
For Kubernetes: use Secret resources mounted as env vars. For
Docker Compose: use env_file pointing at a .env file outside
the image. For plain Docker: use -e KEY=value from the host
environment.
The redactor in tools/_shared/result.ts masks known sensitive
keys in the audit log, so even if a secret does end up in a tool
argument it won't land on disk. This is a defense in depth. Do
not rely on it.
Health checks
The image ships a built-in HEALTHCHECK that hits /healthz on
port 8080. The endpoint returns 200 when:
- The SQLite file is openable.
- The LLM provider has resolved credentials.
- The tool registry has at least one tool registered.
For deeper checks, query /status instead. It returns DB stats,
skill count, active trigger count, and the timestamp of the last
successful LLM call.
Upgrades
Two options:
- Replace the container.
docker pull minara:latest, stop the old one, start the new one with the same volume. The schema migrations inapps/agent/src/platform/db.tsare idempotent and run at boot, so data survives. - Self-update via the CLI.
minara updatefrom inside the container detects the install path and invokes the right upgrade flow. Useful for long-running personal deployments; less useful for orchestrated production where you want the orchestrator to own the lifecycle.
Always take a backup before upgrading across a major version:
docker exec minara \
sqlite3 /data/minara.db ".backup '/data/backups/minara-pre-upgrade.db'"Horizontal scaling
Minara is designed for single-node operation. SQLite is the bottleneck: one writer at a time. For horizontally scaled HTTP gateways you have two options:
- One node owns state, others proxy. Run a single "state" container with the SQLite volume and have replicas forward write operations to it. Reads can hit replicas directly.
- Swap in Postgres. The storage layer under
apps/agent/src/storage/is narrow enough that porting to Postgres is feasible, but no one has done it yet. File an issue if you want to.
In practice most deployments are single-node. The Redis code path
in package.json exists for optional distributed locking when
you do scale out, but it's not a boot-time requirement.
Observability
Container logs are NDJSON on stdout. Pipe them to your log aggregator directly:
docker logs minara | vector --config vector.tomlFor Prometheus-style metrics, scrape /status and parse the JSON
response. There is no dedicated /metrics endpoint yet; if you
want Prometheus format, file an issue or contribute one.
See Observability for the full debug flow.
Resource sizing
Typical resource usage for a single-user deployment:
- CPU: idle ≈ 1% of one core, busy (active turn) ≈ 20% of one core. The LLM provider is where the real work happens.
- Memory: 150–300 MB resident. Growth is mostly the SQLite page cache and the in-memory log ring buffer.
- Disk: 20–100 MB per month of daily use. The audit log is
the dominant consumer;
MINARA_LOG_RETENTION_DAYScaps it. - Network: bursty. Idle is near zero; an active turn can push a few MB to the LLM provider.
These are guidelines. If you're running heavy research workloads or autopilot with many symbols, expect 2–4× on everything.
What NOT to do
- Don't run multiple processes against the same SQLite file and expect concurrent writes to be safe. SQLite is single-writer. Use the single-owner-plus-proxies shape above.
- Don't skip the volume mount. Without it every restart loses audit log, memory, and auth state.
- Don't bind port 8080 to
0.0.0.0without a reverse proxy that terminates TLS and enforces auth. The gateway exposes fund-moving endpoints. - Don't put secrets in the image. Ever. Not even for a "just for today" test.
- Don't disable the permission tier hook chain as a shortcut
for autopilot testing. Use
MINARA_AUTOPILOT_CAP_USD=0.01if you want a no-op spend ceiling instead.