Quick Start
Container (Docker)

Running Route6 in a Container

The container runs the same daemon as the bare-binary install: same MCP endpoint, same identity, same tools. Docker here is an install environment, not a connection method — most people take the binary, and this page is for environments that require a container.

⚠️

A container is not a machine. Inbound port forwards have to be told which machine answers them — your host, or a sibling container. That is the one thing this page exists to explain. Get it wrong and the forward is created and looks healthy, but visitors hang for about ten seconds and then get nothing. The daemon tells you exactly why: docker logs r6me names the target it could not reach. See Troubleshooting.

The base compose file

Get your API key

From the dashboard (opens in a new tab) under Agents. It starts with sk_a6_.

Save a docker-compose.yml

services:
  r6me:
    image: route6me/netid:1.2.4
    container_name: r6me
    restart: unless-stopped
 
    environment:
      ROUTE6_API_KEY: "${ROUTE6_API_KEY:?set ROUTE6_API_KEY in .env}"
 
    ports:
      - "127.0.0.1:3000:3000"   # MCP        — point your agent here
      - "127.0.0.1:3001:3001"   # status API — `curl localhost:3001/status`
      - "127.0.0.1:1080:1080"   # egress proxy — SOCKS5 + HTTP CONNECT + plain HTTP
 
    extra_hosts:
      - "host.docker.internal:host-gateway"
 
    volumes:
      - r6me-state:/var/lib/r6me
 
volumes:
  r6me-state:

Put your key in a .env file beside it:

echo 'ROUTE6_API_KEY=sk_a6_your_key_here' > .env

Start it

docker compose up -d
curl -s localhost:3001/status

You want "transport": "connected" and your agent_id.

The 127.0.0.1: prefixes are the line that decides exposure. Dropping them (plain "3000:3000") publishes MCP on every interface of the host. The MCP endpoint carries your agent's identity — anyone who can reach it acts as your agent — so only widen it deliberately, and never onto a public interface.

The volumes: entry is not optional either. It persists the agent's WireGuard key and applied config generation, so a restart resumes. Without it the agent re-registers every time the container restarts.

Connect your agent to MCP

Point any MCP client at http://localhost:3000/mcp. For example, Claude Desktop:

{
  "mcpServers": {
    "route6": { "url": "http://localhost:3000/mcp" }
  }
}

No API key here — the daemon already holds it.

Outbound: sending traffic from your agent's IP

Port 1080 is a proxy speaking SOCKS5, HTTP CONNECT and plain HTTP on one port. Anything you point at it leaves from your agent's own public address:

export http_proxy=http://127.0.0.1:1080
export https_proxy=http://127.0.0.1:1080
curl https://api64.ipify.org      # your agent's IPv6

IPv4-only destinations work too — the hub reaches them over NAT64.

⚠️

Use lowercase http_proxy with curl. libcurl deliberately ignores an uppercase HTTP_PROXY (it could arrive from a CGI Proxy: header) while honouring HTTPS_PROXY either way. So uppercase-only looks like it works — https tunnels fine — and sends plain http straight out your normal egress instead. Most other clients (python requests, node, Go) accept either case.

For tools that speak SOCKS: ALL_PROXY=socks5h://127.0.0.1:1080.


Inbound: which machine answers a forward?

When you call port_forward { action: "create" }, Route6 accepts the connection on your public IPv6 at the gateway and relays it to your daemon. The daemon then has to connect to whatever is actually serving — and "localhost" inside a container means the container, where nothing is listening.

There are two topologies. They compose freely; you can use both at once.

Topology A — the service runs on your host

This is what extra_hosts: ["host.docker.internal:host-gateway"] is for. With it present, the daemon rewrites loopback forward targets to the host gateway address, so a forward whose target is localhost reaches your host.

# your service, on the host
python3 -m http.server 8080 --bind 0.0.0.0

Then, from your agent:

port_forward (action: create) { external_port: 28080, internal_port: 8080 }
🚫

The one that catches everyone: host.docker.internal resolves to your host's bridge address — typically 172.17.0.1 — not its loopback. A service bound to 127.0.0.1 on the host is not reachable from the container, so the forward is created successfully and then fails on every connection. Bind your service to 0.0.0.0 (or to the bridge address). Note the --bind 0.0.0.0 above — it is doing real work.

Verified end to end: with one service on 0.0.0.0:8500 and another on 127.0.0.1:8600, on the same agent at the same moment, the first answered from the internet and the second produced this in docker logs r6me:

forwards: dial target host.docker.internal:8600 failed:
  dial tcp 172.17.0.1:8600: connect: connection refused (hub will RST the visitor)

If extra_hosts is missing, the daemon says so at startup and leaves targets untranslated — your forwards then resolve inside the container, where nothing is listening:

container mode: no host gateway discoverable — loopback forward targets will NOT be
translated. Add extra_hosts: ["host.docker.internal:host-gateway"] to your compose,
or set R6ME_HOST_GATEWAY.

Topology B — the service runs in a sibling container

Use R6ME_FORWARD_TARGETS to say which container answers which forward. Each service keeps its own network — nothing gives up its network namespace, and nothing is published to the host.

services:
  r6me:
    image: route6me/netid:1.2.4
    container_name: r6me
    restart: unless-stopped
    environment:
      ROUTE6_API_KEY: "${ROUTE6_API_KEY:?}"
      # forward target port  ->  the container that answers it
      R6ME_FORWARD_TARGETS: "8080=api,9000=worker:3000"
    extra_hosts:
      - "host.docker.internal:host-gateway"   # any UNLISTED port still goes to the host
    volumes:
      - r6me-state:/var/lib/r6me
    ports:
      - "127.0.0.1:3000:3000"
    networks: [appnet]
 
  api:
    image: your/api          # listening on 8080
    networks: [appnet]
    environment:             # and its egress leaves from the agent's own address too
      http_proxy: "http://r6me:1080"
      https_proxy: "http://r6me:1080"
 
  worker:
    image: your/worker       # listening on 3000, reached as forward port 9000
    networks: [appnet]
 
networks:
  appnet:
 
volumes:
  r6me-state:

The syntax is "<port>=<host>[:<port>]", comma-separated:

EntryMeaning
8080=apiforward target port 8080 → container api, same port
9000=worker:3000forward target port 9000 → container worker port 3000
5432=127.0.0.1opt port 5432 out of host translation (stay inside the container)

Ports you do not list still resolve to the host, so host and sibling services mix freely. An IPv6 literal must be bracketed (8080=[fd00::2]:8080), the same rule URLs use.

A malformed entry is refused and logged at startup rather than guessed at — a wrong target is worse than a missing one, because it connects to the wrong service. Valid entries are echoed too, so docker logs r6me tells you exactly what the daemon believes:

R6ME_FORWARD_TARGETS: ignoring entry "notaport=x": "notaport" is not a port in 1-65535
forward target: port 8080 -> api
forward target: port 9000 -> worker:3000
⚠️

Give each service a different internal_port. The map is keyed by the forward's target port, because that is the only per-forward identifier the daemon receives. Two sibling services both listening on 8080 cannot be told apart, however different their external ports are.

The older R6ME_HOST_GATEWAY=<service name> still works, but it sends every loopback forward to that one destination, so it cannot serve host and sibling at once. Prefer the map.


Troubleshooting forwards

The failure mode worth naming: the forward is created and looks healthy, but every visitor hangs for about ten seconds and gets nothing. Nothing is wrong on the Route6 side — the daemon tried to reach a target that does not exist from inside the container.

Read the daemon log first — it names the cause

docker logs r6me 2>&1 | grep 'forwards: dial target'

This is the fastest answer you will get, because the daemon prints the address it actually dialled and what went wrong:

forwards: dial target host.docker.internal:8600 failed:
  dial tcp 172.17.0.1:8600: connect: connection refused (hub will RST the visitor)

That line tells you the topology question is already answered — it resolved to the host at 172.17.0.1 — and that the service there is simply not accepting. Only if there is no such line do you need the rest of this list.

Is the forward registered with the daemon?

curl -s localhost:3001/status | grep -A6 forwards

If it is not listed, the problem is upstream of the container — check that port_forward { action: "create" } returned an endpoint.

status echoes the target you asked for, so a forward aimed at the host shows "target_host": "127.0.0.1" even though the daemon translates it. That is expected; the log line above shows what was really dialled.

Did you just create it?

A new forward takes a few seconds to reach the gateway. A connection refused immediately after port_forward { action: "create" }, which then succeeds on retry, is propagation and not a fault.

Is your service reachable from inside the container's network?

The image has no shell, so test from a throwaway container on the same network:

# Topology A — the host
docker run --rm --add-host host.docker.internal:host-gateway curlimages/curl \
  -sS -m 5 http://host.docker.internal:8080/
 
# Topology B — a sibling
docker run --rm --network <your_compose>_appnet curlimages/curl \
  -sS -m 5 http://api:8080/

If that fails, Route6 is not involved yet — it is Docker networking.

Is your host service bound to 0.0.0.0?

ss -ltn | grep 8080

127.0.0.1:8080 will not work (see the warning above). You want 0.0.0.0:8080 or the bridge address.

Is the target map what you think it is?

docker logs r6me 2>&1 | grep -E 'forward target|FORWARD_TARGETS'

Valid entries are echoed at startup and rejected ones are named, so this prints the map the daemon is actually using:

forward target: port 8080 -> api
forward target: port 9000 -> worker:3000
R6ME_FORWARD_TARGETS: ignoring entry "notaport=x": "notaport" is not a port in 1-65535

Other things worth knowing

There is no shell in the image. docker exec has nothing to run. Inspect the agent from the host instead:

curl -s localhost:3001/status     # config generation, wg health, forwards, transport
docker logs r6me

Nothing listens on your public address. The container binds only container-local addresses; MCP, the status API and the proxy are reachable solely through the port mappings above. A port scan of your agent's public address shows only the forwards you deliberately created.

Upgrading. Bump the tag, then:

docker compose pull && docker compose up -d

The state volume means the agent resumes on the same identity — same WireGuard key, same config generation, no re-registration.