> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orcapods.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker Deployment

> Run the Orca stack with Docker Compose for production-parity local environments.

<Note>This is a self-hosting guide. The hosted product at [https://www.orcapods.ai](https://www.orcapods.ai) needs none of this — sign in and go.</Note>

## Compose Stack Overview

The root `docker-compose.yml` is profile-driven:

| Profile | Purpose                                                                        |
| ------- | ------------------------------------------------------------------------------ |
| `real`  | SeaweedFS plus the mode-specific sidecars on host ports `7071`, `7072`, `7073` |
| `poly`  | Capability-matched runners plus mode-specific and `MODE=all` sidecars          |
| `stack` | Full two-conductor, two-runner backend behind nginx on `localhost:8080`        |
| `all`   | Sidecar superset used for ad-hoc local testing                                 |

`make dev` is an alias for `make stack`.

***

## Full Backend Stack

```bash theme={null}
make dev
```

This starts:

| Service                      | Role                                                      |
| ---------------------------- | --------------------------------------------------------- |
| `seaweedfs`                  | Local S3-compatible artifact store                        |
| `seaweedfs-init`             | Creates the `agent-artifacts` bucket                      |
| `agent-worker-claude-poly`   | Claude sidecar, `MODE=claude`, port `7071`                |
| `agent-worker-codex-poly`    | Codex sidecar, `MODE=codex`, port `7072`                  |
| `agent-worker-general-poly`  | General sidecar, `MODE=general`, port `7073`              |
| `runner-1`, `runner-2`       | Stateful runners, each advertising `claude,codex,general` |
| `conductor-1`, `conductor-2` | Stateless control plane replicas                          |
| `nginx`                      | Load balances conductor traffic on `localhost:8080`       |

The dashboard is not containerized by the stack. Run it separately:

```bash theme={null}
make frontend
# http://localhost:5173
```

***

## Building Images

```bash theme={null}
docker build -t orca/conductor -f docker/Dockerfile.conductor .
docker build -t orca/runner -f docker/Dockerfile.runner .
docker build -t orca/mcp-bridge -f docker/Dockerfile.mcp-bridge .
docker build -t orca/agent-worker -f agent-worker/Dockerfile ./agent-worker
```

For normal local development, prefer:

```bash theme={null}
make dev
```

Compose builds the required images for the selected profile.

***

## MCP Bridge

The standalone `mcp-bridge` service is the deployment boundary for Composio credentials and traffic. It listens on port `8091` by default, serves `GET /healthz`, exposes a bearer-gated `GET /metrics`, and accepts bridge operations only on its signed internal API.

Configure it with:

| Variable                                   | Requirement                                                          |
| ------------------------------------------ | -------------------------------------------------------------------- |
| `MCP_BRIDGE_INTERNAL_SIGNING_KEY_CURRENT`  | Required base64-encoded HMAC key that decodes to at least 32 bytes   |
| `MCP_BRIDGE_INTERNAL_SIGNING_KEY_PREVIOUS` | Optional previous signing key during rotation                        |
| `COMPOSIO_API_KEY`                         | Composio platform key owned by this service                          |
| `COMPOSIO_BASE_URL`                        | Optional Composio backend override                                   |
| `COMPOSIO_SUBJECT_PEPPER`                  | Recommended base64 pepper for deriving opaque tenant subjects        |
| `METRICS_AUTH_TOKEN`                       | Bearer token for `/metrics`; when unset, metrics returns `401`       |
| `REDIS_URL`                                | Optional shared nonce store; when unset, nonces are stored in memory |
| `PORT` or `MCP_BRIDGE_PORT`                | Listen port; defaults to `8091`                                      |

The service fails startup when the current signing key is missing, malformed, or too short. Use `docker/railway-mcp-bridge.toml` as the Railway deployment reference.

***

## Shared Storage

All compose profiles inherit the same S3-compatible storage environment:

```yaml theme={null}
AWS_ENDPOINT_URL_S3: "http://seaweedfs:8333"
AWS_REGION: "us-east-1"
AWS_ACCESS_KEY_ID: "agent-orc"
AWS_SECRET_ACCESS_KEY: "agent-orc-secret"
S3_BUCKET: "agent-artifacts"
S3_FORCE_PATH_STYLE: "true"
```

Host access uses `http://localhost:8333`. The Makefile has storage helpers:

```bash theme={null}
make storage-up
make storage-smoke
make storage-ls
make storage-smoke-tools
```

***

## Stack Configuration

The `stack` profile uses two runners and two conductors:

```yaml theme={null}
runner-1:
  profiles: ["stack"]
  environment:
    RUNNER_PORT: "7070"
    RUNNER_BASE_URL: "http://runner-1:7070"
    RUNNER_CAPABILITIES: "claude,codex,general"
    CLAUDE_SIDECAR_URL: "http://agent-worker-claude-poly:7071"
    CODEX_SIDECAR_URL: "http://agent-worker-codex-poly:7072"
    GENERAL_SIDECAR_URL: "http://agent-worker-general-poly:7073"

conductor-1:
  profiles: ["stack"]
  environment:
    CONDUCTOR_PORT: "8080"
    RUNNER_URLS: "http://runner-1:7070,http://runner-2:7070"

nginx:
  profiles: ["stack"]
  ports:
    - "8080:80"
```

Both conductors are stateless and can route any session because session IDs include the owning runner hash.

***

## Specialized Poly Topology

Use the `poly` profile when you want capability-aware routing across runner shapes:

| Runner           | Capabilities           | Sidecar                          |
| ---------------- | ---------------------- | -------------------------------- |
| `runner-claude`  | `claude`               | `agent-worker-claude-poly:7071`  |
| `runner-codex`   | `codex`                | `agent-worker-codex-poly:7072`   |
| `runner-general` | `general`              | `agent-worker-general-poly:7073` |
| `runner-poly`    | `claude,codex,general` | `agent-worker-all-poly:7070`     |

```bash theme={null}
docker compose --profile poly up --build
```

This is useful for testing that the conductor pool routes each profile runtime only to capable runners.

***

## Environment File

Create `.env` at the repo root for provider keys:

```bash theme={null}
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GOOGLE_GENERATIVE_AI_API_KEY=AI...
GROQ_API_KEY=gsk_...
TAVILY_API_KEY=tvly-...
```

Docker Compose loads `.env` automatically.

***

## Health Checks

```bash theme={null}
# Conductor through nginx
curl http://localhost:8080/healthz

# Runners exposed by the stack profile
curl http://localhost:17070/healthz
curl http://localhost:17071/healthz

# Sidecars exposed by the real/stack profiles
curl http://localhost:7071/health
curl http://localhost:7072/health
curl http://localhost:7073/health

# Storage
curl http://localhost:8333/
```

`/healthz` returns text `ok`. The SeaweedFS S3 root may return `403` without credentials; that still means the service is reachable.

***

## Logs

```bash theme={null}
docker compose logs -f
docker compose logs -f conductor-1
docker compose logs --tail=100 runner-1
```

Go services use Zap structured logs. Sidecars write JSON-ish process logs from Node.

***

## nginx Configuration

The repository nginx config proxies all traffic to the conductor pool and disables stream buffering:

```nginx theme={null}
upstream conductors {
    least_conn;
    server conductor-1:8080 max_fails=2 fail_timeout=5s;
    server conductor-2:8080 max_fails=2 fail_timeout=5s;
}

server {
    listen 80;
    location / {
        proxy_pass http://conductors;
        proxy_buffering off;
        proxy_read_timeout 1h;
    }
}
```

This keeps `/api/runs/{id}/stream`, `/api/workflows/runs/{id}/stream`, and runner NDJSON streams usable through the proxy.

***

## Scaling Notes

To add a runner:

1. Give it a stable, unique `RUNNER_BASE_URL`.
2. Set accurate `RUNNER_CAPABILITIES`.
3. Add that same URL to every conductor's `RUNNER_URLS`.

Existing sessions stay on their original runner. New sessions are round-robined across runners that advertise the profile runtime.
