containerkit

Define Docker stacks in Go. The same *kit.Container is a Compose service or an Engine create config. Compose runs in-process through github.com/docker/compose/v5 and does not need a docker compose binary.
Demo
package main
import (
"context"
"log"
"os"
"github.com/aptd3v/containerkit/pkg/kit"
)
func main() {
project := kit.NewProject("hello-world")
project.WithService(kit.NewService("hello",
kit.NewContainer().
Image("alpine:latest").
Command("echo", "Hello from containerkit!"),
))
if err := project.Validate(); err != nil {
log.Fatal(err)
}
app := kit.NewCompose(project)
if err := app.Up(context.Background(), &kit.Up{Writer: os.Stdout}); err != nil {
log.Fatal(err)
}
}
The echo container exits when the command finishes.
Getting started
go get github.com/aptd3v/containerkit@v0.7.2
- Go 1.26.8+
- Docker Engine
- Linux, macOS, or Windows
v0.7.2
- IPv4-only networks:
ProjectNetwork.DisableIPv6 writes enable_ipv6: false. It takes precedence over EnableIPv6, so Compose does not inherit Docker Desktop's dual-stack default.
- Conditional depends_on:
DependsOnCondition accepts service_started, service_healthy, or service_completed_successfully.
- Loaded projects:
Adopt wraps a Compose project loaded elsewhere. ReplaceService swaps a service of the same name.
- Astronomy Shop: IPv4-only attachable network, health checks against
127.0.0.1, a volumes path next to the example, a probe on the Compose network, and the demo stays up until Ctrl+C.
- Published tree: the Docker e2e suite and the Makefile are no longer in the module.
v0.7.1
Docs-only patch so pkg.go.dev picks up pkg/kit as latest after the v0.7.0 release.
v0.7.0
- Astronomy Shop showcase (examples/astronomy_shop): CNCF OpenTelemetry demo as a Go Compose project with
shop / full / observability profiles, health waits, kit-probe Dockerfile-as-Go, and a taskflow Build -> Up -> probe DAG.
- Dockerfile injection harden: fluent Dockerfile scalars reject newline / control-character injection before build context is sent.
- Trap-removal cleanup: nil-safe
Container.Validate, dead daemon deps and unused Makefile targets removed from the published tree.
Features
The published core is pkg/kit and pkg/client, with optional pkg/taskflow for DAGs over the same values. One *kit.Container feeds both Engine create and Compose. Fluent setters accumulate errors; call Validate (or let Up / Export / ContainerCreate do it) before side effects. Dockerfile-as-Go plus Build.Reader, in-process Compose option structs, health/DependsOnHealthy/Up.Wait, and project.WriteTo / Export for plan preview are the agent-friendly path. An HTTP daemon or canvas UI is not part of this module.
Two ways to run the same container
| Piece |
Import |
Role |
Project, *Container |
github.com/aptd3v/containerkit/pkg/kit |
Stack and container config. Fluent methods set image, env, ports, mounts, and the rest. |
| SDK value types |
github.com/aptd3v/containerkit/pkg/kit/types |
ByteSize, PortBinding, Capability, host modes, mount aliases. |
NewCompose, Up, Down, … |
pkg/kit |
In-process Compose runner. Each command takes an option struct. |
| Engine client |
github.com/aptd3v/containerkit/pkg/client |
Docker Engine SDK wrapper. Each call takes a per-op struct (Remove, Exec, ImageBuild, …). |
Compose fits a multi-service stack: networks, volumes, profiles, and Up / Down / Logs. The Engine client fits one container, image builds, and an interactive TTY. A hybrid program uses the client to build or exec, then Compose to run the stack. See examples/nginx, examples/image_inline, and examples/mongo_replica.
Type stacks
pkg/kit config types come from github.com/docker/docker/api/types/.... pkg/client talks to github.com/moby/moby/client and github.com/moby/moby/api/types/.... Those are distinct Go types. Pass kit configs through the client APIs (they convert internally); do not assign across packages by hand.
Compose stack
project := kit.NewProject("my-app")
project.
WithNetwork("backend").
WithVolume("data").
WithService(kit.NewService("api",
kit.NewContainer().
Image("alpine:latest").
Command("sleep", "inf").
PortBinding(types.PortBinding{Protocol: types.TCP, HostIP: "0.0.0.0", HostPort: "8080", ContainerPort: "8080"}).
RWNamedVolumeMount("data", "/data").
Endpoint("backend"),
).DependsOn("db"))
if err := project.Validate(); err != nil {
log.Fatal(err)
}
app := kit.NewCompose(project)
if err := app.Up(ctx, &kit.Up{
Detach: true,
RemoveOrphans: true,
Writer: os.Stdout,
}); err != nil {
log.Fatal(err)
}
defer app.Down(ctx, &kit.Down{RemoveVolumes: true, Writer: os.Stdout})
Export writes the project as Compose YAML. Print writes that YAML to stdout. WriteTo writes it to any io.Writer.
if err := project.Export("./docker-compose.yaml", 0644); err != nil {
log.Fatal(err)
}
_ = project.Print()
_, _ = project.WriteTo(w)
Engine client
NewContainer joins every name part with a hyphen ("my", "api" becomes my-api). Pass a name for ContainerCreate. A Compose service can omit it. Construction options stay funcs on client.
cli, err := client.NewClient(client.FromEnv(), client.WithAPIVersionNegotiation())
if err != nil {
log.Fatal(err)
}
ctr := kit.NewContainer("my-api")
ctr.Image("alpine:latest").Command("sleep", "inf")
resp, err := cli.ContainerCreate(ctx, ctr)
if err != nil {
log.Fatal(err)
}
_ = cli.ContainerRemove(ctx, resp.ID, &client.Remove{Force: true, RemoveVolumes: true})
Pass that same *kit.Container to NewService when the stack should run under Compose.
Task graph
pkg/taskflow is optional. It orders the same *kit.Container, kit.Build, and *kit.Compose values as a DAG. Import it only when you want that; pkg/kit and pkg/client do not depend on it.
flow := taskflow.New("ci", cli)
build := flow.Build("build",
kit.NewBuild().Dockerfile(df),
&client.ImageBuild{Tags: []string{"app:ci"}},
)
up := flow.Compose("up-db", db, &kit.Up{Detach: true, Wait: true})
test := flow.Container("test", kit.NewContainer().Image("app:ci"), nil)
test.DependsOn(up.DependsOn(build))
defer flow.Remove(ctx)
err := flow.Run(ctx, &taskflow.Run{Writer: os.Stdout})
A failed step skips its successors. Full guide: pkg/taskflow. Demo: examples/taskflow.
Dockerfile
NewDockerFile writes a Dockerfile in Go. ParseDockerfile wraps a string and ReadDockerfile loads a file. Export writes the file to disk. A Build carries the Dockerfile plus the build context: pass it to Service.Build for Compose, or call Reader for the tar client.ImageBuild accepts.
df := kit.NewDockerFile().
FromAs("alpine:latest", "build").
Run("echo ok > /out/app").
From("alpine:latest").
Copy("/out/app", "/app", kit.CopyOpts{From: "build"})
if err := df.Export("Dockerfile", 0644); err != nil {
log.Fatal(err)
}
build := kit.NewBuild().Context(".").Dockerfile(df)
project.WithService(kit.NewService("app",
kit.NewContainer().Image("my-image:tag"),
).Build(build))
r, err := build.Reader()
if err != nil {
log.Fatal(err)
}
resp, err := cli.ImageBuild(ctx, r, &client.ImageBuild{Tags: []string{"my-image:tag"}})
Reader packs the context directory, writes the generated Dockerfile over any Dockerfile already there, and honors .dockerignore. An empty context packs only the Dockerfile. When only Context or DockerfilePath is set, Reader reads that file from disk. See examples/dockerfile and examples/image_inline.
Rules
- Setters append to
Container.Errors and to the project. Call Validate before Up, Export, or ContainerCreate. Those three calls run Validate themselves.
- A nil
Up or Down uses defaults. The writer is os.Stdout.
Service.Profiles is Compose’s profiles: key. If the project has any profiled service, set Profiles on the command (Up.Profiles, Down.Profiles, and the rest). Compose returns an error when that list is empty.
project.WithService(kit.NewService("worker",
kit.NewContainer().Image("alpine:latest"),
).Profiles("full"))
_ = app.Up(ctx, &kit.Up{Profiles: []string{"full"}, Writer: os.Stdout})
- Compose-only settings are methods on
*Service: DependsOn, Profiles, Build, Deploy. *Container stays the process config.
- Sizes are a
types.ByteSize: MemoryLimit(100 * types.MiB).
- Branch with an ordinary
if.
c := kit.NewContainer().Image("node:latest")
if runtime.GOOS == "linux" {
c.RWNamedVolumeMount("data", "/app")
} else {
c.VolumeBinds("./:/app/:rw")
}
API surface
Full signatures are on pkg.go.dev.
Naming families (as they exist today; no forced rename):
| Family |
Pattern |
Examples |
| Container fluent |
method on *Container |
.Image(), .Env(), .HealthCheck(...) |
| Project builders |
With* on *Project |
WithService, WithNetwork |
| Service Compose-only |
method on *Service |
.DependsOn(), .Profiles() |
| Client construction |
functional options |
client.WithAPIVersionNegotiation() |
| Client / Compose ops |
per-op structs |
&client.Remove{…}, &kit.Up{…} |
| Registry auth |
With* options |
auth.WithUsername (WithEmail is deprecated) |
Container. Image, env, command, ports, mounts, health, restart, resources, capabilities, logging, and platform. You can also mix in Docker SDK structs. See examples/structs.
Service. NewService(name, container), then DependsOn, DependsOnHealthy, Profiles, Build, Deploy, Secret, and the other Compose-only methods. Pass the service to Project.WithService.
Project. WithService, WithNetwork, WithVolume, WithSecret, WithJob, and WithModel.
Compose commands (selection). Full list: pkg.go.dev.
| Group |
Commands |
| Lifecycle |
Up, Down, Create, Start, Stop, Restart, Kill, Rm, Run, Scale, Pause, Unpause |
| Observe |
Logs, Ps, Events, Top, Stats, Wait, Images, Ls, Port |
| Files / build / config |
Build, Pull, Push, Config, Export, Watch, Cp, Exec, Attach, Commit |
| Also |
Publish, Version, Volumes, Viz, Generate, LoadProject, Bridge* (see pkg.go.dev) |
Engine client. Containers, images, networks, volumes, swarm, and plugins, plus interactive TTY in pkg/client/terminal. See pkg.go.dev/pkg/client.
Examples
Run from the repo root.
| Path |
Path type |
What it shows |
| examples/simple |
Compose |
Minimal NewProject → Up |
| examples/wordpress |
Compose |
Multi-service, OS-conditional mounts, logs, signal Down |
| examples/profiles |
Compose |
Profiles mirrored on Up / Kill |
| examples/events |
Compose |
Events channel + healthcheck |
| examples/compose_commands |
Compose |
Build, Ps, Start, Stop, Restart, Exec, Config, Images, Top, Pause, Run |
| examples/nginx |
Hybrid |
client.ImageBuild then compose Up |
| examples/image_inline |
Hybrid |
Inline Dockerfile; Engine inspect skips a rebuild when the tag exists |
| examples/dockerfile |
Client |
Multi-stage NewDockerFile, Export, ImageBuild, ContainerCreate |
| examples/mongo_replica |
Hybrid |
Compose Up with Wait, then client exec to init a replica set |
| examples/terminal |
Client |
Interactive TTY exec + resize |
| examples/structs |
Config |
Mix Docker SDK structs with fluent methods |
| examples/taskflow |
Task graph |
pkg/taskflow: build, Compose Up, parallel test containers, retry, Down |
| examples/supabase |
Compose |
Full multi-profile stack. See its README |
| examples/astronomy_shop |
Task graph / Compose hybrid |
OpenTelemetry Astronomy Shop: profiles, health waits, kit-probe Dockerfile-as-Go, taskflow Build -> Up -> probe. See its README |
go run ./examples/simple/
go run ./examples/supabase/ # minimal
go run ./examples/supabase/ -profile full
go run ./examples/astronomy_shop/ # store + observability (default)
go run ./examples/astronomy_shop/ -follow
Migrating from go-contain
This project was renamed from go-contain.
| Old |
New |
github.com/aptd3v/go-contain |
github.com/aptd3v/containerkit |
pkg/create, cc.WithImage, … |
pkg/kit, fluent .Image(), … |
pkg/compose, up.WithRemoveOrphans() |
kit.NewCompose + &kit.Up{…} |
pkg/client/options/** |
per-op structs on pkg/client (Remove, Exec, …) |
Contributing
Issues and pull requests are welcome: issues · discussions
License
MIT. See LICENSE.