Skip to content

crunchloop/devcontainer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

devcontainer

CI Status: alpha Go Reference License: Apache 2.0

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.

Contributing

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

License

Apache License 2.0.

About

A programmatic Go runtime for Dev Containers

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors