Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ Run an HTTP server.
Builds the model and starts an HTTP server that exposes the model's inputs
and outputs as a REST API. Compatible with the Cog HTTP protocol.

By default the container port is published on 127.0.0.1 (localhost), so the
server is only reachable from your local machine. The server process inside
the container binds to 0.0.0.0; use --host to control which host interface
the Docker port mapping is published on.

```
cog serve [flags]
```
Expand All @@ -286,6 +291,9 @@ cog serve [flags]
# Start on a custom port
cog serve -p 5000

# Listen on all interfaces (e.g. to expose to the network)
cog serve --host 0.0.0.0

# Test the server
curl http://localhost:8393/predictions \
-X POST \
Expand All @@ -299,6 +307,7 @@ cog serve [flags]
-f, --file string The name of the config file. (default "cog.yaml")
--gpus docker run --gpus GPU devices to add to the container, in the same format as docker run --gpus.
-h, --help help for serve
--host string Host IP to publish the container port on. Use 0.0.0.0 to allow connections from other machines. (default "127.0.0.1")
-p, --port int Port on which to listen (default 8393)
--progress string Set type of build progress output, 'auto' (default), 'tty', 'plain', or 'quiet' (default "auto")
--upload-url string Upload URL for file outputs (e.g. https://example.com/upload/)
Expand Down
6 changes: 5 additions & 1 deletion docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ with your project directory mounted in:
cog serve
```

By default the server runs on port 8393.
By default the server runs on port 8393 and the container port is published on
`127.0.0.1` (localhost), so it is only reachable from your local machine. The
server process inside the container binds to `0.0.0.0`; use `--host` to control
which host interface the Docker port mapping is published on.

Use `-p` to choose a different port:

```console
Expand Down
15 changes: 14 additions & 1 deletion docs/llms.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 18 additions & 3 deletions pkg/cli/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
)

var (
host = "127.0.0.1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — generic var name. Package-level host collides with a common local (docker.go:47 uses host, err := determineDockerHost()). serveHost or bindHost would be clearer and lower the shadowing risk. Consistent with the existing port/uploadURL convention, so low priority.

port = 8393
uploadURL = ""
)
Expand All @@ -28,13 +29,21 @@ func newServeCommand() *cobra.Command {
Long: `Run an HTTP server.

Builds the model and starts an HTTP server that exposes the model's inputs
and outputs as a REST API. Compatible with the Cog HTTP protocol.`,
and outputs as a REST API. Compatible with the Cog HTTP protocol.

By default the container port is published on 127.0.0.1 (localhost), so the
server is only reachable from your local machine. The server process inside
the container binds to 0.0.0.0; use --host to control which host interface
the Docker port mapping is published on.`,
Example: ` # Start the server on the default port (8393)
cog serve

# Start on a custom port
cog serve -p 5000

# Listen on all interfaces (e.g. to expose to the network)
cog serve --host 0.0.0.0

# Test the server
curl http://localhost:8393/predictions \
-X POST \
Expand All @@ -51,6 +60,7 @@ and outputs as a REST API. Compatible with the Cog HTTP protocol.`,
addGpusFlag(cmd)
addConfigFlag(cmd)

cmd.Flags().StringVar(&host, "host", host, "Host IP to publish the container port on. Use 0.0.0.0 to allow connections from other machines.")
cmd.Flags().IntVarP(&port, "port", "p", port, "Port on which to listen")
cmd.Flags().StringVar(&uploadURL, "upload-url", "", "Upload URL for file outputs (e.g. https://example.com/upload/)")

Expand Down Expand Up @@ -158,12 +168,17 @@ func cmdServe(cmd *cobra.Command, arg []string) error {
runOptions.ExtraHosts = []string{"host.docker.internal:host-gateway"}
}

runOptions.Ports = append(runOptions.Ports, command.Port{HostPort: port, ContainerPort: 5000})
runOptions.Ports = append(runOptions.Ports, command.Port{HostPort: port, ContainerPort: 5000, HostIP: host})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — remote-Docker regression. When DOCKER_HOST=tcp://remote:2375, this binding is applied on the remote daemon, so 127.0.0.1 there is unreachable from the cog client. --host 0.0.0.0 is the escape hatch but nothing warns the user. (cog predict was already broken for remote Docker — it connects to localhost — so this is a new issue only for serve.) Consider detecting a non-local DOCKER_HOST and emitting a console warning pointing at --host 0.0.0.0, or at least noting it in the --host help text.


displayHost := host
if displayHost == "0.0.0.0" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — IPv6 display URL. displayHost only special-cases 0.0.0.0. An IPv6 value like ::1 interpolates to http://::1:8393 (missing brackets — invalid URI). Consider bracketing IPv6 literals or validating --host.

Also: this displayHost mapping and the --host defaulting are untested — extracting func displayHostFor(host string) string and table-testing it would cover both.

displayHost = "localhost"
}

console.Info("")
console.Infof("Running %[1]s in Docker with the current directory mounted as a volume...", console.Bold(strings.Join(args, " ")))
console.Info("")
console.Infof("Serving at %s", console.Bold(fmt.Sprintf("http://127.0.0.1:%v", port)))
console.Infof("Serving at %s", console.Bold(fmt.Sprintf("http://%s:%v", displayHost, port)))
console.Info("")

err = docker.Run(ctx, dockerClient, runOptions)
Expand Down
1 change: 1 addition & 0 deletions pkg/docker/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type RunOptions struct {
type Port struct {
HostPort int
ContainerPort int
HostIP string // Host IP to bind to. Defaults to "127.0.0.1" if empty.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — extract a shared constant. "127.0.0.1" is duplicated as the default in 5 sites: this comment, serve.go:20, docker.go:457, run.go:41, and predictor.go:101. A single const DefaultHostIP = "127.0.0.1" here, referenced everywhere, removes the magic-string duplication and the drift risk.

}

type Volume struct {
Expand Down
6 changes: 5 additions & 1 deletion pkg/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,14 @@ func (c *apiClient) containerRun(ctx context.Context, options command.RunOptions
if len(options.Ports) > 0 {
hostCfg.PortBindings = make(nat.PortMap)
for _, port := range options.Ports {
hostIP := port.HostIP
if hostIP == "" {
hostIP = "127.0.0.1"
}
containerPort := nat.Port(fmt.Sprintf("%d/tcp", port.ContainerPort))
hostCfg.PortBindings[containerPort] = []nat.PortBinding{
{
HostIP: "", // use empty string to bind to all interfaces
HostIP: hostIP,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — untested core change. The empty→127.0.0.1 default and this HostIP propagation are the central behavior change of the PR, but containerRun has no unit test. Suggest extracting the PortBindings construction (the loop over options.Ports) into a pure helper like portBindingsFromPorts([]command.Port) nat.PortMap and table-testing it: empty HostIP → 127.0.0.1, explicit 0.0.0.0, explicit 127.0.0.1, custom IP, multiple ports.

HostPort: strconv.Itoa(port.HostPort),
},
}
Expand Down
11 changes: 7 additions & 4 deletions pkg/docker/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ func RunDaemon(ctx context.Context, dockerClient command.Command, options comman
return dockerClient.ContainerStart(ctx, options)
}

func GetHostPortForContainer(ctx context.Context, dockerCommand command.Command, containerID string, containerPort int) (int, error) {
func GetHostPortForContainer(ctx context.Context, dockerCommand command.Command, containerID string, containerPort int, hostIP string) (int, error) {
console.Debugf("=== DockerCommand.GetPort %s/%d", containerID, containerPort)

if hostIP == "" {
hostIP = "127.0.0.1"
}

inspect, err := dockerCommand.ContainerInspect(ctx, containerID)
if err != nil {
return 0, fmt.Errorf("failed to inspect container %q: %w", containerID, err)
Expand All @@ -56,8 +60,7 @@ func GetHostPortForContainer(ctx context.Context, dockerCommand command.Command,
}

for _, portBinding := range inspect.NetworkSettings.Ports[targetPort] {
// TODO[md]: this should not be hardcoded since docker may be bound to a different address
if portBinding.HostIP != "0.0.0.0" {
if portBinding.HostIP != hostIP {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix — brittle exact-string match. portBinding.HostIP != hostIP is stricter than the old != "0.0.0.0" filter (which matched in either direction). Docker runtimes (Docker Desktop VM proxy, rootless, future versions) may normalize the reported IP, causing a silent does not have a port bound to 127.0.0.1 failure on a container that did bind that port. Suggestion: when exactly one PortBinding exists for the target port, return it regardless of HostIP; only filter by hostIP when multiple are present.

continue
}
hostPort, err := nat.ParsePort(portBinding.HostPort)
Expand All @@ -67,5 +70,5 @@ func GetHostPortForContainer(ctx context.Context, dockerCommand command.Command,
return hostPort, nil
}

return 0, fmt.Errorf("container %s does not have a port bound to 0.0.0.0", containerID)
return 0, fmt.Errorf("container %s does not have a port bound to %s", containerID, hostIP)
}
81 changes: 69 additions & 12 deletions pkg/docker/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,63 @@ import (

func TestGetHostPortForContainer(t *testing.T) {
t.Run("WithExposedPort", func(t *testing.T) {
testClient := dockertest.NewMockCommand2(t)
testClient.EXPECT().ContainerInspect(t.Context(), "container123").Return(&container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Status: "running",
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
NetworkSettingsBase: container.NetworkSettingsBase{
Ports: nat.PortMap{
nat.Port("5678/tcp"): []nat.PortBinding{
{
HostIP: "127.0.0.1",
HostPort: "12345",
},
},
},
},
},
}, nil)

hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — table-driven + missing edge cases. Each of the 8 subtests repeats the full InspectResponse boilerplate; a single table with hostIP arg, mocked Ports, and expected (hostPort, errContains) would halve the file (AGENTS.md prefers table-driven). Missing cases: a custom (non-127/0.0.0.0) hostIP, IPv6, and Docker inspect returning an empty HostIP (would currently error — worth a row to document that).

require.NoError(t, err)
require.Equal(t, 12345, hostPort)
})

t.Run("WithExposedPortDefaultHostIP", func(t *testing.T) {
testClient := dockertest.NewMockCommand2(t)
testClient.EXPECT().ContainerInspect(t.Context(), "container123").Return(&container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Status: "running",
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
NetworkSettingsBase: container.NetworkSettingsBase{
Ports: nat.PortMap{
nat.Port("5678/tcp"): []nat.PortBinding{
{
HostIP: "127.0.0.1",
HostPort: "12345",
},
},
},
},
},
}, nil)

// Empty hostIP should default to 127.0.0.1
hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "")
require.NoError(t, err)
require.Equal(t, 12345, hostPort)
})

t.Run("WithExposedPortAllInterfaces", func(t *testing.T) {
testClient := dockertest.NewMockCommand2(t)
testClient.EXPECT().ContainerInspect(t.Context(), "container123").Return(&container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
Expand All @@ -35,7 +92,7 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "0.0.0.0")
require.NoError(t, err)
require.Equal(t, 12345, hostPort)
})
Expand All @@ -54,11 +111,11 @@ func TestGetHostPortForContainer(t *testing.T) {
Ports: nat.PortMap{
nat.Port("5678/tcp"): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostIP: "127.0.0.1",
HostPort: "12345",
},
{
HostIP: "0.0.0.0",
HostIP: "127.0.0.1",
HostPort: "54321",
},
},
Expand All @@ -67,7 +124,7 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
hostPort, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")
require.NoError(t, err)
require.Equal(t, 12345, hostPort)
})
Expand All @@ -86,7 +143,7 @@ func TestGetHostPortForContainer(t *testing.T) {
Ports: nat.PortMap{
nat.Port("5678/tcp"): []nat.PortBinding{
{
HostIP: "127.0.0.1",
HostIP: "0.0.0.0",
HostPort: "12345",
},
},
Expand All @@ -95,8 +152,8 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
require.ErrorContains(t, err, "does not have a port bound to 0.0.0.0")
_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")
require.ErrorContains(t, err, "does not have a port bound to 127.0.0.1")
})

t.Run("WithDifferentPortExposed", func(t *testing.T) {
Expand All @@ -113,7 +170,7 @@ func TestGetHostPortForContainer(t *testing.T) {
Ports: nat.PortMap{
nat.Port("1234/tcp"): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostIP: "127.0.0.1",
HostPort: "12345",
},
},
Expand All @@ -122,8 +179,8 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
require.ErrorContains(t, err, "does not have a port bound to 0.0.0.0")
_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")
require.ErrorContains(t, err, "does not have a port bound to 127.0.0.1")
})

t.Run("WithNoExposedPort", func(t *testing.T) {
Expand All @@ -137,7 +194,7 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")
require.ErrorContains(t, err, "does not have expected network configuration")
})

Expand All @@ -152,7 +209,7 @@ func TestGetHostPortForContainer(t *testing.T) {
},
}, nil)

_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678)
_, err := GetHostPortForContainer(t.Context(), testClient, "container123", 5678, "127.0.0.1")
require.ErrorContains(t, err, "is not running")
})
}
5 changes: 3 additions & 2 deletions pkg/predict/predictor.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func NewPredictor(_ context.Context, opts PredictorOptions) (*Predictor, error)

func (p *Predictor) Start(ctx context.Context, logsWriter io.Writer, timeout time.Duration) (retErr error) {
containerPort := 5000
hostIP := "127.0.0.1"

if p.weightManager != nil {
mounts, err := p.weightManager.Prepare(ctx)
Expand All @@ -124,15 +125,15 @@ func (p *Predictor) Start(ctx context.Context, logsWriter io.Writer, timeout tim
}
}

p.runOptions.Ports = append(p.runOptions.Ports, command.Port{HostPort: 0, ContainerPort: containerPort})
p.runOptions.Ports = append(p.runOptions.Ports, command.Port{HostPort: 0, ContainerPort: containerPort, HostIP: hostIP})

containerID, err := docker.RunDaemon(ctx, p.dockerClient, p.runOptions, logsWriter)
if err != nil {
return fmt.Errorf("Failed to start container: %w", err)
}
p.containerID = containerID

p.port, err = docker.GetHostPortForContainer(ctx, p.dockerClient, p.containerID, containerPort)
p.port, err = docker.GetHostPortForContainer(ctx, p.dockerClient, p.containerID, containerPort, hostIP)
if err != nil {
return fmt.Errorf("Failed to determine container port: %w", err)
}
Expand Down
Loading