# Accessing Priority Forge from inside a Docker container

This guide covers the case where **Priority Forge runs on the host** (the normal
systemd / launchd install) and you want to use it from an MCP client (Claude
Code, Cursor, Droid, etc.) running **inside a Docker container** — for example,
a dev container, a CI container, or a teammate's "helper agent" sandbox.

> If you want to run Priority Forge *itself* in Docker, that's a separate
> workstream — see [`DOCKER_PACKAGING.md`](./DOCKER_PACKAGING.md) (TBD).

## TL;DR

The host backend already listens on all interfaces (`0.0.0.0:3456`), so a
container can reach it via `host.docker.internal`. Point your container's
`.mcp.json` at:

```json
{
  "mcpServers": {
    "priority-forge": {
      "type": "http",
      "url": "http://host.docker.internal:3456/mcp"
    }
  }
}
```

On Linux, add the host-gateway alias to `docker run` / compose so
`host.docker.internal` resolves:

```bash
docker run --add-host=host.docker.internal:host-gateway ...
```

```yaml
# docker-compose.yml
services:
  agent:
    extra_hosts:
      - "host.docker.internal:host-gateway"
```

That's the whole pattern. The rest of this doc explains why it works, how to
verify, and the security considerations.

## Why no code change is needed

The Express server in `src/index.ts` calls `app.listen(PORT, ...)` with no host
argument. Node's default is to bind to all interfaces (`::` dual-stacked to
`0.0.0.0`). You can confirm on the host:

```bash
ss -tlnp | grep 3456
# LISTEN 0  511  *:3456  *:*  users:(("node",pid=...,fd=...))
```

The `*:3456` means it's reachable from any interface — including the Docker
bridge network. The "this only listens on localhost" assumption that often
blocks container-to-host MCP access does **not** apply here.

## Verifying from inside the container

```bash
# health check
curl -sS http://host.docker.internal:3456/health
# {"status":"ok","version":"...","uptime":...}

# MCP endpoint (POST with an init message)
curl -sS -X POST http://host.docker.internal:3456/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```

If both succeed, the MCP client config above will just work.

## Linux: `host.docker.internal` quirks

Docker Desktop (macOS / Windows / Linux-Desktop) defines `host.docker.internal`
automatically. Plain Docker Engine on Linux does **not** — you must add the
`--add-host=host.docker.internal:host-gateway` flag, which has been supported
since Docker 20.10. If you're on an older Docker, fall back to the bridge
gateway IP (usually `172.17.0.1`) or set `--network=host`.

| Setup | Resolves `host.docker.internal`? | Recommended config |
|---|---|---|
| Docker Desktop (any OS) | Yes, automatic | Just use the hostname |
| Docker Engine ≥ 20.10 on Linux | Only with the flag | `--add-host=host.docker.internal:host-gateway` |
| Docker Engine < 20.10 on Linux | No | Use bridge gateway IP, or `--network=host` |
| Rootless Docker | Same as above, plus see rootless docs | Same |

## Security: who can reach 3456?

Because the backend listens on `0.0.0.0`, anything that can route to the host's
IP on port 3456 can hit the REST + MCP API — which exposes full read/write over
your tasks. On a single-user laptop behind a firewall, that's fine; on a shared
network or a cloud host, restrict access at the firewall layer.

A minimal `ufw` rule (Linux) that lets Docker containers and localhost through
but blocks everything else:

```bash
# allow loopback
sudo ufw allow from 127.0.0.1 to any port 3456
# allow the docker bridge network (adjust to your bridge subnet)
sudo ufw allow from 172.17.0.0/16 to any port 3456
# deny everything else
sudo ufw deny 3456
```

Check your Docker bridge subnet with `docker network inspect bridge`.

If you want a stricter posture (bind only to localhost on the host, expose via
SSH tunnel or a reverse proxy with auth), that's a code change in
`src/index.ts:430` — set `app.listen(PORT, '127.0.0.1', ...)` and run a
container-side `socat` / SSH tunnel. Not the default; mentioning for
completeness.

## Common gotchas

- **`localhost` from inside the container is the container itself.** Always use
  `host.docker.internal` (or the bridge gateway IP), never `127.0.0.1` /
  `localhost`, when targeting the host.
- **The stdio proxy at `~/.local/share/priority-forge/mcp-proxy.js` is not the
  right transport for containers.** That proxy bridges stdio MCP clients to
  the HTTP backend on the host. Inside a container, point directly at the HTTP
  endpoint instead — it's simpler and doesn't require mounting the proxy.
- **Frontend (port 5173) is not needed for MCP.** Containerized agents don't
  need the dashboard; only the backend on 3456.
- **WSL2** behaves like Linux Docker Engine for this purpose. The
  `host-gateway` flag works.

## What this does NOT cover

- Running the PF backend itself in Docker (the `DOCKER_PACKAGING.md` workstream).
- Containerizing the hub (Postgres + pgvector). The shared hub already runs on
  AWS RDS for production; local dev hub containerization is a separate question.
- Auth / multi-tenancy on the PF API. PF is currently single-user; the firewall
  rules above are the only access control.
