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.
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.
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. See PRD.md for scope and
roadmap.
| Source kind | Works? | Notes |
|---|---|---|
image |
✅ | Pull, run, exec, lifecycle |
build (Dockerfile) |
✅ | User Dockerfile + features layered atop |
dockerComposeFile |
✅ | compose-go for parse, shell-out to docker compose for orchestration |
| Features (OCI / HTTPS / local) | ✅ | DAG ordering, options validation, content-addressed cache |
| Pre-baked image (skip-already-installed) | ✅ | devcontainer.metadata label read on Up |
Lifecycle phases (onCreate … postAttach) |
✅ | Per-phase idempotency markers in container |
${...} substitution |
✅ | Host context at Resolve, container env post-create |
customizations.<tool> pass-through |
✅ | map[string]json.RawMessage for callers to decode |
Out of scope for v1: Templates spec, dotfiles repos, IDE/SSH injection
hooks, Kubernetes / podman drivers, forwardPorts actuation. See
design/ for the full non-goals list.
go get github.com/crunchloop/devcontainerRequires:
- Go 1.25+
- Docker daemon socket reachable
- Docker Compose v2 plugin (only for
dockerComposeFilesource)
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)
}Runnable end-to-end examples in examples/:
image-source/— minimal image-only devcontainerwith-features/— image + a local feature withcontainerEnvcompose/— multi-servicedockerComposeFile
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) errorSub-packages:
config— devcontainer.json parsing, merging, host-context substitutionruntime— container backend abstraction (Runtime,ComposeRuntime)runtime/docker— Docker Engine API implementation (usesmoby/moby/client)feature— feature resolution (OCI / HTTPS / local), DAG ordering, dockerfile generationcompose—dockerComposeFileparsing viacompose-spec/compose-go, override-file generation
make test # unit tests (~140 cases, ~3s)
make test-integration # integration tests against real Docker (~30s)
make lint # golangci-lintThe 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.
PRD and design docs are kept in design/ (private during incubation).
They will move to docs/ and become public alongside the v0.1.0 tag.
See CONTRIBUTING.md. Bug reports welcome via GitHub issues.