Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c50ddf1
Add `docker model install-runner`
doringeman May 8, 2025
74ed2ed
Add `docker model uninstall-runner`
doringeman May 8, 2025
e8537b9
commands: rename install-runner.go for consistency
xenoscopic May 12, 2025
cf09faf
install: restrict platforms on which installation is supported
xenoscopic May 12, 2025
5c2ebfc
install: use CLI to construct Docker API client
xenoscopic May 12, 2025
b5f5037
install: make non-GPU install the default
xenoscopic May 12, 2025
06b6444
install: switch to JSONMessage type for status updates
xenoscopic May 12, 2025
9ab08aa
install: use strong typing for port argument
xenoscopic May 12, 2025
a3884dd
install: switch to final image name / tags
xenoscopic May 12, 2025
da2a0ac
install: extract common definitions and fix image names
xenoscopic May 12, 2025
d2b3074
install: refactor and extract install code into standalone package
xenoscopic May 12, 2025
fbe96b7
install: auto-invoke install-runner when required
xenoscopic May 13, 2025
288bdc4
compose: enable support for standalone installs
xenoscopic May 13, 2025
99dc177
all: rename DMR_HOST to MODEL_RUNNER_HOST
xenoscopic May 13, 2025
4f6be97
nit: avoid Printf whenever possible
xenoscopic May 13, 2025
fd935cc
nit: wrap errors
xenoscopic May 13, 2025
6895c5e
uninstall: simplify image removal error handling
xenoscopic May 13, 2025
4199c7f
compose: fix support for new context awareness
xenoscopic May 13, 2025
de7a6af
compose: fix formatting verb
xenoscopic May 13, 2025
b8b852e
Update commands/uninstall-runner.go
xenoscopic May 13, 2025
59cbdd5
uninstall: use image name constants and clarify uninstall behavior
xenoscopic May 13, 2025
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
20 changes: 16 additions & 4 deletions commands/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ func newComposeCmd() *cobra.Command {
c := &cobra.Command{
Use: "compose EVENT",
}
c.AddCommand(newUpCommand(desktopClient))
c.AddCommand(newUpCommand())
c.AddCommand(newDownCommand())
c.Hidden = true
c.PersistentFlags().String("project-name", "", "compose project name") // unused by model

return c
}

func newUpCommand(desktopClient *desktop.Client) *cobra.Command {
func newUpCommand() *cobra.Command {
var model string
c := &cobra.Command{
Use: "up",
Expand All @@ -33,6 +33,11 @@ func newUpCommand(desktopClient *desktop.Client) *cobra.Command {
return err
}

if err := ensureStandaloneRunnerAvailable(cmd.Context(), nil); err != nil {
sendErrorf("Failed to initialize standalone model runner: %v", err)
return fmt.Errorf("Failed to initialize standalone model runner: %w", err)
}

_, _, err := desktopClient.Pull(model, func(s string) {
sendInfo(s)
})
Expand All @@ -41,8 +46,15 @@ func newUpCommand(desktopClient *desktop.Client) *cobra.Command {
return fmt.Errorf("Failed to pull model: %v\n", err)
}

// FIXME get actual URL from Docker Desktop
setenv("URL", "http://model-runner.docker.internal/engines/v1/")
if kind := modelRunner.EngineKind(); kind == desktop.ModelRunnerEngineKindDesktop {
// TODO: Get the actual URL from Docker Desktop via some API.
setenv("URL", "http://model-runner.docker.internal/engines/v1/")
} else if kind == desktop.ModelRunnerEngineKindMobyManual {
setenv("URL", modelRunner.URL("/engines/v1/"))
} else if kind == desktop.ModelRunnerEngineKindMoby || kind == desktop.ModelRunnerEngineKindCloud {
// TODO: Find a more robust solution in Moby-like environments.
setenv("URL", "http://172.17.0.1:12434/engines/v1/")
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey @glours and @ndeloof - we're trying to figure out the best way to make the DMR + Compose integration work with the standalone Docker Model Runner containers that we're now supporting on Docker CE. Since Compose projects don't attach their service containers to the host bridge network by default, using the 172.17.0.1 address doesn't work. I'm trying to figure out a more elegant solution without requiring users to change their Compose YAML. Do you have any ideas about a good convention that we could use here?

Copy link
Contributor

Choose a reason for hiding this comment

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

The best idea I have at the moment is just to default to http://host.docker.internal:12434/engines/v1/ and require a host.docker.internal:host-gateway mapping for every container needing the standalone model runner.

}
setenv("MODEL", model)

return nil
Expand Down
3 changes: 3 additions & 0 deletions commands/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ func newInspectCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
inspectedModel, err := inspectModel(args, openai, desktopClient)
if err != nil {
return err
Expand Down
141 changes: 141 additions & 0 deletions commands/install-runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package commands

import (
"context"
"fmt"
"os"
"time"

"github.com/docker/model-cli/commands/completion"
"github.com/docker/model-cli/desktop"
"github.com/docker/model-cli/pkg/standalone"
"github.com/spf13/cobra"
)

type noopPrinter struct{}

func (*noopPrinter) Printf(format string, args ...any) {}

func (*noopPrinter) Println(args ...any) {}

// ensureStandaloneRunnerAvailable is a utility function that other commands can
// use to initialize a default standalone model runner. It is a no-op in
// unsupported contexts or if automatic installs have been disabled.
func ensureStandaloneRunnerAvailable(ctx context.Context, printer standalone.StatusPrinter) error {
// If we're not in a supported model runner context, then don't do anything.
if modelRunner.EngineKind() != desktop.ModelRunnerEngineKindMoby {
return nil
}

// If automatic installation has been disabled, then don't do anything.
if os.Getenv("MODEL_RUNNER_NO_AUTO_INSTALL") != "" {
return nil
}

// Ensure that the output printer is non-nil.
if printer == nil {
printer = &noopPrinter{}
}

// Create a Docker client for the active context.
dockerClient, err := desktop.DockerClientForContext(dockerCLI, dockerCLI.CurrentContext())
if err != nil {
return fmt.Errorf("failed to create Docker client: %w", err)
}

// Check if a model runner container exists.
container, _, err := standalone.FindControllerContainer(ctx, dockerClient)
if err != nil {
return fmt.Errorf("unable to identify existing standalone model runner: %w", err)
} else if container != "" {
return nil
}

// Ensure that we have an up-to-date copy of the image.
if err := standalone.EnsureControllerImage(ctx, dockerClient, false, printer); err != nil {
return fmt.Errorf("unable to pull latest standalone model runner image: %w", err)
}

// Ensure that we have a model storage volume.
modelStorageVolume, err := standalone.EnsureModelStorageVolume(ctx, dockerClient, printer)
if err != nil {
return fmt.Errorf("unable to initialize standalone model storage: %w", err)
}

// Create the model runner container.
if err := standalone.CreateControllerContainer(ctx, dockerClient, standalone.DefaultControllerPort, false, modelStorageVolume, printer); err != nil {
return fmt.Errorf("unable to initialize standalone model runner container: %w", err)
}

// Give the model runner one second to start.
time.Sleep(1 * time.Second)

return nil
}

func newInstallRunner() *cobra.Command {
var port uint16
var gpu bool
c := &cobra.Command{
Use: "install-runner",
Short: "Install Docker Model Runner",
RunE: func(cmd *cobra.Command, args []string) error {
// Ensure that we're running in a supported model runner context.
if kind := modelRunner.EngineKind(); kind == desktop.ModelRunnerEngineKindDesktop {
// TODO: We may eventually want to auto-forward this to
// docker desktop enable model-runner, but we should first make
// sure the CLI flags match.
cmd.Println("Standalone installation not supported with Docker Desktop")
cmd.Println("Use `docker desktop enable model-runner` instead")
return nil
} else if kind == desktop.ModelRunnerEngineKindMobyManual {
cmd.Println("Standalone installation not supported with MODEL_RUNNER_HOST set")
return nil
} else if kind == desktop.ModelRunnerEngineKindCloud {
cmd.Println("Standalone installation not required with Docker Cloud")
return nil
}

// Create a Docker client for the active context.
dockerClient, err := desktop.DockerClientForContext(dockerCLI, dockerCLI.CurrentContext())
if err != nil {
return fmt.Errorf("failed to create Docker client: %w", err)
}

// Check if an active model runner container already exists.
if ctrID, ctrName, err := standalone.FindControllerContainer(cmd.Context(), dockerClient); err != nil {
return err
} else if ctrID != "" {
if ctrName != "" {
cmd.Printf("Model Runner container %s (%s) is already running\n", ctrName, ctrID[:12])
} else {
cmd.Printf("Model Runner container %s is already running\n", ctrID[:12])
}
return nil
}

// Ensure that we have an up-to-date copy of the image.
if err := standalone.EnsureControllerImage(cmd.Context(), dockerClient, gpu, cmd); err != nil {
return fmt.Errorf("unable to pull latest standalone model runner image: %w", err)
}

// Ensure that we have a model storage volume.
modelStorageVolume, err := standalone.EnsureModelStorageVolume(cmd.Context(), dockerClient, cmd)
if err != nil {
return fmt.Errorf("unable to initialize standalone model storage: %w", err)
}

// Create the model runner container.
if err := standalone.CreateControllerContainer(cmd.Context(), dockerClient, port, gpu, modelStorageVolume, cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner container: %w", err)
}

return nil
},
ValidArgsFunction: completion.NoComplete,
}
c.Flags().Uint16Var(&port, "port", standalone.DefaultControllerPort,
"Docker container port for Docker Model Runner")
c.Flags().BoolVar(&gpu, "gpu", false, "Enable GPU support")
return c
}
3 changes: 3 additions & 0 deletions commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func newListCmd() *cobra.Command {
if openai && quiet {
return fmt.Errorf("--quiet flag cannot be used with --openai flag")
}
if err := ensureStandaloneRunnerAvailable(cmd.Context(), nil); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
models, err := listModels(openai, desktopClient, quiet, jsonFormat)
if err != nil {
return err
Expand Down
3 changes: 3 additions & 0 deletions commands/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func newPullCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
return pullModel(cmd, desktopClient, args[0])
},
ValidArgsFunction: completion.NoComplete,
Expand Down
3 changes: 3 additions & 0 deletions commands/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func newPushCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
return pushModel(cmd, desktopClient, args[0])
},
ValidArgsFunction: completion.NoComplete,
Expand Down
3 changes: 3 additions & 0 deletions commands/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ func newRemoveCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
response, err := desktopClient.Remove(args, force)
if response != "" {
cmd.Println(response)
Expand Down
8 changes: 7 additions & 1 deletion commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"github.com/spf13/cobra"
)

// dockerCLI is the Docker CLI environment associated with the command.
var dockerCLI *command.DockerCli

// modelRunner is the model runner context. It is initialized by the root
// command's PersistentPreRunE.
var modelRunner *desktop.ModelRunnerContext
Expand Down Expand Up @@ -41,10 +44,11 @@ func NewRootCmd(cli *command.DockerCli) *cobra.Command {
} else if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
dockerCLI = cli

// Detect the model runner context and create a client for it.
var err error
modelRunner, err = desktop.DetectContext(cli)
modelRunner, err = desktop.DetectContext(dockerCLI)
if err != nil {
return fmt.Errorf("unable to detect model runner context: %w", err)
}
Expand Down Expand Up @@ -81,6 +85,8 @@ func NewRootCmd(cli *command.DockerCli) *cobra.Command {
newInspectCmd(),
newComposeCmd(),
newTagCmd(),
newInstallRunner(),
newUninstallRunner(),
)
return rootCmd
}
4 changes: 4 additions & 0 deletions commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ func newRunCmd() *cobra.Command {
}
}

if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}

modelDetail, err := desktopClient.Inspect(model)
if err != nil {
if !errors.Is(err, desktop.ErrNotFound) {
Expand Down
4 changes: 4 additions & 0 deletions commands/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"encoding/json"
"fmt"
"os"

"github.com/docker/cli/cli-plugins/hooks"
Expand All @@ -14,6 +15,9 @@ func newStatusCmd() *cobra.Command {
Use: "status",
Short: "Check if the Docker Model Runner is running",
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
status := desktopClient.Status()
if status.Error != nil {
return handleClientError(status.Error, "Failed to get Docker Model Runner status")
Expand Down
3 changes: 3 additions & 0 deletions commands/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ func newTagCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
return tagModel(cmd, desktopClient, args[0], args[1])
},
}
Expand Down
66 changes: 66 additions & 0 deletions commands/uninstall-runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package commands

import (
"fmt"

"github.com/docker/model-cli/commands/completion"
"github.com/docker/model-cli/desktop"
"github.com/docker/model-cli/pkg/standalone"
"github.com/spf13/cobra"
)

func newUninstallRunner() *cobra.Command {
var models, images bool
c := &cobra.Command{
Use: "uninstall-runner",
Short: "Uninstall Docker Model Runner",
RunE: func(cmd *cobra.Command, args []string) error {
// Ensure that we're running in a supported model runner context.
if kind := modelRunner.EngineKind(); kind == desktop.ModelRunnerEngineKindDesktop {
// TODO: We may eventually want to auto-forward this to
// docker desktop disable model-runner, but we should first
// make install-runner forward in the same way.
cmd.Println("Standalone uninstallation not supported with Docker Desktop")
cmd.Println("Use `docker desktop disable model-runner` instead")
return nil
} else if kind == desktop.ModelRunnerEngineKindMobyManual {
cmd.Println("Standalone uninstallation not supported with MODEL_RUNNER_HOST set")
return nil
} else if kind == desktop.ModelRunnerEngineKindCloud {
cmd.Println("Standalone uninstallation not supported with Docker Cloud")
return nil
}

// Create a Docker client for the active context.
dockerClient, err := desktop.DockerClientForContext(dockerCLI, dockerCLI.CurrentContext())
if err != nil {
return fmt.Errorf("failed to create Docker client: %w", err)
}

// Remove any model runner containers.
if err := standalone.PruneControllerContainers(cmd.Context(), dockerClient, cmd); err != nil {
return fmt.Errorf("unable to remove model runner container(s): %w", err)
}

// Remove model runner images, if requested.
if images {
if err := standalone.PruneControllerImages(cmd.Context(), dockerClient, cmd); err != nil {
return fmt.Errorf("unable to remove model runner image(s): %w", err)
}
}

// Remove model storage, if requested.
if models {
if err := standalone.PruneModelStorageVolumes(cmd.Context(), dockerClient, cmd); err != nil {
return fmt.Errorf("unable to remove model storage volume(s): %w", err)
}
}

return nil
},
ValidArgsFunction: completion.NoComplete,
}
c.Flags().BoolVar(&models, "models", false, "Remove model storage volume")
c.Flags().BoolVar(&images, "images", false, "Remove "+standalone.ControllerImage+" images")
return c
}
Loading