docker

package
v3.0.0-next.11 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 22 Imported by: 0

README

Docker Executor

Go Reference

Simple, flexible Docker container executor inspired by testcontainers API. Run Docker containers with maximum configurability and easy log/status gathering.

Overview

The docker package provides production-ready Docker container management with two API styles:

  • Functional options - Flexible, chainable configuration
  • Struct-based - Testcontainers-compatible declarative style

Features

  • Dual API Design: Choose between functional options or struct-based configuration
  • Lifecycle Management: Start, Stop, Restart, Terminate, Wait
  • Wait Strategies: Log patterns, port listening, HTTP health checks, custom functions — all against a library-owned ContainerTarget, no Docker client import needed
  • Log Streaming: Real-time log access with filtering and following
  • Status Monitoring: Container state, health checks, resource stats
  • Network Helpers: Easy access to host, ports, endpoints
  • OpenTelemetry v2: Built-in observability with traces and metrics
  • Simple & Powerful: Easy for simple cases, flexible for complex scenarios

Installation

go get github.com/jasoet/pkg/v3/docker

Quick Start

Functional Options Style
package main

import (
    "context"

    "github.com/jasoet/pkg/v3/docker"
)

func main() {
    // Create executor with functional options
    exec, _ := docker.New(
        docker.WithImage("nginx:latest"),
        docker.WithPorts("80:8080"),
        docker.WithEnv("ENV=production"),
        docker.WithAutoRemove(true),
    )

    // Start container
    ctx := context.Background()
    exec.Start(ctx)
    defer exec.Terminate(ctx)

    // Get endpoint
    endpoint, _ := exec.Endpoint(ctx, "80/tcp")
    // Use: http://localhost:8080
    _ = endpoint
}
Struct-Based Style (Testcontainers-like)
package main

import (
    "context"
    "time"

    "github.com/jasoet/pkg/v3/docker"
)

func main() {
    // Create executor with ContainerRequest
    req := docker.ContainerRequest{
        Image:        "postgres:18-alpine",
        ExposedPorts: []string{"5432/tcp"},
        // Publish to an auto-assigned host port so Endpoint/ConnectionString work
        PortBindings: map[string]string{"5432/tcp": ""},
        Env: map[string]string{
            "POSTGRES_PASSWORD": "secret",
            "POSTGRES_USER":     "testuser",
            "POSTGRES_DB":       "testdb",
        },
        // Postgres logs "ready to accept connections" twice (init server, then
        // the real one); "listening on IPv4" only appears once TCP is bound.
        WaitingFor: docker.WaitForLog(`listening on IPv4`).
            WithStartupTimeout(60 * time.Second),
    }

    exec, _ := docker.NewFromRequest(req)

    ctx := context.Background()
    exec.Start(ctx)
    defer exec.Terminate(ctx)

    // Connection string helper — note the {{endpoint}} placeholder
    connStr, _ := exec.ConnectionString(ctx, "5432/tcp",
        "postgres://testuser:secret@{{endpoint}}/testdb")
    _ = connStr
}
Hybrid Style (Mix Both)

NewFromRequest(req, opts...) is sugar over New(WithRequest(req), opts...): it prepends the struct as the first option so that any additional options override or extend the struct fields.

1. Struct within options:

req := docker.ContainerRequest{
    Image: "nginx:latest",
    ExposedPorts: []string{"80/tcp"},
}

exec, _ := docker.New(
    docker.WithRequest(req),
    docker.WithName("my-nginx"),
    docker.WithOTelConfig(otelCfg), // Add observability
)

2. Options after struct:

req := docker.ContainerRequest{
    Image: "postgres:18-alpine",
    Env: map[string]string{
        "POSTGRES_PASSWORD": "secret",
    },
}

// Add additional options that override/extend the struct
exec, _ := docker.NewFromRequest(req,
    docker.WithName("my-postgres"),        // Add name
    docker.WithPorts("5432:15432"),        // Add port mapping
    docker.WithOTelConfig(otelCfg),        // Add observability
)

Note: When using both, later options override earlier ones:

req := docker.ContainerRequest{
    Image: "nginx:latest",
    Name:  "default-name",
}

exec, _ := docker.NewFromRequest(req,
    docker.WithName("override-name"),  // ← This wins!
)

API Styles

Functional Options

Advantages:

  • Chainable and composable
  • Clear and explicit
  • Easy to add/remove options
  • Type-safe with IDE autocomplete

Example:

exec, _ := docker.New(
    docker.WithImage("redis:7-alpine"),
    docker.WithPorts("6379:16379"),
    docker.WithVolume("/data", "/data"),
    docker.WithWaitStrategy(docker.WaitForPort("6379/tcp")),
)
Struct-Based (ContainerRequest)

Advantages:

  • Familiar to testcontainers users
  • Easy to build programmatically
  • Good for configuration files (YAML/JSON)
  • Compact for complex configs

Example:

req := docker.ContainerRequest{
    Image:        "mysql:8",
    ExposedPorts: []string{"3306/tcp"},
    Env: map[string]string{
        "MYSQL_ROOT_PASSWORD": "root",
        "MYSQL_DATABASE":      "app",
    },
    WaitingFor: docker.WaitForLog("ready for connections"),
}
exec, _ := docker.NewFromRequest(req)

Configuration Options

Image & Container
docker.WithImage("nginx:latest")              // Container image
docker.WithName("my-container")               // Container name
docker.WithHostname("app-server")             // Hostname
docker.WithCmd("--verbose", "--debug")        // Override CMD
docker.WithEntrypoint("/bin/sh", "-c")       // Override ENTRYPOINT
docker.WithWorkDir("/app")                    // Working directory
docker.WithUser("1000:1000")                  // User (UID:GID)
Environment
docker.WithEnv("KEY=value")                   // Single env var
docker.WithEnvMap(map[string]string{          // Multiple env vars
    "DB_HOST": "localhost",
    "DB_PORT": "5432",
})
Ports
docker.WithPorts("80:8080")                   // Simple port mapping
docker.WithPorts("443:8443/tcp")              // With protocol
docker.WithPortBindings(map[string]string{    // Multiple ports
    "80/tcp":  "8080",
    "443/tcp": "8443",
})
docker.WithExposedPorts("8080", "9090")       // Expose without binding
Volumes
docker.WithVolume("/host/path", "/container/path")
docker.WithVolumeRO("/host/path", "/container/path") // Read-only
docker.WithVolumes(map[string]string{
    "/host/data": "/data",
    "/host/logs": "/var/log",
})
Network
docker.WithNetwork("my-network")              // Attach to network
docker.WithNetworks("net1", "net2")           // Multiple networks
docker.WithNetworkMode("bridge")              // Network mode
docker.WithNetworkMode("host")                // Host network
Security
docker.WithPrivileged(true)                   // Privileged mode
docker.WithCapAdd("NET_ADMIN", "SYS_TIME")   // Add capabilities
docker.WithCapDrop("CHOWN", "SETUID")        // Drop capabilities
Resources
docker.WithShmSize(67108864)                  // /dev/shm size (64MB)
docker.WithTmpfs("/tmp", "size=64m")         // tmpfs mount
Cleanup
docker.WithAutoRemove(true)                   // Auto-remove on stop
Wait Strategies
docker.WithWaitStrategy(
    docker.WaitForLog("started successfully").
        WithStartupTimeout(60 * time.Second),
)

docker.WithWaitStrategy(
    docker.WaitForPort("8080/tcp"),
)

docker.WithWaitStrategy(
    docker.WaitForHTTP("8080", "/health", 200),
)

docker.WithWaitStrategy(
    docker.WaitForHealthy(),
)

docker.WithWaitStrategy(
    docker.WaitForFunc(func(ctx context.Context, target docker.ContainerTarget) error {
        // Custom readiness check
        return nil
    }),
)

// Combine several strategies (ALL must pass)
docker.WithWaitStrategy(
    docker.WaitForAll(
        docker.WaitForPort("5432/tcp"),
        docker.WaitForLog("ready to accept connections"),
    ),
)

Every strategy exposes WithStartupTimeout(d) (default 60s; 120s for WaitForAll).

Observability
docker.WithOTelConfig(otelCfg)                // OpenTelemetry
docker.WithTimeout(30 * time.Second)          // Operation timeout

Wait Strategies and ContainerTarget

A wait strategy decides when a started container is ready; Start blocks until it passes (or its timeout fires, in which case the container is cleaned up and Start fails).

The strategy contract is:

type WaitStrategy interface {
    WaitUntilReady(ctx context.Context, target ContainerTarget) error
}

Strategies never touch the Docker client. Instead they receive a ContainerTarget — a library-owned, value-type view of the running container:

target.ID()                  // container ID
target.Logs(ctx)             // io.ReadCloser streaming stdout+stderr (follow mode)
target.State(ctx)            // ContainerState{Running, HealthStatus, Ports}

ContainerState is projected from Docker inspect into plain Go types:

type ContainerState struct {
    Running      bool
    HealthStatus string              // "" when no healthcheck is defined
    Ports        map[string][]string // container port ("80/tcp") → host ports
}

Custom strategies implement WaitUntilReady directly, or use WaitForFunc for one-off checks:

docker.WaitForFunc(func(ctx context.Context, target docker.ContainerTarget) error {
    state, err := target.State(ctx)
    if err != nil {
        return err
    }
    if !state.Running {
        return fmt.Errorf("container %s not running", target.ID())
    }
    return nil
})

A ContainerTarget is only usable when constructed by the Executor during Start; the zero value holds a nil client and will panic on Logs and State.

Lifecycle Methods

Start
err := exec.Start(ctx)
// - Pulls image if needed
// - Creates container
// - Starts container
// - Waits for readiness (if strategy configured)
Stop
err := exec.Stop(ctx)
// - Sends SIGTERM
// - Waits for graceful shutdown
// - Container can be restarted
Terminate
err := exec.Terminate(ctx)
// - Force stops container
// - Removes container
// - Cannot be restarted
Restart
err := exec.Restart(ctx)
// - Restarts running container
Wait
exitCode, err := exec.Wait(ctx)
// - Blocks until container exits
// - Returns exit code
Close
err := exec.Close()
// - Closes the Docker client connection
// - Does NOT terminate the container — call Terminate() first if needed

Logs

Get All Logs
logs, err := exec.Logs(ctx)
Stream Logs
logCh, errCh := exec.StreamLogs(ctx, docker.WithFollow())
for log := range logCh {
    fmt.Println(log.Content) // LogEntry{Stream, Content}
}

LogEntry carries the stream name (stdout/stderr) and the frame content. To get timestamps, enable WithTimestamps() — they are embedded as a prefix in Content.

Follow Logs to Writer
err := exec.FollowLogs(ctx, os.Stdout)
Advanced Log Options
logs, err := exec.Logs(ctx,
    docker.WithStdout(true),
    docker.WithStderr(true),
    docker.WithTimestamps(),
    docker.WithTail("100"),        // Last 100 lines
    docker.WithSince("10m"),        // Last 10 minutes
)
Convenience Methods
logs, _ := exec.GetLogsSince(ctx, "5m")
logs, _ := exec.GetLastNLines(ctx, 50)
stdout, _ := exec.GetStdout(ctx)
stderr, _ := exec.GetStderr(ctx)

Status & Monitoring

Get Status
status, err := exec.Status(ctx)
fmt.Println(status.Running)      // true/false
fmt.Println(status.State)        // "running", "exited", etc.
fmt.Println(status.ExitCode)     // Exit code if stopped
fmt.Println(status.Health.Status) // "healthy", "unhealthy", "starting"
Check Running
running, err := exec.IsRunning(ctx)
Get Exit Code
exitCode, err := exec.ExitCode(ctx)
Health Check
health, err := exec.HealthCheck(ctx)
fmt.Println(health.Status)
fmt.Println(health.FailingStreak)
Full Inspection
inspect, err := exec.Inspect(ctx)
// Returns *container.InspectResponse with all details
Resource Stats
stats, err := exec.GetStats(ctx)
// CPU, memory, network, disk I/O
Wait for State
err := exec.WaitForState(ctx, "running", 30*time.Second)
err := exec.WaitHealthy(ctx, 60*time.Second)

Note: the executor method is WaitHealthy (verb phrase); the wait strategy constructor remains docker.WaitForHealthy().

Network Helpers

Get Host
host, err := exec.Host(ctx)
// Returns "localhost" for local Docker
Get Mapped Port
port, err := exec.MappedPort(ctx, "8080/tcp")
// Returns "32768" (example randomly assigned port)
Get Endpoint
endpoint, err := exec.Endpoint(ctx, "8080/tcp")
// Returns "localhost:32768"

// Use directly
resp, _ := http.Get("http://" + endpoint + "/health")
Get All Ports
ports, err := exec.GetAllPorts(ctx)
// map[string]string{
//     "80/tcp": "8080",
//     "443/tcp": "8443",
// }
Get Networks
networks, err := exec.GetNetworks(ctx)
// []string{"bridge", "my-network"}
Get IP Address
ip, err := exec.GetIPAddress(ctx, "bridge")
// "172.17.0.2"
Connection String
connStr, err := exec.ConnectionString(ctx, "5432/tcp",
    "postgres://user:pass@{{endpoint}}/db")
// "postgres://user:pass@localhost:15432/db"

The template placeholder is {{endpoint}}, substituted via plain string replacement — not a fmt.Sprintf verb. %s in the template is left untouched (and would break the DSN), and passwords containing % are safe.

Use Cases

Database Testing
req := docker.ContainerRequest{
    Image: "postgres:18-alpine",
    ExposedPorts: []string{"5432/tcp"},
    PortBindings: map[string]string{"5432/tcp": ""}, // auto-assigned host port
    Env: map[string]string{
        "POSTGRES_PASSWORD": "test",
        "POSTGRES_USER":     "test",
        "POSTGRES_DB":       "test",
    },
    WaitingFor: docker.WaitForLog(`listening on IPv4`), // see note below
}

exec, _ := docker.NewFromRequest(req)
exec.Start(ctx)
defer exec.Terminate(ctx)

endpoint, _ := exec.Endpoint(ctx, "5432/tcp")
db, _ := sql.Open("postgres", "postgres://test:test@"+endpoint+"/test")

Postgres wait pattern: the official image logs database system is ready to accept connections twice — first for the temporary init server (Unix socket only), then for the real server. Waiting on that line can return before TCP 5432 is bound. listening on IPv4 appears only when the real server binds TCP, so it is the safer pattern.

Web Service Testing
exec, _ := docker.New(
    docker.WithImage("nginx:latest"),
    docker.WithPorts("80:0"), // Random host port
    docker.WithWaitStrategy(
        docker.WaitForHTTP("80", "/", 200),
    ),
)

exec.Start(ctx)
defer exec.Terminate(ctx)

endpoint, _ := exec.Endpoint(ctx, "80/tcp")
resp, _ := http.Get("http://" + endpoint)
Message Queue
exec, _ := docker.New(
    docker.WithImage("rabbitmq:3-management"),
    docker.WithPorts("5672:15672"),
    docker.WithPorts("15672:25672"),
    docker.WithEnvMap(map[string]string{
        "RABBITMQ_DEFAULT_USER": "guest",
        "RABBITMQ_DEFAULT_PASS": "guest",
    }),
    docker.WithWaitStrategy(
        docker.WaitForLog("Server startup complete"),
    ),
)
CI/CD Build Container
exec, _ := docker.New(
    docker.WithImage("golang:1.23"),
    docker.WithVolume(pwd, "/app"),
    docker.WithWorkDir("/app"),
    docker.WithCmd("go", "test", "./..."),
    docker.WithAutoRemove(true),
)

exec.Start(ctx)
exitCode, _ := exec.Wait(ctx)
if exitCode != 0 {
    logs, _ := exec.GetStderr(ctx)
    fmt.Println("Tests failed:", logs)
}
Development Environment
// Redis
redis, _ := docker.New(
    docker.WithImage("redis:7-alpine"),
    docker.WithPorts("6379:6379"),
    docker.WithName("dev-redis"),
)

// PostgreSQL
postgres, _ := docker.New(
    docker.WithImage("postgres:18-alpine"),
    docker.WithPorts("5432:5432"),
    docker.WithName("dev-postgres"),
    docker.WithEnvMap(map[string]string{
        "POSTGRES_PASSWORD": "dev",
    }),
)

redis.Start(ctx)
postgres.Start(ctx)

defer redis.Terminate(ctx)
defer postgres.Terminate(ctx)

Best Practices

1. Always Use defer for Cleanup
exec, _ := docker.New(...)
exec.Start(ctx)
defer exec.Terminate(ctx) // Ensures cleanup
2. Use Wait Strategies
// ✅ Good: Wait for readiness
docker.WithWaitStrategy(
    docker.WaitForLog("ready").WithStartupTimeout(30*time.Second),
)

// ❌ Bad: No wait strategy (race conditions)
3. Set Timeouts
// ✅ Good: Reasonable timeout
docker.WithTimeout(30 * time.Second)

// ❌ Bad: No timeout (hangs forever)
4. Use AutoRemove for Tests
docker.WithAutoRemove(true) // Clean up automatically
5. Handle Errors
if err := exec.Start(ctx); err != nil {
    logs, _ := exec.GetStderr(ctx)
    log.Fatalf("Failed to start: %v\nLogs: %s", err, logs)
}
6. Use Context for Cancellation
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

exec.Start(ctx) // Will respect context timeout
7. Check Container Health
exec.Start(ctx)

// Verify it's actually working
running, _ := exec.IsRunning(ctx)
if !running {
    status, _ := exec.Status(ctx)
    log.Fatalf("Container failed: %s", status.Error)
}

OpenTelemetry Integration

The docker package includes full OpenTelemetry v2 instrumentation for observability.

import (
    "github.com/jasoet/pkg/v3/otel"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)

// Initialize OTel providers
tp := sdktrace.NewTracerProvider(...)
mp := sdkmetric.NewMeterProvider(...)

otelCfg := &otel.Config{
    TracerProvider: tp,
    MeterProvider:  mp,
}

// Use with executor (functional option or struct field — both work)
exec, _ := docker.New(
    docker.WithImage("nginx:latest"),
    docker.WithOTelConfig(otelCfg),
)

req := docker.ContainerRequest{
    Image:      "nginx:latest",
    OTelConfig: otelCfg, // excluded from yaml/mapstructure decoding
}

// Automatic instrumentation:
// - Traces: docker.Start, docker.Stop, docker.Terminate, docker.Restart, docker.Wait
// - Metrics:
//   - docker.containers.started
//   - docker.containers.stopped
//   - docker.containers.terminated
//   - docker.containers.restarted
//   - docker.container.errors
// - Error tracking: Errors recorded in both traces and metrics with attributes

Migrating from v2

Breaking changes in v3:

  • Import path: github.com/jasoet/pkg/v3/docker (was /v2/docker).
  • WaitStrategy contract: WaitUntilReady(ctx, target ContainerTarget) — strategies no longer receive the Docker *client.Client and container ID. Use ContainerTarget.ID(), .Logs(ctx), and .State(ctx) instead. WaitForFunc signatures change accordingly.
  • Executor.WaitForHealthyExecutor.WaitHealthy: the method was renamed; the strategy constructor docker.WaitForHealthy() is unchanged.
  • Removed helpers: NatPort, PortBindings, and ExposedPorts (thin wrappers over github.com/docker/go-connections/nat) are gone; port strings like "8080/tcp" are parsed internally.
  • LogEntry.Timestamp removed: the field was never populated. Enable WithTimestamps() and read the prefix from Content instead.
  • ConnectionString placeholder: templates use {{endpoint}}, not %s (plain string replacement, safe for passwords containing %).

Testing

The package has comprehensive unit and integration tests.

# Run all tests (requires Docker)
go test ./docker -v

# With coverage
go test ./docker -cover

# Run specific test
go test ./docker -run TestExecutor_FunctionalOptions -v

# Run benchmarks
go test ./docker -bench=. -benchmem

Test Requirements:

  • Docker daemon running
  • Docker API accessible
  • Internet access (for pulling images)

Examples

See the examples/docker/ directory for complete, runnable examples:

  • basic - Functional options, struct-based, and hybrid styles
  • database - PostgreSQL container with real database operations
  • logs - Log streaming, filtering, and following
  • multi_container - Running multiple containers (Nginx + Redis)

Run examples:

go run -tags=example ./examples/docker/basic
go run -tags=example ./examples/docker/database
go run -tags=example ./examples/docker/logs
go run -tags=example ./examples/docker/multi_container

Comparison with Testcontainers

Feature Docker Executor Testcontainers-go
API Style Functional options + Struct Struct-based
Simplicity ⭐⭐⭐⭐⭐ ⭐⭐⭐
Flexibility ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Dependencies Minimal Many
OTel Support Built-in v2 No
Learning Curve Low Medium
Use Case General purpose Testing focus

Architecture

Key Components
  • Executor - Main container lifecycle manager
  • Config - Container configuration with functional options
  • Wait Strategies - Readiness checking against a ContainerTarget
  • Network - Port mapping and endpoint resolution
  • Logs - Log streaming and filtering
  • Status - Container state monitoring
  • OTel - OpenTelemetry v2 instrumentation
Design Principles
  1. Simple by default, powerful when needed - Easy basic usage, advanced features available
  2. Two API styles - Functional options for Go idioms, structs for testcontainers compatibility
  3. No client leakage - Public API never exposes the Docker client; strategies work against ContainerTarget
  4. Context-aware - All operations respect context cancellation and timeouts
  5. Observable - Built-in OpenTelemetry v2 support for production monitoring

Troubleshooting

Container fails to start
if err := exec.Start(ctx); err != nil {
    // Check logs for startup errors
    logs, _ := exec.GetStderr(ctx)
    fmt.Println("Error logs:", logs)

    // Check container status
    status, _ := exec.Status(ctx)
    fmt.Printf("State: %s, Error: %s\n", status.State, status.Error)
}
Port already in use
// Use random port (0)
docker.WithPorts("80:0")  // Host port auto-assigned
Wait strategy timeout
// Increase timeout
docker.WithWaitStrategy(
    docker.WaitForLog("ready").
        WithStartupTimeout(120 * time.Second),  // 2 minutes
)

If a log-based wait never succeeds, check that the pattern is a regex that matches a single log line as the container actually prints it — docker logs <container> shows the ground truth.

Image pull fails
// Pull manually first
exec, _ := docker.New(docker.WithImage("myregistry.com/image:tag"))

// Or check pull errors
if err := exec.Start(ctx); err != nil {
    if strings.Contains(err.Error(), "pull") {
        fmt.Println("Image pull failed - check registry credentials")
    }
}
  • otel - OpenTelemetry v2 configuration and utilities
  • config - Configuration management with validation
  • logging - Structured logging with context

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ForListeningPort

func ForListeningPort(port string) *waitForPort

ForListeningPort is an alias for WaitForPort for testcontainers compatibility.

func WaitForAll

func WaitForAll(strategies ...WaitStrategy) *multiWait

WaitForAll creates a wait strategy that waits for all strategies to pass.

func WaitForFunc

func WaitForFunc(fn func(ctx context.Context, target ContainerTarget) error) *waitFunc

WaitForFunc creates a wait strategy from a custom function. This allows users to implement their own wait logic.

Example

WaitForFunc wraps arbitrary readiness logic. The strategy receives a ContainerTarget exposing the container ID, a log stream, and a projected runtime state — no Docker client import required.

Output is non-deterministic; compile-checked only.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/jasoet/pkg/v3/docker"
)

func main() {
	strategy := docker.WaitForFunc(func(ctx context.Context, target docker.ContainerTarget) error {
		state, err := target.State(ctx)
		if err != nil {
			return err
		}
		if !state.Running {
			return fmt.Errorf("container %s not running", target.ID())
		}
		return nil
	}).WithStartupTimeout(30 * time.Second)

	exec, err := docker.New(
		docker.WithImage("redis:7-alpine"),
		docker.WithWaitStrategy(strategy),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer func() { _ = exec.Close() }()
}

func WaitForHTTP

func WaitForHTTP(port, path string, expectedStatus int) *waitForHTTP

WaitForHTTP creates a wait strategy that waits for an HTTP endpoint. Port format: "8080" or "8080/tcp" Path: HTTP path (e.g., "/health") Expected status: HTTP status code (e.g., 200)

func WaitForHealthy

func WaitForHealthy() *waitForHealthy

WaitForHealthy creates a wait strategy that waits for health check to pass. Container must have a HEALTHCHECK defined in Dockerfile or via Docker API.

func WaitForLog

func WaitForLog(pattern string) *waitForLog

WaitForLog creates a wait strategy that waits for a log pattern. Pattern can be a simple string or regex pattern. If the pattern is invalid, WaitUntilReady will return an error instead of panicking.

Example

WaitForLog creates a strategy that blocks Start until a line matching the pattern appears in the container logs. The pattern is a regular expression; plain strings work because they are valid regexes.

Output is non-deterministic; compile-checked only.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/jasoet/pkg/v3/docker"
)

func main() {
	strategy := docker.WaitForLog("database system is ready to accept connections").
		WithStartupTimeout(60 * time.Second)

	exec, err := docker.New(
		docker.WithImage("postgres:18-alpine"),
		docker.WithEnvMap(map[string]string{
			"POSTGRES_PASSWORD": "secret",
		}),
		docker.WithWaitStrategy(strategy),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer func() { _ = exec.Close() }()

	// Start blocks until the log pattern matches (requires a Docker daemon):
	//
	//	ctx := context.Background()
	//	if err := exec.Start(ctx); err != nil { ... }
	//	defer exec.Terminate(ctx)
	_ = context.Background()
}

func WaitForPort

func WaitForPort(port string) *waitForPort

WaitForPort creates a wait strategy that waits for a port to be listening. Port format: "8080/tcp" or just "8080" (defaults to tcp).

Types

type ContainerRequest

type ContainerRequest struct {
	// Image is the container image to use (e.g., "nginx:latest")
	Image string

	// Name is the container name (optional)
	Name string

	// Hostname sets the container hostname
	Hostname string

	// ExposedPorts are ports to expose (e.g., []string{"80/tcp", "443/tcp"})
	ExposedPorts []string

	// Env is a map of environment variables
	Env map[string]string

	// Cmd overrides the default command
	Cmd []string

	// Entrypoint overrides the default entrypoint
	Entrypoint []string

	// WorkingDir sets the working directory
	WorkingDir string

	// User sets the user (e.g., "1000:1000" or "username")
	User string

	// Labels are container labels
	Labels map[string]string

	// Volumes maps host paths to container paths
	// Key: host path, Value: container path
	Volumes map[string]string

	// BindMounts allows raw Docker bind mount specs. Use with caution: callers are responsible for
	// ensuring mount paths do not expose sensitive host directories.
	// Key: container path, Value: bind mount spec
	BindMounts map[string]string

	// Networks to attach the container to
	Networks []string

	// NetworkMode sets the network mode (bridge, host, none, container:<name>)
	NetworkMode string

	// PortBindings maps container ports to host ports
	// Key: container port (e.g., "80/tcp"), Value: host port (e.g., "8080")
	PortBindings map[string]string

	// AutoRemove automatically removes the container when it stops
	AutoRemove bool

	// Privileged runs the container in privileged mode
	Privileged bool

	// CapAdd adds Linux capabilities
	CapAdd []string

	// CapDrop drops Linux capabilities
	CapDrop []string

	// Tmpfs mounts tmpfs filesystems
	// Key: container path, Value: options
	Tmpfs map[string]string

	// ShmSize sets the size of /dev/shm
	ShmSize int64

	// WaitingFor specifies the wait strategy for container readiness
	WaitingFor WaitStrategy

	// Timeout for container operations (default: 30s)
	Timeout time.Duration

	// OTelConfig enables OpenTelemetry instrumentation (optional)
	OTelConfig *otel.Config `yaml:"-" mapstructure:"-"`
}

ContainerRequest represents a declarative container configuration, similar to testcontainers.ContainerRequest. This allows users to configure containers using a struct-based approach.

type ContainerState

type ContainerState struct {
	Running      bool
	HealthStatus string              // "" when no healthcheck is defined
	Ports        map[string][]string // containerPort ("80/tcp") → hostPorts
}

ContainerState is a snapshot of a container's runtime state, projected from ContainerInspect into a library-owned type.

type ContainerTarget

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

ContainerTarget is the runtime surface a WaitStrategy can inspect. It wraps the Docker client and container ID internally so that strategies never need to import the docker client.

A ContainerTarget is only usable when constructed by the Executor (as passed to WaitStrategy.WaitUntilReady). The zero value holds a nil client and will panic on Logs and State.

func (ContainerTarget) ID

func (t ContainerTarget) ID() string

ID returns the container ID.

func (ContainerTarget) Logs

Logs streams the container's stdout and stderr (follow mode). The caller is responsible for closing the returned reader.

func (ContainerTarget) State

State inspects the container and projects the result into a ContainerState.

A nil inspect.State is treated as an error: it indicates an abnormal inspect response, and strategies polling for Running/HealthStatus would otherwise spin on a meaningless zero state until timeout. A nil NetworkSettings is tolerated (e.g., containers without networking) and yields an empty Ports map.

type Executor

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

Executor manages a Docker container lifecycle.

func New

func New(opts ...Option) (*Executor, error)

New creates a new Docker executor with functional options.

Example with functional options:

exec := docker.New(
    docker.WithImage("nginx:latest"),
    docker.WithPorts("80:8080"),
    docker.WithEnv("KEY=value"),
)

Example with ContainerRequest struct:

req := docker.ContainerRequest{
    Image: "nginx:latest",
    ExposedPorts: []string{"80/tcp"},
    Env: map[string]string{"KEY": "value"},
}
exec := docker.New(docker.WithRequest(req))

Example combining both:

req := docker.ContainerRequest{Image: "nginx:latest"}
exec := docker.New(
    docker.WithRequest(req),
    docker.WithOTelConfig(otelCfg), // Add observability
)
Example

New assembles an executor from functional options. Constructing the executor only validates configuration and creates a Docker client handle; no container is started until Start is called.

package main

import (
	"fmt"

	"github.com/jasoet/pkg/v3/docker"
)

func main() {
	exec, err := docker.New(
		docker.WithImage("nginx:latest"),
		docker.WithPorts("80:0"), // host port auto-assigned
		docker.WithEnv("ENV=production"),
		docker.WithAutoRemove(true),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer func() { _ = exec.Close() }()

	fmt.Println("executor created, container started:", exec.ContainerID() != "")

}
Output:
executor created, container started: false

func NewFromRequest

func NewFromRequest(req ContainerRequest, opts ...Option) (*Executor, error)

NewFromRequest creates a new Docker executor from a ContainerRequest struct. Additional options can be passed to override or extend the request configuration.

Example with just struct:

req := docker.ContainerRequest{
    Image: "nginx:latest",
    Env: map[string]string{"KEY": "value"},
}
exec, err := docker.NewFromRequest(req)

Example with additional options (options override struct fields):

req := docker.ContainerRequest{
    Image: "nginx:latest",
    Name:  "default-name",
}
exec, err := docker.NewFromRequest(req,
    docker.WithName("override-name"),  // Overrides struct name
    docker.WithOTelConfig(otelCfg),    // Adds observability
    docker.WithPorts("80:8080"),       // Adds port mapping
)
Example

NewFromRequest is sugar over New(WithRequest(req), ...): it builds an executor from a ContainerRequest struct and lets trailing options override individual struct fields.

package main

import (
	"fmt"
	"time"

	"github.com/jasoet/pkg/v3/docker"
)

func main() {
	req := docker.ContainerRequest{
		Image:        "postgres:18-alpine",
		ExposedPorts: []string{"5432/tcp"},
		Env: map[string]string{
			"POSTGRES_PASSWORD": "secret",
		},
		WaitingFor: docker.WaitForLog("ready to accept connections").
			WithStartupTimeout(60 * time.Second),
	}

	// WithName overrides the (empty) struct field; options always win.
	exec, err := docker.NewFromRequest(req, docker.WithName("my-postgres"))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer func() { _ = exec.Close() }()

	fmt.Println("executor created from request")

}
Output:
executor created from request

func (*Executor) Close

func (e *Executor) Close() error

Close closes the Docker client connection. The container is NOT terminated automatically - call Terminate() first if needed. After Close(), any method that uses the Docker client will return an error.

func (*Executor) ConnectionString

func (e *Executor) ConnectionString(ctx context.Context, containerPort, template string) (string, error)

ConnectionString builds a connection string for the container. This is useful for database containers. Use {{endpoint}} as the placeholder for the host:port value.

Example:

// For PostgreSQL
connStr, err := exec.ConnectionString(ctx, "5432/tcp", "postgres://user:pass@{{endpoint}}/db")
// Result: "postgres://user:pass@localhost:32768/db"

func (*Executor) ContainerID

func (e *Executor) ContainerID() string

ContainerID returns the Docker container ID. Returns empty string if container hasn't been started yet.

func (*Executor) Endpoint

func (e *Executor) Endpoint(ctx context.Context, containerPort string) (string, error)

Endpoint returns the full endpoint address (host:port) for a container port. This is a convenience method combining Host() and MappedPort().

Example:

endpoint, err := exec.Endpoint(ctx, "80/tcp")
// endpoint might be "localhost:32768"

// Use it directly with HTTP client
resp, err := http.Get("http://" + endpoint + "/health")

func (*Executor) ExitCode

func (e *Executor) ExitCode(ctx context.Context) (int, error)

ExitCode retrieves the container exit code. Returns an error if the container hasn't exited yet.

func (*Executor) FollowLogs

func (e *Executor) FollowLogs(ctx context.Context, w io.Writer, opts ...LogOption) error

FollowLogs streams container logs to the provided writer. This is useful for piping logs to stdout or a file.

Example:

err := exec.FollowLogs(ctx, os.Stdout)

func (*Executor) GetAllPorts

func (e *Executor) GetAllPorts(ctx context.Context) (map[string]string, error)

GetAllPorts returns all exposed ports and their mappings. Returns a map of container ports to host ports.

Example output:

{
    "80/tcp": "8080",
    "443/tcp": "8443",
}

func (*Executor) GetIPAddress

func (e *Executor) GetIPAddress(ctx context.Context, network string) (string, error)

GetIPAddress returns the container's IP address in a specific network. If network is empty, returns the IP from the first available network.

func (*Executor) GetLastNLines

func (e *Executor) GetLastNLines(ctx context.Context, n int) (string, error)

GetLastNLines retrieves the last N lines of logs.

func (*Executor) GetLogsSince

func (e *Executor) GetLogsSince(ctx context.Context, since string) (string, error)

GetLogsSince retrieves logs since a specific time. Time can be RFC3339 timestamp or duration string (e.g., "10m", "1h").

func (*Executor) GetNetworks

func (e *Executor) GetNetworks(ctx context.Context) ([]string, error)

GetNetworks returns all networks the container is connected to.

func (*Executor) GetStats

GetStats retrieves container resource usage statistics. This includes CPU, memory, network, and disk I/O stats. Returns the stats response reader which can be read and decoded by the caller. Remember to close the response body after reading.

func (*Executor) GetStderr

func (e *Executor) GetStderr(ctx context.Context) (string, error)

GetStderr retrieves only stderr logs (excludes stdout).

func (*Executor) GetStdout

func (e *Executor) GetStdout(ctx context.Context) (string, error)

GetStdout retrieves only stdout logs (excludes stderr).

func (*Executor) HealthCheck

func (e *Executor) HealthCheck(ctx context.Context) (*HealthStatus, error)

HealthCheck retrieves the current health status. Returns an error if health check is not configured for the container.

func (*Executor) Host

func (e *Executor) Host(_ context.Context) (string, error)

Host returns the container host address. For local Docker, this is always "localhost" since containers use port forwarding.

func (*Executor) Inspect

func (e *Executor) Inspect(ctx context.Context) (*container.InspectResponse, error)

Inspect returns the full container inspection details. This provides access to all container metadata.

func (*Executor) IsRunning

func (e *Executor) IsRunning(ctx context.Context) (bool, error)

IsRunning checks if the container is currently running.

func (*Executor) Logs

func (e *Executor) Logs(ctx context.Context, opts ...LogOption) (string, error)

Logs returns all container logs as a string. Use LogOptions for more control.

func (*Executor) MappedPort

func (e *Executor) MappedPort(ctx context.Context, containerPort string) (string, error)

MappedPort returns the host port mapped to a container port. Port format: "8080/tcp" or "8080" (defaults to tcp).

Example:

hostPort, err := exec.MappedPort(ctx, "80/tcp")
// hostPort might be "32768" (randomly assigned by Docker)

func (*Executor) Restart

func (e *Executor) Restart(ctx context.Context) error

Restart restarts the container. Note: There is a small TOCTOU window between the containerID check and the Docker API call. Concurrent Terminate() may cause a benign "container not found" error.

func (*Executor) Start

func (e *Executor) Start(ctx context.Context) error

Start pulls the image (if needed), creates and starts the container. If a wait strategy is configured, it blocks until the container is ready.

func (*Executor) Status

func (e *Executor) Status(ctx context.Context) (*Status, error)

Status retrieves the current container status.

func (*Executor) Stop

func (e *Executor) Stop(ctx context.Context) error

Stop gracefully stops the container (sends SIGTERM). The container can still be restarted after stopping. Note: There is a small TOCTOU window between the containerID check and the Docker API call. Concurrent Terminate() may cause a benign "container not found" error.

func (*Executor) StreamLogs

func (e *Executor) StreamLogs(ctx context.Context, opts ...LogOption) (<-chan LogEntry, <-chan error)

StreamLogs streams container logs to a channel. The channel is closed when streaming completes or context is canceled. Docker multiplexed stream headers are parsed to correctly identify stdout vs stderr.

func (*Executor) Terminate

func (e *Executor) Terminate(ctx context.Context) error

Terminate forcefully stops and removes the container. This is a destructive operation and the container cannot be restarted.

func (*Executor) Wait

func (e *Executor) Wait(ctx context.Context) (int64, error)

Wait blocks until the container stops and returns its exit code.

func (*Executor) WaitForState

func (e *Executor) WaitForState(ctx context.Context, targetState string, timeout time.Duration) error

WaitForState waits for the container to reach a specific state. Valid states: running, paused, restarting, removing, exited, dead

func (*Executor) WaitHealthy

func (e *Executor) WaitHealthy(ctx context.Context, timeout time.Duration) error

WaitHealthy waits for the container to become healthy. Returns an error if the container doesn't have health checks configured.

type HealthLog

type HealthLog struct {
	// Start is when the check started
	Start time.Time

	// End is when the check completed
	End time.Time

	// ExitCode is the health check exit code
	ExitCode int

	// Output is the health check output
	Output string
}

HealthLog represents a single health check result.

type HealthStatus

type HealthStatus struct {
	// Status is the health status (healthy, unhealthy, starting)
	Status string

	// FailingStreak is the number of consecutive failures
	FailingStreak int

	// Log contains recent health check results
	Log []HealthLog
}

HealthStatus represents container health check status.

type LogEntry

type LogEntry struct {
	// Stream identifies the source (stdout or stderr)
	Stream string

	// Content is the log line content
	Content string
}

LogEntry represents a single log entry from the container.

type LogOption

type LogOption func(*logOptions)

LogOption is a functional option for log retrieval.

func WithFollow

func WithFollow() LogOption

WithFollow streams logs in real-time (default: false).

func WithSince

func WithSince(since string) LogOption

WithSince shows logs since a timestamp (RFC3339) or duration (e.g., "10m").

func WithStderr

func WithStderr(include bool) LogOption

WithStderr includes stderr in the logs (default: true).

func WithStdout

func WithStdout(include bool) LogOption

WithStdout includes stdout in the logs (default: true).

func WithTail

func WithTail(lines string) LogOption

WithTail limits the number of lines from the end of the logs. Use "all" for all logs, or a number like "100" for last 100 lines.

func WithTimestamps

func WithTimestamps() LogOption

WithTimestamps includes timestamps in log entries (default: false).

func WithUntil

func WithUntil(until string) LogOption

WithUntil shows logs until a timestamp (RFC3339) or duration.

type Option

type Option func(*config) error

Option is a functional option for configuring the executor.

func WithAutoRemove

func WithAutoRemove(autoRemove bool) Option

WithAutoRemove automatically removes the container when it stops.

func WithCapAdd

func WithCapAdd(caps ...string) Option

WithCapAdd adds Linux capabilities to the container. WARNING: Adding capabilities increases the container's privileges. Only add capabilities that are strictly required.

func WithCapDrop

func WithCapDrop(caps ...string) Option

WithCapDrop drops Linux capabilities.

func WithCmd

func WithCmd(cmd ...string) Option

WithCmd sets the container command.

func WithEntrypoint

func WithEntrypoint(entrypoint ...string) Option

WithEntrypoint sets the container entrypoint.

func WithEnv

func WithEnv(env string) Option

WithEnv adds an environment variable in KEY=VALUE format.

func WithEnvMap

func WithEnvMap(env map[string]string) Option

WithEnvMap sets environment variables from a map.

func WithExposedPorts

func WithExposedPorts(ports ...string) Option

WithExposedPorts exposes ports without binding to host.

func WithHostname

func WithHostname(hostname string) Option

WithHostname sets the container hostname.

func WithImage

func WithImage(image string) Option

WithImage sets the container image.

func WithLabel

func WithLabel(key, value string) Option

WithLabel adds a container label.

func WithLabels

func WithLabels(labels map[string]string) Option

WithLabels sets multiple container labels.

func WithName

func WithName(name string) Option

WithName sets the container name.

func WithNetwork

func WithNetwork(network string) Option

WithNetwork attaches the container to a network.

func WithNetworkMode

func WithNetworkMode(mode string) Option

WithNetworkMode sets the network mode (bridge, host, none, container:<name>).

func WithNetworks

func WithNetworks(networks ...string) Option

WithNetworks attaches the container to multiple networks.

func WithOTelConfig

func WithOTelConfig(otelConfig *otel.Config) Option

WithOTelConfig enables OpenTelemetry instrumentation.

func WithPortBindings

func WithPortBindings(bindings map[string]string) Option

WithPortBindings sets detailed port bindings. Key: container port with protocol (e.g., "80/tcp") Value: host port (e.g., "8080")

func WithPorts

func WithPorts(portMapping string) Option

WithPorts adds port mappings in "containerPort:hostPort" format (e.g., "80:8080"). Protocol defaults to TCP. Use "80:8080/udp" for UDP.

func WithPrivileged

func WithPrivileged(privileged bool) Option

WithPrivileged runs the container in privileged mode. WARNING: Privileged containers have full access to the host. Only use with trusted images.

func WithRequest

func WithRequest(req ContainerRequest) Option

WithRequest creates an option from a ContainerRequest struct. This allows combining struct-based and functional options:

req := docker.ContainerRequest{Image: "nginx:latest", ...}
exec := docker.New(
    docker.WithRequest(req),
    docker.WithOTelConfig(otelCfg), // Additional options
)

func WithShmSize

func WithShmSize(size int64) Option

WithShmSize sets the size of /dev/shm in bytes.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the timeout for container operations.

func WithTmpfs

func WithTmpfs(path, options string) Option

WithTmpfs mounts a tmpfs filesystem.

func WithUser

func WithUser(user string) Option

WithUser sets the user.

func WithVolume

func WithVolume(hostPath, containerPath string) Option

WithVolume mounts a volume. Format: "hostPath:containerPath" or "hostPath:containerPath:ro"

func WithVolumeRO

func WithVolumeRO(hostPath, containerPath string) Option

WithVolumeRO mounts a read-only volume.

func WithVolumes

func WithVolumes(volumes map[string]string) Option

WithVolumes sets multiple volume mounts.

func WithWaitStrategy

func WithWaitStrategy(strategy WaitStrategy) Option

WithWaitStrategy sets the wait strategy for container readiness.

func WithWorkDir

func WithWorkDir(workDir string) Option

WithWorkDir sets the working directory.

type Status

type Status struct {
	// ID is the container ID
	ID string

	// Name is the container name
	Name string

	// Image is the container image
	Image string

	// State is the container state (running, exited, etc.)
	State string

	// Status is the detailed status string
	Status string

	// Running indicates if the container is running
	Running bool

	// Paused indicates if the container is paused
	Paused bool

	// Restarting indicates if the container is restarting
	Restarting bool

	// ExitCode is the exit code (only valid if container has exited)
	ExitCode int

	// Error contains any error message from the container
	Error string

	// StartedAt is when the container started
	StartedAt time.Time

	// FinishedAt is when the container finished (only if stopped)
	FinishedAt time.Time

	// Health is the health check status (if configured)
	Health *HealthStatus
}

Status represents the container status information.

type WaitStrategy

type WaitStrategy interface {
	// WaitUntilReady blocks until the container is ready or timeout occurs.
	WaitUntilReady(ctx context.Context, target ContainerTarget) error
}

WaitStrategy defines how to wait for a container to be ready.

Jump to

Keyboard shortcuts

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