Cloudflare Containers

Container Port Forwarding

Two containers communicating over configurable ports

A
Container A
PID 1 · go-server
42MB · RUNNING
0
Sent
0B
Bytes Out
:8080 -> :3000
TCP
waiting...
B
Container B
No data received yet
PID 1 · dotnet-runtime
128MB · LISTENING
0
Received
0B
Bytes In

How it actually works

The Worker is the routing layer. It receives requests and forwards them to containers on any port.

Step 1
Request hits edge
Worker receives HTTP request at 330+ locations
Step 2
Route to container
Worker picks a container instance via Durable Object
Step 3
Forward to port
Container.fetch() proxies to your app's port
Step 4
Response
Container responds, Worker returns to client
wrangler.toml config
name = "my-app"
main = "src/index.ts"

# Define your containers
[[containers]]
class_name = "ContainerA"
image      = "./containers/a/Dockerfile"
instances  = 3

[[containers]]
class_name = "ContainerB"
image      = "./containers/b/Dockerfile"
instances  = 3

# Containers are backed by Durable Objects
[[durable_objects.bindings]]
class_name = "ContainerA"
name       = "CONTAINER_A"

[[durable_objects.bindings]]
class_name = "ContainerB"
name       = "CONTAINER_B"
src/index.ts worker
import { Container, getContainer } from "@cloudflare/containers";

// Container A - set the port it listens on
export class ContainerA extends Container {
  override port = 8080;  // <-- change this!
}

// Container B - different port
export class ContainerB extends Container {
  override port = 3000;  // <-- change this!
}

export default {
  async fetch(req, env) {
    // Get container instances
    const a = getContainer(env.CONTAINER_A, "a");
    const b = getContainer(env.CONTAINER_B, "b");

    // Forward request from A -> B
    const data = await a.fetch("/get-payload");
    const result = await b.fetch("/receive", {
      method: "POST",
      body: data.body,
    });

    return result;
  },
};
containers/a/Dockerfile container a
FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o server .

# EXPOSE only needed for local dev
# In production, all ports are auto-accessible
EXPOSE 8080

CMD ["./server"]
containers/a/main.go app code
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/get-payload", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, `{"event": "ping", "from": "container-a"}`)
    })

    // Listen on the port the Worker expects
    fmt.Println("Container A listening on :8080")
    http.ListenAndServe(":8080", nil)
}
!
The key insight: There's no -p 8080:3000 port mapping like Docker. The Worker IS the routing layer. It calls container.fetch() which hits whatever port you set in override port. In production, all container ports are automatically accessible to the Worker - no EXPOSE needed. Your app just listens on a port, and the Worker knows where to find it.
Network Log 0 entries
Send a payload to see network traffic