Skip to content

🐳 Dynamic Docker Compose for Go - Write your container orchestration in pure Go, not YAML

License

Notifications You must be signed in to change notification settings

aptd3v/go-contain

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

go-contain

go-contain brings declarative, dynamic Docker Compose to Go. Programmatically define, extend, and orchestrate multi-container environments with the flexibility of code, all while staying fully compatible with existing YAML workflows.

Go Version Go Reference Go Report Card

Features

  • Support for Docker Compose commands up, down, logs, (more coming soon!)
  • Declarative container/service creation with chainable options
  • Native Go option setters for containers, networks, volumes, and health checks etc.
  • IDE-friendly
  • Designed for automation, CI/CD pipelines, and advanced dev environments

Why go-contain?

While Docker Compose YAML files work great for simple, static configurations, go-contain unlocks the full power of programmatic infrastructure definition. Here's why you might choose go-contain over traditional approaches:

Programmatic Infrastructure Control

// Generate infrastructure from data, APIs, configs - A real pain with static YAML

//// Generate a unique environment for each microservice from a config object.
func setupEnvironment(envConfig EnvironmentConfig) *create.Project {
    project := create.NewProject(envConfig.Name)
    
    // Generate services from database records, API responses, etc.
    for _, service := range envConfig.Services {
        replicas := envConfig.GetReplicas(service.Name)
        
        for i := 0; i < replicas; i++ {
            project.WithService(fmt.Sprintf("%s-%d", service.Name, i),
                create.NewContainer(service.Name).
                    WithContainerConfig(
                        cc.WithImagef("%s:%s", service.Image, envConfig.Version),
                        cc.WithEnv("INSTANCE_ID", strconv.Itoa(i)),
                        cc.WithEnv("ENVIRONMENT", envConfig.Environment),
                    ).
                    WithHostConfig(
                        hc.WithPortBindings("tcp", "0.0.0.0", 
                            strconv.Itoa(8080+i), "8080"),
                    ),
            )
        }
    }
    return project
}

// Call with live data from your application
envConfig := fetchEnvironmentFromAPI()
project := setupEnvironment(envConfig)
compose.NewCompose(project).Up(context.Background())
# Docker Compose scaling creates IDENTICAL containers - no per-instance customization
version: '3.8'
services:
  api:
    image: myapp:v1.2.3
    environment:
      - ENVIRONMENT=production
      # All scaled instances get the SAME environment variables
      # No way to give each replica different INSTANCE_ID or ports
    ports:
      - "8080:8080"  # Port conflicts when scaling!

# docker compose up --scale api=3
# ↑ Creates 3 identical containers, but:
# - All have the same environment variables
# - Port binding conflicts (all try to bind to 8080)
# - No way to customize individual instances

Dynamic & Conditional Configuration

// Environment-based logic, loops, and conditionals
for _, env := range []string{"dev", "staging", "prod"} {
    project.WithService(fmt.Sprintf("api-%s", env),
    	create.NewContainer().
            WithContainerConfig(
                cc.WithImagef("myapp:%s", env),
                tools.WhenTrue(env == "prod", 
                    cc.WithEnv("CACHE_ENABLED", "true"),
                ),
            ),
    )
}

// Static configuration requires multiple files or templating
// No native support for conditionals or loops

Code Reusability & Composition

// Create reusable components and patterns
func DatabaseContainer(name, version string)  *create.Container {
    return create.NewContainer().
        WithContainerConfig(
            cc.WithImagef("postgres:%s", version),
            cc.WithEnv("POSTGRES_DB", name),
        ).
        WithHostConfig(
            hc.WithPortBindings("tcp", "0.0.0.0", "5432", "5432"),
        )
}

func RedisContainer() *create.Container {
    return create.NewContainer().
        WithContainerConfig(
            cc.WithImage("redis:7-alpine"),
        ).
        WithHostConfig(
            hc.WithPortBindings("tcp", "0.0.0.0", "6379", "6379"),
        )
}

// Microservices architecture - each service gets its own database
project.WithService("user-service-db", DatabaseContainer("users", "latest"))
project.WithService("user-service-cache", RedisContainer())
project.WithService("order-service-db", DatabaseContainer("orders", "latest"))
project.WithService("order-service-cache", RedisContainer())

Perfect for Automation & CI/CD

// Integrate with existing Go tools and workflows
func DeployEnvironment(ctx context.Context, env string, replicas int) error {
    project := create.NewProject(fmt.Sprintf("app-%s", env))
    
    // Build services programmatically based on parameters
    for i := 0; i < replicas; i++ {
        project.WithService(fmt.Sprintf("worker-%d", i), 
            // ... configure based on env and replica count
        )
    }
    
    compose := compose.NewCompose(project)
    return compose.Up(ctx, up.WithDetach())
}

Portable Container Configuration

In go-contain the underlying docker sdk is also wrapped as well, allowing you to use the same configuration for docker client control, and compose.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aptd3v/go-contain/pkg/client"
	"github.com/aptd3v/go-contain/pkg/create"
	"github.com/aptd3v/go-contain/pkg/create/config/cc"
)

func main() {
	cli, err := client.NewClient()
	if err != nil {
		log.Fatalf("Error creating client: %v", err)
	}
	// Reuse the same config to create a container using the Docker SDK
	resp, err := cli.ContainerCreate(
		context.Background(),
		MySimpleContainer("latest"),
	)
	if err != nil {
		log.Fatalf("Error creating container: %v", err)
	}
	fmt.Println(resp.ID) //container id

	//create a compose project with the same container configuration
	project := create.NewProject("my-project")

	project.WithService("simple-service", MySimpleContainer("latest"))

	err = project.Export("./docker-compose.yml", 0644)
	if err != nil {
		log.Fatalf("Error exporting project: %v", err)
	}

}

func MySimpleContainer(tag string) *create.Container {
	return create.NewContainer().
		WithContainerConfig(
			cc.WithImagef("alpine:%s", tag),
			cc.WithCommand("echo", "hello world"),
		)
}

Leverage Go's Ecosystem

  • Testing: Write unit tests for your infrastructure code
  • Debugging: Use Go's debugging tools and error handling
  • Libraries: Integrate with any Go package (HTTP clients, databases, etc.)
  • Tooling: Build CLIs, APIs, and automation around your containers

Still Docker Compose Compatible

// Export to standard YAML when needed
if err := project.Export("./docker-compose.yaml", 0644); err != nil {
    log.Fatal(err)
}
// Now use with: docker compose up -d

go-contain gives you the best of both worlds: the flexibility and power of Go programming with full compatibility with the Docker Compose ecosystem.


Prerequisites

  • Go: 1.23+
  • Docker: 28.2.0+ with Docker Compose v2.37.0
  • Operating System: Linux, macOS, or Windows

Installation

go get github.com/aptd3v/go-contain@latest

Quick Start

Get up and running in 30 seconds:

# Create a new Go module
mkdir my-containers && cd my-containers
go mod init my-containers
go get github.com/aptd3v/go-contain@latest

Create main.go

package main

import (
	"context"
	"github.com/aptd3v/go-contain/pkg/compose"
	"github.com/aptd3v/go-contain/pkg/create"
	"github.com/aptd3v/go-contain/pkg/create/config/cc"
)

func main() {
	project := create.NewProject("hello-world")
	project.WithService("hello-service", 
		create.NewContainer().
			WithContainerConfig(
				cc.WithImage("alpine:latest"),
				cc.WithCommand("echo", "Hello from go-contain!"),
			),
	)
	
	compose.NewCompose(project).Up(context.Background())
}

Run it

go run main.go

Basic Usage

package main

import (
	"context"
	"log"
	"os"

	"github.com/aptd3v/go-contain/pkg/compose"
	"github.com/aptd3v/go-contain/pkg/compose/options/up"
	"github.com/aptd3v/go-contain/pkg/create"
	"github.com/aptd3v/go-contain/pkg/create/config/cc"
)

func main() {

	project := create.NewProject("my-app")

	project.WithService("my-service",
		create.NewContainer("my-container").
			WithContainerConfig(
				cc.WithImage("alpine:latest"),
				cc.WithCommand("tail", "-f", "/dev/null"),
			),
	)
	//export yaml if desired. (not needed)
	if err := project.Export("./docker-compose.yaml", 0644); err != nil {
		log.Fatal(err)
	}
	//create a new compose instance
	compose := compose.NewCompose(project)

	//execute the up command
	err := compose.Up(
		context.Background(),
		up.WithWriter(os.Stdout),
		up.WithRemoveOrphans(),
		up.WithDetach(),
	)
	if err != nil {
		log.Fatal(err)
	}
}

Declarative Container Configuration

Each setter type is defined in its own package

project.WithService("api",
	create.NewContainer("my-api-container").
        WithContainerConfig(
			//cc == container config
            cc.WithImagef("ubuntu:%s", tag)
        ).
        WithHostConfig(
			// hc == host config
            hc.WithPortBindings("tcp", "0.0.0.0", "8080", "80"),
        ).
        WithNetworkConfig(
			// nc == network config
            nc.WithEndpoint("my-network"),
        ).
        WithPlatformConfig(
			//pc == platform config
            pc.WithArchitecture("amd64"),
        ),
)

Or use underlying docker SDK structs if desired

Check out examples/structs to see how using both can be useful.

project.WithService("api", &create.Container{
		Config: &create.MergedConfig{
			Container: &container.Config{
				Image: fmt.Sprintf("ubuntu:%s", tag),
			},
			Host: &container.HostConfig{
				PortBindings: nat.PortMap{
					"8080/tcp": []nat.PortBinding{
						{
							HostIP:   "0.0.0.0",
							HostPort: "8080",
						},
					},
				},
			},
			Network: &network.NetworkingConfig{
				EndpointsConfig: map[string]*network.EndpointSettings{
					"my-network": {
						Aliases: []string{"my-api-container"},
					}},
			},
			Platform: &ocispec.Platform{
				Architecture: "amd64",
			},
		},
	})

tools Package: Declarative Logic for Setters

The tools package provides composable helpers for conditional configuration. These are useful when flags, environment variables, or dynamic inputs control what options get applied.

Highlights

  • tools.WhenTrue(...) – Apply setters only if a boolean is true
  • tools.WhenTrueFn(...) – Like above, but accepts predicate closure func() bool
  • tools.OnlyIf(...) – Apply a setter only if a runtime check passes func () (bool, error)
  • tools.Group(...) – Combine multiple setters into one func[T any, O ~func(T) error](fns ...O) O
  • tools.And(...), tools.Or(...) – Compose multiple predicate closures

Example

package main

import (
	"context"
	"log"
	"os"
	"runtime"

	"github.com/aptd3v/go-contain/pkg/compose"
	"github.com/aptd3v/go-contain/pkg/create"
	"github.com/aptd3v/go-contain/pkg/create/config/cc"
	"github.com/aptd3v/go-contain/pkg/create/config/hc"
	"github.com/aptd3v/go-contain/pkg/create/config/nc"
	"github.com/aptd3v/go-contain/pkg/create/config/sc"
	"github.com/aptd3v/go-contain/pkg/tools"
)

func main() {
	enableDebug := true //imagine this is a flag from your cli or something
	isLinux := runtime.GOOS == "linux"
	project := create.NewProject("conditional-env")
	envVars := tools.Group(
		cc.WithEnv("MYSQL_ROOT_PASSWORD", "password"),
		cc.WithEnv("MYSQL_DATABASE", "mydb"),
		cc.WithEnv("MYSQL_USER", "myuser"),
		cc.WithEnv("MYSQL_PASSWORD", "mypassword"),

		tools.WhenTrueFn(
			tools.Or(enableDebug, os.Getenv("NODE_ENV") == "development"),
			cc.WithEnv("DEBUG", "true"),
		),
	)

	project.WithService("express",
		create.NewContainer().
			WithContainerConfig(
				cc.WithImage("node:latest"),
				cc.WithCommand("npm", "start"),
				envVars,
			).
			WithHostConfig(
				tools.WhenTrueElse(isLinux,//if
					hc.WithRWNamedVolumeMount("node-data", "/app"),//true 
					hc.WithVolumeBinds("./:/app/:rw"),//else 
				),
			).
			WithNetworkConfig(
				nc.WithEndpoint("express-network"),
			),
		// service level configuration
		tools.OnlyIf(EnvFileExists(".ThisFileDoesNotExist.env"),
			sc.WithEnvFile(".ThisFileDoesNotExist.env"),
		),
	).
		WithNetwork("express-network").
		WithVolume("node-data")

	compose := compose.NewCompose(project)

	if err := compose.Up(context.Background()); err != nil {
		// will output .ThisFileDoesNotExist.env: no such file or directory
		log.Fatal(err)

	}
}
// CheckClosure is just a func() (bool, error)
func EnvFileExists(name string) tools.CheckClosure {
	return func() (bool, error) {
		_, err := os.Stat(name)
		if err != nil {
			return false, err
		}
		return true, nil
	}
}

Examples

Explore examples in the examples/ directory:

Getting Help

Roadmap

Current Features

  • βœ… Core Compose commands: up, down, logs
  • βœ… Container, network, and volume service configuration
  • βœ… Conditional logic with tools package
  • βœ… YAML export for compatibility

In Development

  • Additional Compose commands: restart, stop, start, ps
  • Enhanced Docker SDK client features
  • Image registry authentication helpers
  • More comprehensive test coverage

Ideas & Suggestions

Have ideas for go-contain? We'd love to hear them! Open an issue or start a discussion.

License

MIT License. See LICENSE for details.

Contributions

Contributions, feedback, and issues are welcome! Fork the repo and submit a PR or open an issue with your idea.

Packages

No packages published

Languages