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.
The bind-address rule
Section titled “The bind-address rule”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.
The conventional port
Section titled “The conventional port”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.
Request flow
Section titled “Request flow”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.
Streaming
Section titled “Streaming”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().
Failure modes
Section titled “Failure modes”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? |
Which hop produced the error?
Section titled “Which hop produced the error?”- 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.
Calling the proxy
Section titled “Calling the proxy”Authentication is mTLS, inherited from swarm-server. Use the
credentials stored by swarm login:
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/hiforwards/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
Connectionheader.Hostis not forwarded (the upstream sees the bridge IP). NoX-Forwarded-Foris 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>"}withContent-Type: application/json. - No retries: one client request = at most one upstream request.
vm unreachableandcontainer not runningare safe to retry;target port unreachablemay 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).
Not supported in v1
Section titled “Not supported in v1”- No WebSocket / HTTP upgrades —
Upgradeheaders 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.
Debugging
Section titled “Debugging”Work outward from the container, one hop at a time.
- Is the container running?
swarm container info <id>— look forstatus: runningand a Docker ID. - Is the service bound to
0.0.0.0:8900? Shell into the container from the VM and check listeners:If you seessh root@<vm-ip> -- docker exec -it <docker-id> bashss -lntp127.0.0.1:8900, that’s the bug — rebind to0.0.0.0. - Look at swarm-server logs.
container proxy agent connectmeans the SSH-tunnel step failed;container proxy upstreammeans the agent replied with an error. - Look at swarm-agent logs (on the VM):
proxy: resolve(docker inspect failed) andproxy: dial(TCP dial to the bridge IP failed).
- Security model — how mTLS authentication works
- API reference — the full HTTP surface