Documentation
¶
Overview ¶
Package docker brings a devcontainer up against a Docker Engine by talking to the engine API directly — no `docker` CLI shell-out, so the package is safely embeddable in services and operators. Podman is not supported.
Compatibility with the upstream devcontainer CLI is intentional. Container labels (devcontainer.local_folder, devcontainer.config_file), marker file paths, and lifecycle command semantics mirror microsoft/devcontainers-cli so a container created here can be picked up by upstream tooling (and vice versa).
Embedding ¶
Construct a Host with a *client.Client you already manage:
cli, _ := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
h := docker.New(cli, docker.Options{Logger: log.Printf})
res, err := h.Up(ctx, docker.UpRequest{...})
The package never reads $DOCKER_HOST, never writes to stdout/stderr unless explicitly given writers in Options or per-call request, never consults global state. That makes it safe to drop into an HTTP handler, a controller-runtime reconcile loop, or a test.
relationships:
references: [../../../internal/dockerimage]
relationships:
references: [up, spawn, metadata]
Index ¶
- Constants
- Variables
- type ContainerInfo
- type ExecRequest
- type ExecResult
- type Host
- func (h *Host) Client() *dockerclient.Client
- func (h *Host) Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)
- func (h *Host) Execer(containerID string) runner.Execer
- func (h *Host) FindByLabels(ctx context.Context, labels map[string]string) (string, error)
- func (h *Host) Inspect(ctx context.Context, localWorkspace string) (*ContainerInfo, error)
- func (h *Host) InspectByID(ctx context.Context, id string) (*ContainerInfo, error)
- func (h *Host) Remove(ctx context.Context, localWorkspace string) error
- func (h *Host) RemoveByID(ctx context.Context, id string) error
- func (h *Host) Stop(ctx context.Context, localWorkspace string) error
- func (h *Host) StopByID(ctx context.Context, id string) error
- func (h *Host) Up(ctx context.Context, req UpRequest) (*UpResult, error)
- type Options
- type RegistryAuth
- type RemoteUserUIDUpdate
- type UpRequest
- type UpResult
Constants ¶
const ( LabelLocalFolder = "devcontainer.local_folder" LabelConfigFile = "devcontainer.config_file" )
Labels written by Up so containers can be rediscovered later. Names match upstream's src/spec-node/singleContainer.ts.
Variables ¶
var ErrContainerNotFound = errors.New("docker host: container not found")
ErrContainerNotFound is returned by Inspect/Stop/Remove when no container matches the supplied selector.
Functions ¶
This section is empty.
Types ¶
type ContainerInfo ¶
type ContainerInfo struct {
ID string
Image string
Running bool
StartedAt string
CreatedAt string
Labels map[string]string
WorkingDir string
}
ContainerInfo is a small projection of docker's InspectResponse. Returned by Inspect; sufficient for the most common embedder needs without exposing the full SDK type at the API boundary.
type ExecRequest ¶
type ExecRequest struct {
// TerminalSize sets the initial PTY size [rows, columns] when TTY is true.
TerminalSize *[2]uint
ContainerID string
// Cmd is the argv passed to the container. Caller is responsible
// for shell wrapping (string lifecycle commands wrap with /bin/sh -c
// before reaching here).
Cmd []string
// User is "<uid>[:<gid>]" or a username — same as docker exec -u.
User string
// WorkDir is the cwd inside the container.
WorkDir string
// Env is appended to the container's environment for this exec.
Env map[string]string
// TTY allocates a pseudo-terminal. Required for interactive shells.
TTY bool
// Stdin / Stdout / Stderr are forwarded to/from the exec session.
// Any of them may be nil; nil stdin sends EOF immediately, nil
// stdout/stderr discards that stream.
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
ExecRequest captures the inputs to a single docker exec call.
type ExecResult ¶
type ExecResult struct {
ExitCode int
// Stdout / Stderr capture output IF the request did not provide
// writers — captured-mode is the default for "did the marker check
// succeed?" probes. When the request supplies writers, these are
// empty.
Stdout []byte
Stderr []byte
}
ExecResult reports the outcome of a docker exec.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is the per-daemon connection. Safe for concurrent use.
func New ¶
func New(c *dockerclient.Client, opts Options) *Host
New wraps an existing docker client. The client lifetime is the caller's responsibility; New does not Close it.
func (*Host) Client ¶
func (h *Host) Client() *dockerclient.Client
Client returns the underlying docker client. Provided so embedders can issue ad-hoc calls (e.g. an operator that wants to attach a custom network) without re-dialing the daemon.
func (*Host) Exec ¶
func (h *Host) Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)
Exec runs a command inside an existing container and waits for it to finish. Returns the exit code and any captured output.
When Stdin/Stdout/Stderr are nil, output is captured into the returned ExecResult. When supplied, output streams to the caller's writers and the returned ExecResult buffers are empty.
func (*Host) Execer ¶
Execer returns a runner.Execer that runs commands inside the given container ID. Embedders that drive lifecycle commands themselves (rather than calling Up) can hand the result to runner.Run.
func (*Host) FindByLabels ¶
FindByLabels returns the ID of the newest container that carries every label, as upstream selects a container by its --id-label set. It returns ErrContainerNotFound when none does.
func (*Host) Inspect ¶
Inspect returns the current state of the devcontainer for the given local workspace path. Returns ErrContainerNotFound if no container matches the workspace label.
func (*Host) InspectByID ¶
InspectByID is Inspect when you already have a container ID (from e.g. Up's UpResult).
func (*Host) Remove ¶
Remove stops (if running) and removes the devcontainer for a given workspace, including its anonymous volumes.
func (*Host) RemoveByID ¶
RemoveByID is Remove when you already have a container ID.
func (*Host) Stop ¶
Stop sends SIGTERM and waits up to Options.StopTimeout for the container to exit. Returns nil even if the container was already stopped, so it's safe to call as a teardown step regardless of state.
func (*Host) Up ¶
Up brings the devcontainer up: find or create the container, start it if stopped, then run the appropriate lifecycle phases gated by marker files. Idempotent: calling Up twice on the same workspace reuses the existing container and only re-runs the phases whose markers indicate they need to.
type Options ¶
type Options struct {
// TerminalSize is the optional lifecycle PTY size [rows, columns].
TerminalSize *[2]uint
// Logger receives informational messages. If nil, messages are
// discarded — appropriate for a service or operator that handles
// its own structured logging via context.
Logger func(format string, args ...any)
// Stdout / Stderr default to nil (silent). Lifecycle command output
// is written to these unless the per-call request overrides.
Stdout io.Writer
Stderr io.Writer
// StopTimeout bounds how long Stop waits for graceful shutdown
// before SIGKILL. Defaults to 10s.
StopTimeout time.Duration
// RegistryAuth supplies the registry credentials sent with image
// pulls. When nil, pulls are anonymous. A *registryauth.Store from
// registryauth.FromEnvironment reads the docker CLI's configuration.
RegistryAuth RegistryAuth
}
Options tunes a Host. All fields are optional.
type RegistryAuth ¶
RegistryAuth returns the Engine API X-Registry-Auth value for an image reference, or "" for an anonymous request.
type RemoteUserUIDUpdate ¶
type RemoteUserUIDUpdate struct {
// HostUID / HostGID are the IDs the in-image remoteUser should be
// remapped to. Typically the embedder calls os.Getuid / os.Getgid
// on linux/macOS; the package never does so itself.
HostUID int
HostGID int
// DefaultOff requires an explicit updateRemoteUserUID: true in the spec.
DefaultOff bool
}
RemoteUserUIDUpdate, when supplied on UpRequest, asks the host to rewrite the configured remoteUser's UID/GID inside the running container so files created in bind-mounted host paths land with the invoking user's ownership instead of the image-baked user's. This is the embeddable equivalent of upstream's `updateRemoteUserUID` behavior — except we patch /etc/passwd + /etc/group at runtime via `docker exec` instead of rebuilding the image, which keeps the package shell-out-free.
The caller controls host-platform eligibility by omitting this request. The CLI enables it by default on Linux; macOS requires an explicit --update-remote-user-uid-default on flag.
The update is skipped — without error — when:
- the spec sets `updateRemoteUserUID: false`,
- DefaultOff is set and the spec does not explicitly enable remapping,
- the resolved remoteUser is "root" or a numeric UID,
- the in-image UID/GID already match HostUID/HostGID,
- or the in-image /etc/passwd does not contain remoteUser.
The rewrite is idempotent. It runs on every Up call so a host that later moves to a different UID still converges.
type UpRequest ¶
type UpRequest struct {
// ContainerDataFolder selects lifecycle and dotfiles marker storage.
ContainerDataFolder string
// ContainerSystemDataFolder selects the upstream system patch marker folder.
ContainerSystemDataFolder string
// GPUAvailability controls implicit hostRequirements.gpu requests. Empty means detect.
GPUAvailability string
// OmitConfigRemoteEnvFromMetadata omits the current config remoteEnv from new labels.
OmitConfigRemoteEnvFromMetadata bool
// Spec is the parsed devcontainer.json. Required.
Spec *spec.DevContainer
// SpecPath is the host-side path the spec was loaded from. Stored
// in the devcontainer.config_file label so upstream tooling can
// reverse-look-up the source file.
SpecPath string
// LocalWorkspace is the host-side workspace folder (absolute).
// Required: it's the discovery key (devcontainer.local_folder
// label) and the default bind-mount source.
LocalWorkspace string
// ContainerWorkspace is the path inside the container where the
// workspace is mounted. When empty, falls back to spec
// workspaceFolder, then "/workspaces/<basename>". Without a spec
// workspaceFolder, a reused container's bind mounts take precedence.
ContainerWorkspace string
// Image is the image reference to run (use pkg/builder to produce
// one). Up pulls it when the daemon does not have it.
Image string
// ImageBuilt means Image was built from this request's spec and features.
// Its label already contains those contributions; append only runtime updates.
ImageBuilt bool
// SkipLifecycle suppresses all lifecycle commands for this invocation.
SkipLifecycle bool
// SkipPostAttach suppresses postAttachCommand for this invocation.
SkipPostAttach bool
// Dotfiles installs the user's dotfiles after postCreateCommand unless
// SkipLifecycle is set.
Dotfiles runner.Dotfiles
// Lifecycle selects lifecycle stop points and prebuild behavior.
Lifecycle runner.LifecycleControls
// RemoteEnv is the caller's remote environment for lifecycle commands
// and the dotfiles install (upstream --remote-env). The configuration's
// remoteEnv wins for the same name, as upstream. It is not stored.
RemoteEnv map[string]string
// DefaultUserEnvProbe selects the shell probe when userEnvProbe is not configured.
DefaultUserEnvProbe string
// Secrets are secret environment variables for lifecycle commands and
// the dotfiles install (upstream --secrets-file). They win over every
// other layer and are never stored in container configuration, labels
// or files.
Secrets map[string]string
// Features as resolved by pkg/features. Their containerEnv and
// lifecycle commands are merged into the container.
Features []features.Resolved
// LocalEnv is the host environment used for ${localEnv:VAR}
// substitution. Pass an empty map (not nil) for no host env.
LocalEnv map[string]string
// IDLabels, when non-empty, replace the workspace labels
// (devcontainer.local_folder, devcontainer.config_file) as the
// container's identity, as upstream --id-label does: Up reuses only a
// container that carries all of them, sets exactly them on a new
// container, and derives ${devcontainerId} from them.
IDLabels map[string]string
// WorkspaceMount, when non-nil, replaces the default workspace bind
// mount (LocalWorkspace at the container workspace folder), for
// example to mount the workspace's Git root. A spec workspaceMount
// still wins.
WorkspaceMount *mount.Mount
// Mounts are added after the spec's and features' mounts, for example
// a CLI --mount or a Git worktree's common folder. Up adds no host
// path that is not in the spec or here.
Mounts []mount.Mount
// RemoteUserUIDUpdate, when non-nil, asks the host to remap the
// container's remoteUser to HostUID/HostGID after the container
// starts so bind-mounted files land with host-side ownership. The
// spec's `updateRemoteUserUID: false` overrides and disables the
// remap regardless. The package never reads os.Getuid itself —
// embedders pass the IDs they want applied.
RemoteUserUIDUpdate *RemoteUserUIDUpdate
// contains filtered or unexported fields
}
UpRequest captures everything Up needs. The caller is responsible for: building/tagging the image (use pkg/builder), resolving feature metadata, and passing the host environment in for variable substitution. Up does not read os.Environ() itself — that's the CLI's responsibility, kept out of the package so a service or operator can supply a curated environment.
type UpResult ¶
type UpResult struct {
ContainerID string
CreatedThisCall bool
StartedThisCall bool
// Phases lists the lifecycle phases that actually ran (markers may
// have skipped some).
PhasesRun []lifecycle.Hook
}
UpResult reports what happened. Fields with a "thisCall" flag denote state changes attributable to *this* Up invocation, so a caller can log "created+started" vs "reused+lifecycle-only" cleanly.