devcontainer

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

devcontainer

CI Go Reference

A programmatic Go runtime for Dev Containers. Embed the full devcontainer lifecycle — resolve, build, up, exec, lifecycle phases, down — into your Go application without shelling out to the Node @devcontainers/cli.

Why

The reference @devcontainers/cli is a Node binary. Embedding it in a Go service means a Node runtime dependency, opaque failure modes (success exit codes with outcome:error JSON on stdout), and CLI-flag-shaped APIs for every interaction. This library is a clean Go implementation of the spec's embedding-relevant subset, designed to be a drop-in replacement for shelling out.

Status

Alpha. API is stable enough for early integration but may change between minor versions until v1.0.0. The events channel surface is explicitly experimental.

Backends

The container backend is pluggable. Pick one at engine construction time:

  • runtime/docker — Docker Engine over moby/moby/client. Default choice; requires a reachable Docker daemon socket.
  • runtime/applecontainer — Apple's container runtime on darwin/arm64 (macOS 15+). Talks to Apple's apiserver through an embedded Swift bridge (libACBridge.dylib, dlopen'd at runtime). Lets you run devcontainers on Apple Silicon without Docker Desktop.

Both backends implement the same runtime.Runtime interface — the engine, feature pipeline, lifecycle, and compose paths don't care which one you wire in.

Apple-container gotchas

Things specific to runtime/applecontainer that don't apply to the Docker backend. None of these are bugs in this library; they reflect the current state of Apple's container runtime (0.12.x).

  • Daemon + builder lifecycle is manual. Run container system start once per boot; container builder start once per machine before any build-source devcontainer or features install. Engine surfaces a typed runtime.BuilderUnavailableError with a remediation hint when the builder is down.
  • Image-store credentials are separate from Docker's. Pulls from private registries require container registry login <host> before Up; ~/.docker/config.json is not consulted.
  • Multi-arch base images. Apple's BuildKit shim ships amd64-only, so the builder VM always runs amd64 (Rosetta on Apple Silicon). Output images target the host platform (arm64) by default — feature builds produce native arm64 images that run without Rosetta. If your FROM base image is amd64-only, the resulting image is also amd64 and the runtime container needs Rosetta-for-Linux to boot.
  • No amd64 default kernel. Running amd64 containers requires installing the amd64 kernel explicitly: container system kernel set --tar <url-to-amd64-tarball> --arch amd64. Don't use --force without --arch; it can overwrite the arm64 default registration.
  • Port forwarding (forwardPorts / compose ports:) is parsed but not actuated. Apple's networking model differs from Docker's host-port-publish; on this backend ports are silently dropped today.
  • Compose feature gates that depend on upstream apple/container fixes are refused at Plan-validate time with typed errors: service_healthy / service_completed_successfully (no healthcheck surface yet — apple/container #1502, #1501), namespace sharing modes (network_mode: service:<x>, pid:, ipc: — architectural, VM-per-container), shared named volumes (ext4 single-mount — apple/container #889). restart: policies are silently ignored with a one-shot warning (apple/container #286).
  • Service-name DNS is not native on the project network (apple/container #856). The compose orchestrator patches /etc/hosts in each running container after each level so depends_on-declared peers resolve; intra-level peers without an explicit edge can lose a first lookup. Documented limitation.
Spec compliance

Status of each Dev Containers spec field/behavior the library covers. Legend: ✅ acted on · ⚠️ parsed but not enforced (or partial) · ❌ missing · ➖ out of scope.

Sources

Field Status Notes
image Pull, run, exec
build (dockerfile, context, args, target, cacheFrom) User Dockerfile + features layered atop
dockerComposeFile, service, runServices compose-go parse + either shell-out to docker compose (default) or an in-process orchestrator (EngineOptions.ComposeBackend = ComposeBackendNative). The native orchestrator drives any Runtime implementing the compose primitives — works against both runtime/docker and runtime/applecontainer.

Container config

Field Status Notes
workspaceFolder, workspaceMount
mounts (bind / volume / tmpfs)
containerEnv, remoteEnv
containerUser, remoteUser
updateRemoteUserUID Portable shell (Debian, Alpine/BusyBox)
userEnvProbe (all four modes)
runArgs, init, privileged, capAdd, securityOpt, overrideCommand
shutdownAction Honored via Engine.Shutdown (none / stopContainer / stopCompose)

Features

Field Status Notes
features (OCI / HTTPS / local) DAG ordering, options validation, content-addressed cache
dependsOn / installsAfter / overrideFeatureInstallOrder
Pre-baked-image short-circuit devcontainer.metadata label read on Up
devcontainer-lock.json #26

Lifecycle

Phase Status Notes
initializeCommand (host) Opt-in via UpOptions.RunInitializeCommand; requires EngineOptions.HostExecutor
onCreateCommand, updateContentCommand, postCreateCommand Run-once idempotency markers
postStartCommand, postAttachCommand Run-every-start / run-every-Up
waitFor
Parallel command form (object → named commands run concurrently)
secretsCommand Opt-in via UpOptions.RunSecretsCommand; requires EngineOptions.HostExecutor

Substitution

Variable Status Notes
${localWorkspaceFolder[Basename]}, ${containerWorkspaceFolder[Basename]}
${localEnv:VAR[:default]}, ${devcontainerId}
${containerEnv:VAR[:default]} Two-pass: host context pre-create, re-applied per Exec via the workspace substituter

Ports

Field Status Notes
forwardPorts ⚠️ parsed Not actuated; #7
portsAttributes, otherPortsAttributes ⚠️ parsed Surfaced on ResolvedConfig; not enforced
appPort (deprecated) ✅ translated Folded into forwardPorts (skipping container ports already declared); deprecation warning still emitted

Other

Behavior Status Notes
customizations.<tool> pass-through map[string]json.RawMessage for callers
hostRequirements ⚠️ parsed Surfaced; not enforced
devcontainer.metadata label round-trip Written on build, read + merged on next Up
Unknown-field warnings (top-level + nested) Includes build, hostRequirements, gpu sub-objects
GPG / SSH agent forwarding #28

Out of scope (➖): Templates spec, dotfiles repos, IDE injection hooks, Kubernetes / podman drivers.

Install

go get github.com/crunchloop/devcontainer

Requires:

  • Go 1.25+
  • A container backend, one of:
    • Docker: daemon socket reachable; Docker Compose v2 plugin only when running dockerComposeFile projects under the default shellout backend (skip the plugin if you opt into ComposeBackendNative).
    • Apple container: macOS 15+ on Apple Silicon, container system start already up. Swift toolchain only if you're building the bridge from source — releases embed the pre-built dylib.

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	devcontainer "github.com/crunchloop/devcontainer"
	"github.com/crunchloop/devcontainer/runtime/docker"
)

func main() {
	ctx := context.Background()

	rt, err := docker.New(ctx, docker.Options{})
	if err != nil {
		log.Fatalf("docker: %v", err)
	}
	defer rt.Close()

	eng, err := devcontainer.New(devcontainer.EngineOptions{Runtime: rt})
	if err != nil {
		log.Fatalf("engine: %v", err)
	}

	ws, err := eng.Up(ctx, devcontainer.UpOptions{
		LocalWorkspaceFolder: "/path/to/your/project",
	})
	if err != nil {
		log.Fatalf("up: %v", err)
	}
	defer eng.Down(ctx, ws, devcontainer.DownOptions{Remove: true})

	res, err := eng.Exec(ctx, ws, devcontainer.ExecOptions{
		Cmd: []string{"sh", "-c", "echo $USER"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("user:", res.Stdout)
}

To target Apple's container runtime instead of Docker, swap the backend import — the rest of the engine code is unchanged:

import "github.com/crunchloop/devcontainer/runtime/applecontainer"

rt, err := applecontainer.New(ctx, applecontainer.Options{})
// ... pass to devcontainer.New the same way

Runnable end-to-end examples in examples/:

API surface

The main entry points live in the root package:

type Engine struct { /* ... */ }

func New(opts EngineOptions) (*Engine, error)
func Resolve(ctx context.Context, opts ResolveOptions) (*ResolvedConfig, error)

func (*Engine) Up(ctx, UpOptions) (*Workspace, error)
func (*Engine) Attach(ctx, WorkspaceID) (*Workspace, error)
func (*Engine) Exec(ctx, *Workspace, ExecOptions) (ExecResult, error)
func (*Engine) ExecByID(ctx, WorkspaceID, ExecOptions) (ExecResult, error)
func (*Engine) RunLifecycle(ctx, *Workspace, LifecyclePhase) error
func (*Engine) Down(ctx, *Workspace, DownOptions) error

Sub-packages:

  • config — devcontainer.json parsing, merging, host-context substitution
  • runtime — container backend abstraction (Runtime, ComposeRuntime, capabilities, network/volume/list primitives)
  • runtime/docker — Docker Engine API implementation (uses moby/moby/client)
  • runtime/applecontainer — Apple container implementation; darwin/arm64 only, cgo-linked Swift bridge
  • feature — feature resolution (OCI / HTTPS / local), DAG ordering, dockerfile generation
  • composedockerComposeFile parsing via compose-spec/compose-go, plus a runtime-agnostic in-process orchestrator (Orchestrator, Plan, topological + health gating) used when ComposeBackendNative is selected

Tests

make test              # unit tests
make test-integration  # integration tests against real Docker
make lint              # golangci-lint

The integration suite (build tag integration) exercises real Docker: pulls public images from GHCR, builds Dockerfiles, runs feature install scripts, drives docker compose up/down. Skipped automatically if a Docker daemon isn't reachable.

Apple-container integration tests are tagged integration && darwin && arm64 and run against a live container apiserver — skipped when the daemon isn't running. CI runs both the Linux + Docker suite and a macos-26 job that builds the Swift bridge and runs the applecontainer unit tests.

Design

Architectural notes — the choices behind the public API and why they were made — live under design/. Useful when contributing or when embedding the library in a non-trivial way; not required reading for normal use.

Contributing

See CONTRIBUTING.md. Bug reports welcome via GitHub issues.

License

Apache License 2.0.

Documentation

Overview

Package devcontainer is the top-level entry point for the devcontainer runtime library. The high-level engine API will land in subsequent milestones; this milestone exposes the configuration resolution surface.

Index

Constants

View Source
const (
	LabelDevcontainerID       = "dev.containers.id"
	LabelLocalWorkspaceFolder = "dev.containers.localWorkspaceFolder"
	LabelConfigPath           = "dev.containers.configPath"
	LabelEngine               = "dev.containers.engine"
)

Common labels written to every container the engine creates. Labels are the source of truth for container ↔ workspace mapping; container names are deterministic but not relied upon for lookup.

Variables

View Source
var ErrHostExecutorNotConfigured = errors.New("host executor not configured (set EngineOptions.HostExecutor)")

ErrHostExecutorNotConfigured is returned (wrapped in *LifecycleError) when a host-side hook is configured in devcontainer.json but the engine has no HostExecutor to dispatch it. Callers wanting to detect this specifically use errors.Is.

Functions

func IsLifecycleError

func IsLifecycleError(err error) bool

IsLifecycleError reports whether err is a *LifecycleError.

Types

type AttachOptions

type AttachOptions struct {
	// LocalEnv overrides os.Environ() for the substituter's localEnv
	// pass. Nil means use the current process environment.
	LocalEnv map[string]string
}

AttachOptions configures Engine.Attach. The default zero value is fine for most callers — it discovers the container by label and reads the host process environment for any localEnv references the substituter might still need.

type ComposeBackend added in v0.2.0

type ComposeBackend int

ComposeBackend selects between the legacy shellout and the new runtime-agnostic native orchestrator for compose-source projects.

const (
	// ComposeBackendShellout (default) uses runtime.ComposeRuntime
	// — `docker compose` v2 plugin under the hood. Reliable for
	// Docker, refused-with-typed-error for backends that don't
	// implement the sub-interface (i.e. applecontainer).
	ComposeBackendShellout ComposeBackend = 0

	// ComposeBackendNative uses compose.Orchestrator driving
	// runtime.Runtime primitives. Backend-agnostic; requires the
	// backend to implement CreateNetwork / CreateVolume /
	// ListContainers / ListImages / RemoveImage / RemoveNetwork /
	// RemoveVolume.
	ComposeBackendNative ComposeBackend = 1
)

type DownOptions

type DownOptions struct {
	// Remove, when true, removes the container after stopping it. Default
	// false leaves the container in stopped state for fast subsequent Up.
	Remove bool

	// RemoveVolumes, when true with Remove, also removes anonymous volumes
	// the container created. Has no effect when Remove is false.
	RemoveVolumes bool

	// Events optionally receives structured engine events for the duration
	// of this Down call (container.stopped, container.removed). See package
	// events (experimental until v1.0.0).
	//
	// Ownership: the caller owns the channel. The engine only writes —
	// it never closes the channel. The caller MUST NOT close it before
	// Down returns.
	Events chan<- events.Event
}

DownOptions configures Engine.Down.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine drives the devcontainer lifecycle on top of a Runtime.

func New

func New(opts EngineOptions) (*Engine, error)

New constructs an Engine. Returns an error if Runtime is nil or the feature store cannot be built.

func (*Engine) Attach

func (e *Engine) Attach(ctx context.Context, id WorkspaceID) (*Workspace, error)

Attach finds an existing workspace container by its devcontainer id and returns a *Workspace with a substituter bound to its live env.

Returns *runtime.ContainerNotFoundError if no container with the matching label exists. The returned workspace's Config.LocalEnv is the AttachOptions.LocalEnv (or os.Environ() if nil) — note that LocalWorkspaceFolder and ConfigPath cannot be recovered from a running container alone, so callers needing those should use Up.

The returned Workspace.Config is the MINIMAL form (LocalWorkspaceFolder, ContainerWorkspaceFolder, ContainerUser/RemoteUser, source kind plus any image-metadata-merged fields). Devcontainer.json-only fields (Lifecycle hooks, Mounts, Customizations, Features) are NOT reconstructed here — Attach does not re-read the source devcontainer.json. Callers that need the full ResolvedConfig should either call Resolve directly or use Engine.Up. See the Workspace type docs for the full breakdown.

func (*Engine) AttachWith

func (e *Engine) AttachWith(ctx context.Context, id WorkspaceID, opts AttachOptions) (*Workspace, error)

AttachWith is Attach plus options.

func (*Engine) Down

func (e *Engine) Down(ctx context.Context, ws *Workspace, opts DownOptions) error

Down stops the workspace's container. With opts.Remove, the container is also removed (matching shutdownAction=stopContainer style cleanup).

For compose workspaces, Down maps to `docker compose stop` (without Remove) or `docker compose down` (with Remove) — i.e., the whole project is treated as a unit. This is asymmetric vs image/build workspaces where Down only touches the workspace's primary container, but matches the spec's stopCompose shutdownAction semantics.

Down is safe to call on a workspace whose container has already been stopped or removed externally — the underlying ContainerNotFoundError is treated as success.

func (*Engine) Exec

func (e *Engine) Exec(ctx context.Context, ws *Workspace, opts ExecOptions) (ExecResult, error)

Exec runs a command inside the workspace's container. Strings in opts.Cmd, opts.Env values, opts.User, and opts.WorkingDir are substituted against the live container's environment before being handed to the runtime. ${containerEnv:VAR} resolves to the container's actual value; missing entries substitute to empty string with a (discarded) warning, matching VS Code semantics.

func (*Engine) ExecByID

func (e *Engine) ExecByID(ctx context.Context, id WorkspaceID, opts ExecOptions) (ExecResult, error)

ExecByID is a convenience wrapper around Attach + Exec for callers that hold only a WorkspaceID. Hot-loop callers that hold a *Workspace from Up should call Exec directly to avoid re-inspecting the container on every invocation.

func (*Engine) RunLifecycle

func (e *Engine) RunLifecycle(ctx context.Context, ws *Workspace, phase config.LifecyclePhase) error

RunLifecycle executes a single named lifecycle phase against the workspace, applying idempotency markers. Phases run in-container except initialize, which runs on the host.

A phase may have multiple LifecycleCommand hooks (one per metadata layer that contributed); they run in order [base image label entries → each feature → user devcontainer.json] per spec. The marker covers the whole phase: any one hook's non-zero exit aborts and re-runs on next Up.

User commands go through the workspace Substituter so ${containerEnv:*} resolves against the live container.

Returns nil if the phase has no commands configured. Returns a *LifecycleError if any hook exited non-zero.

func (*Engine) Shutdown

func (e *Engine) Shutdown(ctx context.Context, ws *Workspace) error

Shutdown tears the workspace down according to its devcontainer.json `shutdownAction`. Use this for editor-close / idle-timeout style teardown where the spec field should drive behavior. For unconditional teardown (always stop, optionally remove), use Down — it is the caller's explicit "I want this gone" call.

Mapping (per https://containers.dev/implementors/json_reference/):

  • "none": no-op; container left running.
  • "stop", "stopContainer", "" (unset, image/build source): stop the container; do not remove. Restart-friendly.
  • "stopCompose", "" (unset, compose source): `docker compose stop` on the project (containers preserved); for full teardown including volumes, callers should use Down with Remove=true.

"" (unset) defaults to the source-appropriate stop variant, matching upstream @devcontainers/cli behavior.

Idempotent: calling Shutdown on an already-stopped workspace returns nil.

func (*Engine) Up

func (e *Engine) Up(ctx context.Context, opts UpOptions) (*Workspace, error)

Up resolves the workspace's devcontainer.json, ensures its container is running, and returns a *Workspace ready for Exec.

Re-attach semantics:

  • existing running container with our label → attach (no restart)
  • existing stopped container → restart, attach
  • existing + UpOptions.Recreate=true → stop + remove, fresh create
  • no existing container → fresh create

Image source only in this milestone. Build / compose return runtime.ErrNotImplemented.

type EngineOptions

type EngineOptions struct {
	// Runtime is the container backend. Required.
	Runtime runtime.Runtime

	// FeatureStore overrides the default feature store. Default:
	// feature.NewDiskStore with the cache and auth options below. Tests
	// substitute this with an in-memory store; production callers
	// usually leave it nil and configure via FeatureCacheDir / OCIKeychain
	// instead.
	FeatureStore feature.Store

	// FeatureCacheDir overrides the default OCI / HTTPS feature cache
	// location (os.UserCacheDir()/devcontainer-go/features). Ignored if
	// FeatureStore is set explicitly.
	FeatureCacheDir string

	// OCIKeychain provides credentials for OCI feature pulls. Nil falls
	// back to authn.DefaultKeychain (ambient docker config / env vars /
	// credential helpers). Callers with short-lived registry tokens
	// (e.g. ECR via STS, GCR via metadata-server) supply a custom
	// Keychain that returns fresh credentials per call. Ignored if
	// FeatureStore is set explicitly.
	OCIKeychain authn.Keychain

	// FeatureDownloadHeaders are additional headers to send on HTTPS
	// feature fetches. Ignored if FeatureStore is set explicitly.
	FeatureDownloadHeaders map[string]string

	// FeatureHTTPSClient overrides the default *http.Client for HTTPS
	// feature fetches. Tests use this to drive httptest servers.
	// Ignored if FeatureStore is set explicitly.
	FeatureHTTPSClient *http.Client

	// StrictFeatureVersionMatch controls how the engine decides whether
	// a feature recorded in a base image's devcontainer.metadata label
	// satisfies the request. Default false (permissive: id match plus
	// baked semver >= requested). True requires byte-level equality on
	// the resolved digest — for reproducible builds. See
	// design/features.md §10.3.
	StrictFeatureVersionMatch bool

	// HostExecutor enables host-side spec hooks (initializeCommand,
	// future secretsCommand). Nil means host hooks return a
	// *LifecycleError wrapping ErrHostExecutorNotConfigured, since
	// host execution is opt-in and security-sensitive — see
	// HostExecutor docs.
	HostExecutor HostExecutor

	// ComposeBackend selects how compose-source devcontainers are
	// brought up. ComposeBackendShellout (default) uses the legacy
	// runtime.ComposeRuntime sub-interface, which on docker shells
	// out to the `docker compose` v2 plugin. ComposeBackendNative
	// uses the runtime-agnostic compose.Orchestrator under compose/
	// driving runtime.Runtime primitives directly (no shellout, no
	// compose plugin dependency, works on every backend that
	// satisfies the §4 primitive surface).
	//
	// See design/compose-native.md §10 for the rollout schedule:
	// Shellout stays default until a confirmed-green release on
	// Native; then the default flips and the shellout path is
	// deleted.
	ComposeBackend ComposeBackend
}

EngineOptions configures a new Engine.

type ExecOptions

type ExecOptions struct {
	Cmd        []string
	Env        map[string]string
	User       string
	WorkingDir string
	Tty        bool
	Stdin      io.Reader
	Stdout     io.Writer
	Stderr     io.Writer

	// SkipUserEnvProbe, when true, makes Exec bypass the merge of
	// probedEnv AND cfg.RemoteEnv into the process environment.
	// Only opts.Env (after substitution) plus whatever the runtime
	// inherits from the container reaches the exec'd process.
	//
	// Default false: every Exec inherits both probedEnv (so PATH and
	// other vars set by the user's rc files are visible — nvm/asdf/
	// feature-installed tools just work) and RemoteEnv (the
	// devcontainer.json author's declared env).
	//
	// Set this for callers that need a clean, deterministic
	// environment — internal probes, low-level fs operations,
	// DiscoverPath-style helpers that read raw container env.
	SkipUserEnvProbe bool

	// EmitEvents, when true and Events is non-nil, sends ExecStartEvent
	// and ExecCompletedEvent for this call. Default false: hot-loop
	// callers (readiness probes that run hundreds of execs per minute)
	// would otherwise drown the events channel.
	EmitEvents bool

	// Events optionally receives ExecStartEvent / ExecCompletedEvent for
	// this call. Used only when EmitEvents is true. See package events
	// (experimental until v1.0.0).
	//
	// Ownership: the caller owns the channel. The engine only writes —
	// it never closes the channel. The caller MUST NOT close it before
	// Exec returns.
	Events chan<- events.Event
}

ExecOptions configures Engine.Exec. Cmd, Env, User, and WorkingDir all pass through Workspace.Substituter so ${containerEnv:*} placeholders resolve against the live container.

type ExecResult

type ExecResult struct {
	ExitCode int
	Stdout   string // populated only if ExecOptions.Stdout was nil
	Stderr   string // populated only if ExecOptions.Stderr was nil
}

ExecResult is the outcome of Engine.Exec.

type HostCommand

type HostCommand struct {
	// Shell is a single shell command line (`sh -c <Shell>` style).
	// Mutually exclusive with Exec.
	Shell string

	// Exec is a literal argv invocation (no shell). Mutually
	// exclusive with Shell.
	Exec []string

	// Env is merged on top of the host process environment.
	// Implementations decide whether to filter the inherited host
	// env (e.g. drop secrets) or pass it through.
	Env map[string]string

	// WorkingDir is the host directory to run in. Empty leaves the
	// choice to the executor; the engine populates this with the
	// workspace's LocalWorkspaceFolder for spec hooks.
	WorkingDir string
}

HostCommand is the input to HostExecutor.ExecHost. Shape mirrors runtime.ExecOptions / config.Command so callers building both can reuse mental model.

type HostExecResult

type HostExecResult struct {
	ExitCode int
	Stdout   string
	Stderr   string
}

HostExecResult is the outcome of HostExecutor.ExecHost.

type HostExecutor

type HostExecutor interface {
	// ExecHost runs a host-side command. The executor is responsible
	// for the shell / exec dispatch (HostCommand.Shell vs
	// HostCommand.Exec — exactly one is set), environment merging,
	// and working-directory selection. Cancellation via ctx must
	// propagate to the spawned process.
	//
	// Return a non-nil error only for executor-internal failures
	// (process couldn't start, I/O error). A non-zero command exit
	// is reported via HostExecResult.ExitCode with a nil error so
	// the engine can wrap it consistently with container-side
	// execution.
	ExecHost(ctx context.Context, cmd HostCommand) (HostExecResult, error)
}

HostExecutor runs commands on the host. Callers supply one via EngineOptions.HostExecutor to enable host-side spec hooks like initializeCommand (and, in a follow-up, secretsCommand). The library does NOT ship a default implementation: host execution is security-sensitive (devcontainer.json can declare arbitrary commands), and the policy decisions — sandboxing, env filtering, timeouts, max output, working directory — belong to the embedding application.

When EngineOptions.HostExecutor is nil and a hook would run, the engine returns a *LifecycleError wrapping ErrHostExecutorNotConfigured. Callers can detect this via errors.Is to surface a useful message ("set EngineOptions.HostExecutor to enable initializeCommand") or to skip silently in environments where host execution isn't permitted.

type LifecycleError

type LifecycleError struct {
	Phase    config.LifecyclePhase
	ExitCode int
	Stdout   string
	Stderr   string
	Cause    error
}

LifecycleError describes a lifecycle phase that ran but exited non-zero. Wrapped errors include exec-level failures (daemon connectivity etc.) and unwrap to those.

func (*LifecycleError) Error

func (e *LifecycleError) Error() string

func (*LifecycleError) Unwrap

func (e *LifecycleError) Unwrap() error

type PullPolicy

type PullPolicy string

PullPolicy controls when images are pulled from a registry.

const (
	PullIfNotPresent PullPolicy = "" // default
	PullAlways       PullPolicy = "always"
	PullNever        PullPolicy = "never"
)

type ResolveOptions

type ResolveOptions struct {
	// LocalWorkspaceFolder is the absolute host path containing the project.
	// Required.
	LocalWorkspaceFolder string

	// ConfigPath is the absolute path to the devcontainer.json file. If
	// empty, Resolve looks for:
	//   <LocalWorkspaceFolder>/.devcontainer/devcontainer.json
	//   <LocalWorkspaceFolder>/.devcontainer.json
	// in that order.
	ConfigPath string

	// LocalEnv overrides os.Environ() for ${localEnv:*} resolution.
	// Nil means use the current process environment.
	LocalEnv map[string]string

	// DevcontainerIDFunc lets callers customize the workspace id derivation.
	// Nil uses config.DevcontainerID(LocalWorkspaceFolder, ConfigPath).
	DevcontainerIDFunc func(localWorkspaceFolder, configPath string) string
}

ResolveOptions controls how Resolve loads and resolves a devcontainer.json.

type ResolvedConfig

type ResolvedConfig = config.ResolvedConfig

ResolvedConfig is re-exported from the config package for caller convenience. See config.ResolvedConfig.

func Resolve

func Resolve(ctx context.Context, opts ResolveOptions) (*ResolvedConfig, error)

Resolve loads and resolves a devcontainer.json document.

Image-label metadata merging and feature OCI resolution are stubbed in this milestone (see PRD §13). Build and compose source kinds are parsed and surfaced; actuating them requires the runtime layer landing in M2/M4.

type Substituter

type Substituter struct {
	// contains filtered or unexported fields
}

Substituter resolves devcontainer.json substitution placeholders against a fully-populated SubstitutionContext, including the live container's environment. Use it to rewrite Exec / RunLifecycle inputs at call time without mutating the underlying ResolvedConfig.

func (*Substituter) Map

func (s *Substituter) Map(in map[string]string) (map[string]string, []config.Warning)

Map applies String to each value and returns a new map. Keys are not substituted.

func (*Substituter) Slice

func (s *Substituter) Slice(in []string) ([]string, []config.Warning)

Slice applies String to each element in place and concatenates the warnings.

func (*Substituter) String

func (s *Substituter) String(in string) (string, []config.Warning)

String resolves placeholders in s. Returned warnings are accumulated non-fatal diagnostics; callers may discard them or surface as events.

type UpOptions

type UpOptions struct {
	// LocalWorkspaceFolder is the absolute host path to the project. Required.
	LocalWorkspaceFolder string

	// ConfigPath is the absolute path to devcontainer.json. If empty,
	// discovered under LocalWorkspaceFolder per Resolve's rules.
	ConfigPath string

	// LocalEnv overrides os.Environ() for ${localEnv:*} resolution.
	// Nil means use the current process environment.
	LocalEnv map[string]string

	// Recreate, when true, stops + removes any existing container with our
	// label and creates a fresh one. Default false: an existing stopped
	// container is restarted (preserving in-container state); an existing
	// running container is attached to.
	Recreate bool

	// PullPolicy controls image pulling. Default IfNotPresent.
	PullPolicy PullPolicy

	// Events optionally receives structured engine events for the duration
	// of this Up call (config resolved, feature resolve, build/pull
	// progress, container lifecycle, spec lifecycle phases). Drop-on-full;
	// the engine never blocks on send. See package events for the type
	// surface (experimental until v1.0.0).
	//
	// Ownership: the caller owns the channel. The engine only writes —
	// it never closes the channel. The caller MUST NOT close it before
	// Up returns; closing a channel while the engine is still sending
	// races with the engine's send and will panic. Close after Up
	// returns (or simply leave the channel open and let it be GC'd).
	Events chan<- events.Event

	// SkipLifecycle, when true, suppresses automatic invocation of
	// devcontainer lifecycle phases (onCreate, postCreate, etc.) from
	// Up. Phases can still be run explicitly via Engine.RunLifecycle.
	// Default false: Up runs the full configured lifecycle.
	SkipLifecycle bool

	// RunInitializeCommand, when true, runs the host-side initializeCommand
	// before container creation. Default false because the spec lets
	// devcontainer.json execute arbitrary host commands; opt-in only.
	// Note: v1 initialize execution is a stub that returns an error;
	// real host execution requires caller-supplied wiring (PRD §11).
	RunInitializeCommand bool

	// RunSecretsCommand, when true, runs the host-side secretsCommand
	// before container creation and merges its stdout (parsed as
	// key=value lines) into the container's environment. Default false
	// for the same reason as RunInitializeCommand: arbitrary host
	// execution is opt-in. Requires EngineOptions.HostExecutor to be
	// set; otherwise a *LifecycleError wrapping
	// ErrHostExecutorNotConfigured is returned.
	//
	// Only applied on fresh container creation. On reattach the
	// existing container's env is already baked, so re-running
	// secretsCommand would have no effect and we skip it; callers
	// wanting a refresh should pass Recreate=true.
	RunSecretsCommand bool

	// ExtraMounts are appended to the mounts derived from devcontainer.json.
	// They layer on top of cfg.WorkspaceMount and cfg.Mounts and are
	// preserved across reattach (they only apply on fresh container
	// creation, since reattach inherits the original container's mounts).
	// For compose sources, only Type == runtime.MountBind entries are
	// honored — other mount types are silently dropped to match the
	// devcontainer.json `mounts` semantics.
	ExtraMounts []runtime.MountSpec

	// ExtraContainerEnv is merged into the container's environment, layered
	// on top of cfg.ContainerEnv. Entries here are baked into the container
	// at start time, so every subsequent exec — including lifecycle scripts
	// and feature install — inherits them. Use this for callers that need
	// to inject host-derived env (PATH overrides, proxy vars, short-lived
	// auth tokens) without mutating devcontainer.json.
	ExtraContainerEnv map[string]string
	// contains filtered or unexported fields
}

UpOptions configures Engine.Up.

type Workspace

type Workspace struct {
	ID WorkspaceID

	// Config is the resolved devcontainer.json. Full after Up; minimal
	// after Attach — see the type-level docs for the field breakdown.
	Config    *config.ResolvedConfig
	Container *runtime.ContainerDetails
	// contains filtered or unexported fields
}

Workspace is a live devcontainer: a resolved config plus a running container plus a substituter bound to the container's effective env.

Workspace is safe for concurrent reads (Exec/Inspect/Logs) but not for concurrent mutation. Engine.Attach returns a fresh Workspace; callers should not share Workspace values across re-attaches.

Config asymmetry between Up and Attach:

  • Workspaces returned by Engine.Up have a fully-populated Config: every field from devcontainer.json (lifecycle commands, mounts, customizations, features, etc.) is present, image-metadata merged, and warnings accumulated on Config.Warnings.

  • Workspaces returned by Engine.Attach have a MINIMAL Config reconstructed from container labels and image inspect: LocalWorkspaceFolder, ContainerWorkspaceFolder, ContainerUser, RemoteUser (from image-metadata merge), and the source kind. Lifecycle hooks, Mounts, Customizations, Features, and most other devcontainer.json fields are NOT repopulated — Attach does not re-read devcontainer.json from disk.

    This is enough to drive Exec (the substituter is bound to the live container env) and Down, but callers that need the full devcontainer.json view should call Resolve themselves, or use Engine.Up which always returns the full Config.

type WorkspaceID

type WorkspaceID string

WorkspaceID is a stable identifier for a workspace, derived from (LocalWorkspaceFolder, ConfigPath). See config.DevcontainerID.

Directories

Path Synopsis
Package compose handles the Docker Compose source kind for devcontainer.json: parsing the user's compose project via compose-spec/compose-go and synthesizing override files that the engine layers on top during `docker compose up`.
Package compose handles the Docker Compose source kind for devcontainer.json: parsing the user's compose project via compose-spec/compose-go and synthesizing override files that the engine layers on top during `docker compose up`.
Package config defines the parsed and resolved devcontainer.json configuration types.
Package config defines the parsed and resolved devcontainer.json configuration types.
Package events defines the structured event surface emitted by Engine operations (Up / Down / Exec / RunLifecycle) and feature/build/runtime activity reached through them.
Package events defines the structured event surface emitted by Engine operations (Up / Down / Exec / RunLifecycle) and feature/build/runtime activity reached through them.
examples
compose command
compose drives a 2-service devcontainer described by a Docker Compose project: a primary `app` service and a sidecar `db`.
compose drives a 2-service devcontainer described by a Docker Compose project: a primary `app` service and a sidecar `db`.
image-source command
image-source is the smallest end-to-end example: pull an image, run it, exec a command, tear it down.
image-source is the smallest end-to-end example: pull an image, run it, exec a command, tear it down.
with-features command
with-features adds a local devcontainer feature on top of an image- source devcontainer, proves the feature's install.sh ran inside the container, and confirms the feature's containerEnv was applied.
with-features adds a local devcontainer feature on top of an image- source devcontainer, proves the feature's install.sh ran inside the container, and confirms the feature's containerEnv was applied.
Package feature implements the devcontainer feature pipeline: reference resolution (OCI / HTTPS / Local), option processing, dependency-graph ordering, and dockerfile generation.
Package feature implements the devcontainer feature pipeline: reference resolution (OCI / HTTPS / Local), option processing, dependency-graph ordering, and dockerfile generation.
Package runtime defines the container backend abstraction used by the devcontainer engine.
Package runtime defines the container backend abstraction used by the devcontainer engine.
applecontainer
Package applecontainer is an Apple `container` implementation of runtime.Runtime targeting macOS 15+ on arm64.
Package applecontainer is an Apple `container` implementation of runtime.Runtime targeting macOS 15+ on arm64.
docker
Package docker is a Docker Engine API implementation of runtime.Runtime.
Package docker is a Docker Engine API implementation of runtime.Runtime.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL