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
11 changes: 10 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ cog exec <command> [arg...] [flags]
--gpus docker run --gpus GPU devices to add to the container, in the same format as docker run --gpus.
-h, --help help for exec
--progress string Set type of build progress output, 'auto' (default), 'tty', 'plain', or 'quiet' (default "auto")
-p, --publish stringArray Publish a container's port to the host, e.g. -p 8000
-p, --publish stringArray Publish a container's port to the host, e.g. -p 8000 or -p 0.0.0.0:8000
--use-cog-base-image Use pre-built Cog base image for faster cold boots (default true)
--use-cuda-base-image string Use Nvidia CUDA base image, 'true' (default) or 'false' (use python base image). False results in a smaller image but may cause problems for non-torch projects (default "auto")
```
Expand Down 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
17 changes: 15 additions & 2 deletions docs/llms.txt

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

82 changes: 74 additions & 8 deletions pkg/cli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"errors"
"fmt"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -59,14 +60,82 @@ exploring the environment your model will run in.`,
// Flags after first argument are considered args and passed to command

// This is called `publish` for consistency with `docker run`
cmd.Flags().StringArrayVarP(&execPorts, "publish", "p", []string{}, "Publish a container's port to the host, e.g. -p 8000")
cmd.Flags().StringArrayVarP(&execPorts, "publish", "p", []string{}, "Publish a container's port to the host, e.g. -p 8000 or -p 0.0.0.0:8000")
cmd.Flags().StringArrayVarP(&envFlags, "env", "e", []string{}, "Environment variables, in the form name=value")

flags.SetInterspersed(false)

return cmd
}

// parsePublishFlags parses the values passed to `cog exec -p`. Each value may
// be either a port number ("8000") or a host:port pair ("0.0.0.0:8000" or
// "[::1]:8000"). When no host is given, the port is bound to
// command.DefaultHostIP.
func parsePublishFlags(values []string) ([]command.Port, error) {
ports := make([]command.Port, 0, len(values))
for _, portString := range values {
hostIP, portStr, err := splitPublishFlag(portString)
if err != nil {
return nil, err
}

port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("invalid port %q: %w", portString, err)
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("invalid port %q: port must be between 1 and 65535", portString)
}

ports = append(ports, command.Port{HostPort: port, ContainerPort: port, HostIP: hostIP})
}
return ports, nil
}

// splitPublishFlag splits a publish flag value into host and port parts. It
// supports plain ports ("8000"), IPv4 host:port ("0.0.0.0:8000"), bare IPv6
// ("::1:8000"), and bracketed IPv6 ("[::1]:8000").
func splitPublishFlag(value string) (host, port string, err error) {
host = command.DefaultHostIP
port = value

if value == "" {
return "", "", fmt.Errorf("invalid port %q: value cannot be empty", value)
}

// Bracketed IPv6 form: [::1]:8000
if strings.HasPrefix(value, "[") {
end := strings.Index(value, "]")
if end == -1 {
return "", "", fmt.Errorf("invalid port %q: missing closing bracket for IPv6 address", value)
}
if end == len(value)-1 {
return "", "", fmt.Errorf("invalid port %q: port is required after IPv6 address", value)
}
if value[end+1] != ':' {
return "", "", fmt.Errorf("invalid port %q: expected ':' after ']'", value)
}
host = value[1:end]
port = value[end+2:]
if host == "" {
return "", "", fmt.Errorf("invalid port %q: host cannot be empty", value)
}
return host, port, nil
}

// Standard host:port form, splitting on the last colon to tolerate IPv6.
if idx := strings.LastIndex(value, ":"); idx != -1 {
host = value[:idx]
port = value[idx+1:]
if host == "" {
return "", "", fmt.Errorf("invalid port %q: host cannot be empty", value)
}
}

return host, port, nil
}

func execCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

Expand Down Expand Up @@ -118,14 +187,11 @@ func execCmd(cmd *cobra.Command, args []string) error {
Workdir: "/src",
}

for _, portString := range execPorts {
port, err := strconv.Atoi(portString)
if err != nil {
return err
}

runOptions.Ports = append(runOptions.Ports, command.Port{HostPort: port, ContainerPort: port})
ports, err := parsePublishFlags(execPorts)
if err != nil {
return err
}
runOptions.Ports = ports

console.Info("")
console.Infof("Running %s in Docker with the current directory mounted as a volume...", console.Bold(strings.Join(args, " ")))
Expand Down
114 changes: 114 additions & 0 deletions pkg/cli/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cli

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/replicate/cog/pkg/docker/command"
)

func TestParsePublishFlags(t *testing.T) {
tests := []struct {
name string
values []string
wantPorts []command.Port
wantErr string
}{
{
name: "empty",
values: []string{},
wantPorts: []command.Port{},
},
{
name: "port only",
values: []string{"8000"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: command.DefaultHostIP},
},
},
{
name: "host:port",
values: []string{"0.0.0.0:8000"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: "0.0.0.0"},
},
},
{
name: "IPv6 host:port",
values: []string{"::1:8000"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: "::1"},
},
},
{
name: "multiple ports",
values: []string{"8000", "0.0.0.0:8888"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: command.DefaultHostIP},
{HostPort: 8888, ContainerPort: 8888, HostIP: "0.0.0.0"},
},
},
{
name: "bracketed IPv6 host:port",
values: []string{"[::1]:8000"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: "::1"},
},
},
{
name: "bracketed IPv6 with zone",
values: []string{"[::1%lo0]:8000"},
wantPorts: []command.Port{
{HostPort: 8000, ContainerPort: 8000, HostIP: "::1%lo0"},
},
},
{
name: "invalid port",
values: []string{"not-a-port"},
wantErr: "invalid port",
},
{
name: "empty value",
values: []string{""},
wantErr: "cannot be empty",
},
{
name: "empty host",
values: []string{":8000"},
wantErr: "host cannot be empty",
},
{
name: "empty port",
values: []string{"0.0.0.0:"},
wantErr: "invalid port",
},
{
name: "port out of range",
values: []string{"99999"},
wantErr: "between 1 and 65535",
},
{
name: "missing closing bracket",
values: []string{"[::1:8000"},
wantErr: "missing closing bracket",
},
{
name: "port after bracket without colon",
values: []string{"[::1]8000"},
wantErr: "expected ':' after ']'",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ports, err := parsePublishFlags(tt.values)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantPorts, ports)
})
}
}
Loading
Loading