diff --git a/.github/workflows/cli-validate.yml b/.github/workflows/cli-validate.yml index 2ee7f8c79..f2098b35e 100644 --- a/.github/workflows/cli-validate.yml +++ b/.github/workflows/cli-validate.yml @@ -62,5 +62,4 @@ jobs: uses: docker/bake-action@v6 with: files: ./cmd/cli/docker-bake.hcl - workdir: ./cmd/cli targets: ${{ matrix.target }} diff --git a/cmd/cli/Dockerfile b/cmd/cli/Dockerfile index 73a059781..d45e73505 100644 --- a/cmd/cli/Dockerfile +++ b/cmd/cli/Dockerfile @@ -39,7 +39,7 @@ RUN --mount=target=/context \ git add -A rm -rf cmd/cli/docs/reference/* cp -rf /out/* ./cmd/cli/docs/reference/ - if [ -n "$(git status --porcelain -- docs/reference)" ]; then + if [ -n "$(git status --porcelain -- cmd/cli/docs/reference)" ]; then echo >&2 'ERROR: Docs result differs. Please update with "make docs"' git status --porcelain -- cmd/cli/docs/reference exit 1 diff --git a/cmd/cli/commands/configure.go b/cmd/cli/commands/configure.go index 6f86568f6..848be0587 100644 --- a/cmd/cli/commands/configure.go +++ b/cmd/cli/commands/configure.go @@ -12,9 +12,8 @@ func newConfigureCmd() *cobra.Command { var opts scheduling.ConfigureRequest c := &cobra.Command{ - Use: "configure [--context-size=] MODEL [-- ]", - Short: "Configure runtime options for a model", - Hidden: true, + Use: "configure [--context-size=] MODEL [-- ]", + Short: "Configure runtime options for a model", Args: func(cmd *cobra.Command, args []string) error { argsBeforeDash := cmd.ArgsLenAtDash() if argsBeforeDash == -1 { diff --git a/cmd/cli/commands/install-runner.go b/cmd/cli/commands/install-runner.go index 6878e3164..62fca1079 100644 --- a/cmd/cli/commands/install-runner.go +++ b/cmd/cli/commands/install-runner.go @@ -127,12 +127,15 @@ func ensureStandaloneRunnerAvailable(ctx context.Context, printer standalone.Sta // Create the model runner container. port := uint16(standalone.DefaultControllerPortMoby) + // For auto-installation, always bind to localhost for security. + // Users can run install-runner explicitly with --host to change this. + host := "127.0.0.1" environment := "moby" if engineKind == types.ModelRunnerEngineKindCloud { port = standalone.DefaultControllerPortCloud environment = "cloud" } - if err := standalone.CreateControllerContainer(ctx, dockerClient, port, environment, false, gpu, modelStorageVolume, printer, engineKind); err != nil { + if err := standalone.CreateControllerContainer(ctx, dockerClient, port, host, environment, false, gpu, modelStorageVolume, printer, engineKind); err != nil { return nil, fmt.Errorf("unable to initialize standalone model runner container: %w", err) } @@ -159,6 +162,7 @@ func ensureStandaloneRunnerAvailable(ctx context.Context, printer standalone.Sta func newInstallRunner() *cobra.Command { var port uint16 + var host string var gpuMode string var doNotTrack bool c := &cobra.Command{ @@ -245,7 +249,7 @@ func newInstallRunner() *cobra.Command { return fmt.Errorf("unable to initialize standalone model storage: %w", err) } // Create the model runner container. - if err := standalone.CreateControllerContainer(cmd.Context(), dockerClient, port, environment, doNotTrack, gpu, modelStorageVolume, cmd, engineKind); err != nil { + if err := standalone.CreateControllerContainer(cmd.Context(), dockerClient, port, host, environment, doNotTrack, gpu, modelStorageVolume, cmd, engineKind); err != nil { return fmt.Errorf("unable to initialize standalone model runner container: %w", err) } @@ -256,6 +260,7 @@ func newInstallRunner() *cobra.Command { } c.Flags().Uint16Var(&port, "port", 0, "Docker container port for Docker Model Runner (default: 12434 for Docker CE, 12435 for Cloud mode)") + c.Flags().StringVar(&host, "host", "127.0.0.1", "Host address to bind Docker Model Runner") c.Flags().StringVar(&gpuMode, "gpu", "auto", "Specify GPU support (none|auto|cuda)") c.Flags().BoolVar(&doNotTrack, "do-not-track", false, "Do not track models usage in Docker Model Runner") return c diff --git a/cmd/cli/commands/install-runner_test.go b/cmd/cli/commands/install-runner_test.go new file mode 100644 index 000000000..bce0d8131 --- /dev/null +++ b/cmd/cli/commands/install-runner_test.go @@ -0,0 +1,96 @@ +package commands + +import ( + "testing" +) + +func TestInstallRunnerHostFlag(t *testing.T) { + // Create the install-runner command + cmd := newInstallRunner() + + // Verify the --host flag exists + hostFlag := cmd.Flags().Lookup("host") + if hostFlag == nil { + t.Fatal("--host flag not found") + } + + // Verify the default value + if hostFlag.DefValue != "127.0.0.1" { + t.Errorf("Expected default host value to be '127.0.0.1', got '%s'", hostFlag.DefValue) + } + + // Verify the flag type + if hostFlag.Value.Type() != "string" { + t.Errorf("Expected host flag type to be 'string', got '%s'", hostFlag.Value.Type()) + } + + // Test setting the flag value + testCases := []struct { + name string + value string + }{ + {"localhost", "127.0.0.1"}, + {"all interfaces", "0.0.0.0"}, + {"specific IP", "192.168.1.100"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Reset the command for each test + cmd := newInstallRunner() + err := cmd.Flags().Set("host", tc.value) + if err != nil { + t.Errorf("Failed to set host flag to '%s': %v", tc.value, err) + } + + // Verify the value was set + hostValue, err := cmd.Flags().GetString("host") + if err != nil { + t.Errorf("Failed to get host flag value: %v", err) + } + if hostValue != tc.value { + t.Errorf("Expected host value to be '%s', got '%s'", tc.value, hostValue) + } + }) + } +} + +func TestInstallRunnerCommandFlags(t *testing.T) { + cmd := newInstallRunner() + + // Verify all expected flags exist + expectedFlags := []string{"port", "host", "gpu", "do-not-track"} + for _, flagName := range expectedFlags { + if cmd.Flags().Lookup(flagName) == nil { + t.Errorf("Expected flag '--%s' not found", flagName) + } + } +} + +func TestInstallRunnerCommandType(t *testing.T) { + cmd := newInstallRunner() + + // Verify command properties + if cmd.Use != "install-runner" { + t.Errorf("Expected command Use to be 'install-runner', got '%s'", cmd.Use) + } + + if cmd.Short != "Install Docker Model Runner (Docker Engine only)" { + t.Errorf("Unexpected command Short description: %s", cmd.Short) + } + + // Verify RunE is set + if cmd.RunE == nil { + t.Error("Expected RunE to be set") + } +} + +func TestInstallRunnerValidArgsFunction(t *testing.T) { + cmd := newInstallRunner() + + // The install-runner command should not accept any arguments + // So ValidArgsFunction should be set to handle no arguments + if cmd.ValidArgsFunction == nil { + t.Error("Expected ValidArgsFunction to be set") + } +} diff --git a/cmd/cli/docs/reference/docker_model.yaml b/cmd/cli/docs/reference/docker_model.yaml index 19c4c7110..a43b12a83 100644 --- a/cmd/cli/docs/reference/docker_model.yaml +++ b/cmd/cli/docs/reference/docker_model.yaml @@ -6,6 +6,7 @@ long: |- pname: docker plink: docker.yaml cname: + - docker model configure - docker model df - docker model inspect - docker model install-runner @@ -24,6 +25,7 @@ cname: - docker model unload - docker model version clink: + - docker_model_configure.yaml - docker_model_df.yaml - docker_model_inspect.yaml - docker_model_install-runner.yaml diff --git a/cmd/cli/docs/reference/docker_model_configure.yaml b/cmd/cli/docs/reference/docker_model_configure.yaml index 86af6e42b..c132d5158 100644 --- a/cmd/cli/docs/reference/docker_model_configure.yaml +++ b/cmd/cli/docs/reference/docker_model_configure.yaml @@ -16,7 +16,7 @@ options: kubernetes: false swarm: false deprecated: false -hidden: true +hidden: false experimental: false experimentalcli: false kubernetes: false diff --git a/cmd/cli/docs/reference/docker_model_install-runner.yaml b/cmd/cli/docs/reference/docker_model_install-runner.yaml index f389bc9b3..67cec4dc5 100644 --- a/cmd/cli/docs/reference/docker_model_install-runner.yaml +++ b/cmd/cli/docs/reference/docker_model_install-runner.yaml @@ -26,6 +26,16 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: host + value_type: string + default_value: 127.0.0.1 + description: Host address to bind Docker Model Runner + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: port value_type: uint16 default_value: "0" diff --git a/cmd/cli/docs/reference/docker_model_package.yaml b/cmd/cli/docs/reference/docker_model_package.yaml index 712a94804..bed0cd43c 100644 --- a/cmd/cli/docs/reference/docker_model_package.yaml +++ b/cmd/cli/docs/reference/docker_model_package.yaml @@ -1,10 +1,11 @@ command: docker model package short: | - Package a GGUF file into a Docker model OCI artifact, with optional licenses. + Package a GGUF file or Safetensors directory into a Docker model OCI artifact. long: |- - Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified. - When packaging a sharded model --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf). -usage: docker model package --gguf [--license ...] [--context-size ] [--push] MODEL + Package a GGUF file or Safetensors directory into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified. + When packaging a sharded GGUF model, --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf). + When packaging a Safetensors model, --safetensors-dir should point to a directory containing .safetensors files and config files (*.json, merges.txt). All files will be auto-discovered and config files will be packaged into a tar archive. +usage: docker model package (--gguf | --safetensors-dir ) [--license ...] [--context-size ] [--push] MODEL pname: docker model plink: docker_model.yaml options: @@ -29,7 +30,7 @@ options: swarm: false - option: gguf value_type: string - description: absolute path to gguf file (required) + description: absolute path to gguf file deprecated: false hidden: false experimental: false @@ -58,6 +59,15 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: safetensors-dir + value_type: string + description: absolute path to directory containing safetensors files and config + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false deprecated: false hidden: false experimental: false diff --git a/cmd/cli/docs/reference/model.md b/cmd/cli/docs/reference/model.md index ecfefdd3d..9aac7bed4 100644 --- a/cmd/cli/docs/reference/model.md +++ b/cmd/cli/docs/reference/model.md @@ -5,25 +5,26 @@ Docker Model Runner ### Subcommands -| Name | Description | -|:------------------------------------------------|:------------------------------------------------------------------------------| -| [`df`](model_df.md) | Show Docker Model Runner disk usage | -| [`inspect`](model_inspect.md) | Display detailed information on one model | -| [`install-runner`](model_install-runner.md) | Install Docker Model Runner (Docker Engine only) | -| [`list`](model_list.md) | List the models pulled to your local environment | -| [`logs`](model_logs.md) | Fetch the Docker Model Runner logs | -| [`package`](model_package.md) | Package a GGUF file into a Docker model OCI artifact, with optional licenses. | -| [`ps`](model_ps.md) | List running models | -| [`pull`](model_pull.md) | Pull a model from Docker Hub or HuggingFace to your local environment | -| [`push`](model_push.md) | Push a model to Docker Hub | -| [`requests`](model_requests.md) | Fetch requests+responses from Docker Model Runner | -| [`rm`](model_rm.md) | Remove local models downloaded from Docker Hub | -| [`run`](model_run.md) | Run a model and interact with it using a submitted prompt or chat mode | -| [`status`](model_status.md) | Check if the Docker Model Runner is running | -| [`tag`](model_tag.md) | Tag a model | -| [`uninstall-runner`](model_uninstall-runner.md) | Uninstall Docker Model Runner | -| [`unload`](model_unload.md) | Unload running models | -| [`version`](model_version.md) | Show the Docker Model Runner version | +| Name | Description | +|:------------------------------------------------|:-------------------------------------------------------------------------------| +| [`configure`](model_configure.md) | Configure runtime options for a model | +| [`df`](model_df.md) | Show Docker Model Runner disk usage | +| [`inspect`](model_inspect.md) | Display detailed information on one model | +| [`install-runner`](model_install-runner.md) | Install Docker Model Runner (Docker Engine only) | +| [`list`](model_list.md) | List the models pulled to your local environment | +| [`logs`](model_logs.md) | Fetch the Docker Model Runner logs | +| [`package`](model_package.md) | Package a GGUF file or Safetensors directory into a Docker model OCI artifact. | +| [`ps`](model_ps.md) | List running models | +| [`pull`](model_pull.md) | Pull a model from Docker Hub or HuggingFace to your local environment | +| [`push`](model_push.md) | Push a model to Docker Hub | +| [`requests`](model_requests.md) | Fetch requests+responses from Docker Model Runner | +| [`rm`](model_rm.md) | Remove local models downloaded from Docker Hub | +| [`run`](model_run.md) | Run a model and interact with it using a submitted prompt or chat mode | +| [`status`](model_status.md) | Check if the Docker Model Runner is running | +| [`tag`](model_tag.md) | Tag a model | +| [`uninstall-runner`](model_uninstall-runner.md) | Uninstall Docker Model Runner | +| [`unload`](model_unload.md) | Unload running models | +| [`version`](model_version.md) | Show the Docker Model Runner version | diff --git a/cmd/cli/docs/reference/model_install-runner.md b/cmd/cli/docs/reference/model_install-runner.md index bbbd38af2..e8911ccaa 100644 --- a/cmd/cli/docs/reference/model_install-runner.md +++ b/cmd/cli/docs/reference/model_install-runner.md @@ -5,11 +5,12 @@ Install Docker Model Runner (Docker Engine only) ### Options -| Name | Type | Default | Description | -|:-----------------|:---------|:--------|:---------------------------------------------------------------------------------------------------| -| `--do-not-track` | `bool` | | Do not track models usage in Docker Model Runner | -| `--gpu` | `string` | `auto` | Specify GPU support (none\|auto\|cuda) | -| `--port` | `uint16` | `0` | Docker container port for Docker Model Runner (default: 12434 for Docker CE, 12435 for Cloud mode) | +| Name | Type | Default | Description | +|:-----------------|:---------|:------------|:---------------------------------------------------------------------------------------------------| +| `--do-not-track` | `bool` | | Do not track models usage in Docker Model Runner | +| `--gpu` | `string` | `auto` | Specify GPU support (none\|auto\|cuda) | +| `--host` | `string` | `127.0.0.1` | Host address to bind Docker Model Runner | +| `--port` | `uint16` | `0` | Docker container port for Docker Model Runner (default: 12434 for Docker CE, 12435 for Cloud mode) | diff --git a/cmd/cli/docs/reference/model_package.md b/cmd/cli/docs/reference/model_package.md index a0448f79f..262070ac5 100644 --- a/cmd/cli/docs/reference/model_package.md +++ b/cmd/cli/docs/reference/model_package.md @@ -1,18 +1,20 @@ # docker model package -Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified. -When packaging a sharded model --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf). +Package a GGUF file or Safetensors directory into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified. +When packaging a sharded GGUF model, --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf). +When packaging a Safetensors model, --safetensors-dir should point to a directory containing .safetensors files and config files (*.json, merges.txt). All files will be auto-discovered and config files will be packaged into a tar archive. ### Options -| Name | Type | Default | Description | -|:------------------|:--------------|:--------|:---------------------------------------------------------------------------------------| -| `--chat-template` | `string` | | absolute path to chat template file (must be Jinja format) | -| `--context-size` | `uint64` | `0` | context size in tokens | -| `--gguf` | `string` | | absolute path to gguf file (required) | -| `-l`, `--license` | `stringArray` | | absolute path to a license file | -| `--push` | `bool` | | push to registry (if not set, the model is loaded into the Model Runner content store) | +| Name | Type | Default | Description | +|:--------------------|:--------------|:--------|:---------------------------------------------------------------------------------------| +| `--chat-template` | `string` | | absolute path to chat template file (must be Jinja format) | +| `--context-size` | `uint64` | `0` | context size in tokens | +| `--gguf` | `string` | | absolute path to gguf file | +| `-l`, `--license` | `stringArray` | | absolute path to a license file | +| `--push` | `bool` | | push to registry (if not set, the model is loaded into the Model Runner content store) | +| `--safetensors-dir` | `string` | | absolute path to directory containing safetensors files and config | diff --git a/cmd/cli/pkg/standalone/containers.go b/cmd/cli/pkg/standalone/containers.go index d28d6c42a..aed796ea1 100644 --- a/cmd/cli/pkg/standalone/containers.go +++ b/cmd/cli/pkg/standalone/containers.go @@ -218,7 +218,7 @@ func ensureContainerStarted(ctx context.Context, dockerClient client.ContainerAP } // CreateControllerContainer creates and starts a controller container. -func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, port uint16, environment string, doNotTrack bool, gpu gpupkg.GPUSupport, modelStorageVolume string, printer StatusPrinter, engineKind types.ModelRunnerEngineKind) error { +func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, port uint16, host string, environment string, doNotTrack bool, gpu gpupkg.GPUSupport, modelStorageVolume string, printer StatusPrinter, engineKind types.ModelRunnerEngineKind) error { // Determine the target image. var imageName string switch gpu { @@ -260,11 +260,14 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, Name: "always", }, } - portBindings := []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: portStr}} + portBindings := []nat.PortBinding{{HostIP: host, HostPort: portStr}} if os.Getenv("_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY") != "1" { // Don't bind the bridge gateway IP if we're treating Docker Desktop as Moby. - if bridgeGatewayIP, err := determineBridgeGatewayIP(ctx, dockerClient); err == nil && bridgeGatewayIP != "" { - portBindings = append(portBindings, nat.PortBinding{HostIP: bridgeGatewayIP, HostPort: portStr}) + // Only add bridge gateway IP binding if host is 127.0.0.1 + if host == "127.0.0.1" { + if bridgeGatewayIP, err := determineBridgeGatewayIP(ctx, dockerClient); err == nil && bridgeGatewayIP != "" { + portBindings = append(portBindings, nat.PortBinding{HostIP: bridgeGatewayIP, HostPort: portStr}) + } } } hostConfig.PortBindings = nat.PortMap{