Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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"
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated
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})
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated

displayHost := host
if displayHost == "0.0.0.0" {
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated
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.
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated
}

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,
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated
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 {
Comment thread
anish-sahoo marked this conversation as resolved.
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")
Comment thread
anish-sahoo marked this conversation as resolved.
Outdated
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