MINARA

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:

  1. Stage 1: builder. node:24-slim plus python3 / make / g++ for compiling better-sqlite3's native addon. Installs deps, type-checks, and emits dist/ via tsc.
  2. Stage 2: runtime. node:24-slim with just the built dist/, package.json, and node_modules/ copied from the builder. Drops to the non-root node user and exposes port 8080.

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=/data

All 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 profiles

Mount /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=... \
  minara

The 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.js

Override 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.js

Both 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.js

The 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 (or OPENROUTER_API_KEY, etc.) for the LLM provider.
  • MINARA_API_KEY for 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: debug is 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:

  1. Replace the container. docker pull minara:latest, stop the old one, start the new one with the same volume. The schema migrations in apps/agent/src/platform/db.ts are idempotent and run at boot, so data survives.
  2. Self-update via the CLI. minara update from 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:

  1. 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.
  2. 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.toml

For 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_DAYS caps 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.0 without 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.01 if you want a no-op spend ceiling instead.

On this page