Skip to content

Container HTTP proxy

The container HTTP proxy lets you reach a service running inside a deployed container via ANY /api/containers/<id>/proxy/<path> on swarm-server. Swarm-server forwards the request to the target VM’s swarm-agent, which proxies it to the container’s bridge IP on a conventional port.

If you get 502 {"error":"target port unreachable"} from the proxy, check this first — not a firewall, not a misconfigured port, not an agent bug. Almost every “the proxy doesn’t work” report comes down to this.

Swarm has one proxy target port, shared by all containers on all VMs. The default is 8900. Services must listen on this port inside the container.

The port is configured server-side only:

{
"proxy_target_port": 9000
}

Then restart swarm-server. Existing containers that still listen on the old port will start returning 502 target port unreachable until they’re updated.

Multi-port per container is not supported in v1. One port, one service per container.

caller
│ HTTP request
▼
swarm-server /api/containers/<id>/proxy/<path>
│ look up container → VM
│ open SSH tunnel to VM's swarm-agent
▼
swarm-agent on VM POST /proxy (via SSH tunnel)
│ docker inspect <container> → bridge IP
│ dial <bridge-ip>:<port>
▼
container your service on 0.0.0.0:<port>

All four hops are HTTP. The tunnel is transparent to the caller: a request and its response are a single byte-for-byte stream from the caller to the container and back.

Server-Sent Events, chunked request bodies, and chunked response bodies pass through as produced — no buffering. Client disconnect cancels the in-container request via context propagation; long-running handlers should respect r.Context().

Every error the proxy returns is a JSON body of the form {"error":"<message>"}. The strings are stable and safe to match against in scripts.

Status Body Cause What to check
404 container "<id>" not found No container with that id/alias in the registry swarm container list
404 vm "<vmid>" not found Container exists but its VM is missing (stale) swarm vm list; re-deploy
503 container not ready Container still deploying; no Docker ID yet swarm container info <id> — wait for running
502 vm unreachable SSH tunnel from server to the agent failed Is the VM up? Is swarm-agent running?
502 container not running Docker can’t find the container, or no usable bridge IP docker ps on the VM
502 target port unreachable TCP dial to the container’s bridge IP failed Is the service bound to 0.0.0.0? Is the port correct?
  • 404 errors are always from swarm-server (registry lookup).
  • 503 container not ready is always from swarm-server.
  • 502 vm unreachable is swarm-server: the SSH tunnel failed before any request reached the agent.
  • 502 container not running is swarm-agent.
  • 502 target port unreachable can come from either hop: swarm-agent emits it when its TCP dial fails (the common case — the bind-address rule); swarm-server emits it if the agent’s reply is unparseable or the agent is killed mid-stream.

Authentication is mTLS, inherited from swarm-server. Use the credentials stored by swarm login:

Terminal window
curl --cert ~/.swarm/client.crt --key ~/.swarm/client.key \
--cacert ~/.swarm/server.pem \
https://swarm-server/api/containers/<id>/proxy/<path>

Cert-less requests get 401; unknown-CA certs fail the handshake. There is no CLI wrapper command — callers use any HTTP client.

Wire contract notes for client implementers

Section titled “Wire contract notes for client implementers”
  • Path handling: /api/containers/echo/proxy/ forwards /; /proxy/hi forwards /hi; query strings pass through unmodified. Always include at least one character after /proxy/ to avoid a 301 redirect hop. Trailing slashes are preserved.
  • Headers: all request headers pass through except RFC 7230 §6.1 hop-by-hop headers and anything named in the incoming Connection header. Host is not forwarded (the upstream sees the bridge IP). No X-Forwarded-For is added — upstreams cannot see the client IP.
  • Bodies: unbuffered in both directions; binary-safe; no size limit imposed by the proxy.
  • Status codes: upstream statuses pass through byte-for-byte, including upstream 5xx. Proxy-generated errors are always {"error":"<message>"} with Content-Type: application/json.
  • No retries: one client request = at most one upstream request. vm unreachable and container not running are safe to retry; target port unreachable may have reached the container mid-stream.
  • No timeouts: the proxy imposes no wall-clock limit — clients must set their own (but not per-request deadlines on streaming calls).
  • Cancellation: closing the client connection cancels the upstream request end-to-end (typically <100 ms).
  • No WebSocket / HTTP upgrades — Upgrade headers are stripped; the handshake cannot complete.
  • No multi-port per container and no per-request target override.
  • No HTTP/2 end-to-end — the upstream hop is always HTTP/1.1.
  • No access to the original client IP.

Work outward from the container, one hop at a time.

  1. Is the container running? swarm container info <id> — look for status: running and a Docker ID.
  2. Is the service bound to 0.0.0.0:8900? Shell into the container from the VM and check listeners:
    ssh root@<vm-ip> -- docker exec -it <docker-id> bash
    ss -lntp
    If you see 127.0.0.1:8900, that’s the bug — rebind to 0.0.0.0.
  3. Look at swarm-server logs. container proxy agent connect means the SSH-tunnel step failed; container proxy upstream means the agent replied with an error.
  4. Look at swarm-agent logs (on the VM): proxy: resolve (docker inspect failed) and proxy: dial (TCP dial to the bridge IP failed).