docker

package module
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

README

go-docker-testsuite

Go Reference Go Version Last Commit License PRs Welcome

Go library to run third-party dependencies in Docker containers for integration testing. Spin up any Docker image — databases, queues, caches, or object storage — and connect to them via network from your Go tests.


Features

  • Container — low-level wrapper to create, run, await output, and clean up any Docker image
  • Group — run multiple containers in an isolated Docker network with IP-level connectivity
  • Applications — ready-to-use wrappers for popular services (MySQL, PostgreSQL, Redis, Kafka, etc.)
  • Hooks — lifecycle callbacks (BeforeRun, AfterRun, BeforeClose, AfterClose) per container
  • Exec — run commands inside a running container and capture stdout, stderr, and the exit code
  • Lifecycle commands — run startup / after-ready commands via exec (WithStartupCommand, WithAfterReadyCommand)
  • Copy files into containers — seed files before start with WithFiles (FileFromBytes for small content, or stream large files via io.Reader + Size)
  • Copy files out of containers — read a file/directory back out of a running container as a tar stream with docker.CopyFromContainer (inverse of WithFiles)
  • Resource limits — cap CPU/memory/pids to protect the host and CI (WithMemoryLimit, WithCPUs, WithPidsLimit, ...)
  • Matchers — await container logs with substring, exact, or regexp matchers before proceeding
  • Wait strategies — composable readiness probes beyond log matchers: ForLog, ForHTTPGet, ForCommand, ForTCPConnection, combined with ForAll / ForAny / ForAtLeast (github.com/teran/go-docker-testsuite/wait)
  • Environment builder — fluent DSL to declare typed environment variables
  • Port bindings — DNAT port mapping with random or one-to-one port allocation
  • Network mode — run a container on the host or a custom network (WithNetworkMode(NetworkModeHost), WithHostNetwork())
  • IMAGE_PREFIX — optional IMAGE_PREFIX env var to route images through a proxy/mirror
  • *testing.T binding — bind a container/group to a test for automatic teardown (t.Cleanup) and t.Logf lifecycle logging, with safe t.Parallel()

Requirements

  • Go 1.26+ (uses go.1.26.0 directive in go.mod)
  • A running Docker daemon (also works with remote Docker hosts via DOCKER_HOST, etc.)

Installation

The repository is a multi-module workspace: the root is the core module and each application under applications/<name> is its own Go module. Pull only what you need — the core, and any application wrapper — instead of dragging every database/queue client dependency into your go.mod:

# The core (Container, Group, wait, images, ...):
go get github.com/teran/go-docker-testsuite

# A specific application wrapper (own module, own version):
go get github.com/teran/go-docker-testsuite/applications/redis
go get github.com/teran/go-docker-testsuite/applications/postgres

go get .../applications/<name>@latest resolves the application's most recent tagged version (applications/<name>/v...) and transitively pulls the core at the version the application requires. See Modules & releases.

Applications

The test suite provides ready-to-use wrappers (each returns a typed client interface and handles startup, health checks, and cleanup). Several wrappers (PostgreSQL, OpenSearch, nginx, Redis, MySQL, MongoDB, Forgejo) use the wait package internally to poll for readiness. Here's the full list:

Application Package Description
Ceph (RGW) applications/ceph Ceph RGW (S3) with AWS SDK v2 client
ClickHouse applications/clickhouse ClickHouse with clickhouse-go (database/sql)
Forgejo applications/forgejo Forgejo (git hosting) with SQLite
K3s applications/k3s K3s (Kubernetes) with client-go
Kafka applications/kafka Apache Kafka with Sarama client
Memcache applications/memcache Memcached with gomemcache client
MinIO (Silo fork) applications/minio S3-compatible object storage
MongoDB applications/mongodb MongoDB with mongo-driver client
MySQL / MariaDB / Percona Server applications/mysql MySQL-compatible databases
NetBox applications/netbox NetBox (DCIM/IPAM) with PostgreSQL + Redis (Group)
Nginx applications/nginx nginx reverse proxy / web server
OpenSearch applications/opensearch OpenSearch with opensearch-go client
Paperless-ngx applications/paperless-ngx Paperless-ngx (document management) with PostgreSQL + Valkey (Group)
PostgreSQL applications/postgres PostgreSQL with pgx client
Prometheus applications/prometheus Prometheus with prometheus/client_golang (HTTP API)
RabbitMQ applications/rabbitmq RabbitMQ (AMQP + Management API)
Redis applications/redis Redis with go-redis client
ScyllaDB applications/scylladb ScyllaDB with gocql client
Vault applications/vault HashiCorp Vault
Libvirtd applications/libvirtd KVM/QEMU virtualization manager
— applications/*/versions/ Per-version integration tests

Ceph image: the Ceph wrapper runs the multi-arch demo image ghcr.io/teran/ceph-container/ceph:v<version> (built for the squid and tentacle release trains). It is built and published independently from github.com/teran/ceph-container. Use a specific version (e.g. ceph.NewWithImage(ctx, "ghcr.io/teran/ceph-container/ceph:v20.2.4")) when the default (images.Ceph) isn't the one you need.

Many application packages include testable Examples (Example* functions in *_test.go files) that demonstrate real usage. They are displayed on pkg.go.dev and can be verified locally:

# Run all examples (requires a running Docker daemon):
go test -run Example ./applications/... .

# Run a specific example:
go test -run "^Example$" ./applications/mysql/

Usage

Quick start — MySQL
package main

import (
    "context"
    "database/sql"
    "time"

    _ "github.com/go-sql-driver/mysql"

    "github.com/teran/go-docker-testsuite/applications/mysql"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
    defer cancel()

    app, err := mysql.New(ctx, "index.docker.io/library/mysql:8.0.4")
    if err != nil {
        panic(err)
    }
    defer app.Close(ctx)

    if err := app.CreateDB(ctx, "important_database"); err != nil {
        panic(err)
    }

    db, err := sql.Open("mysql", app.MustDSN("important_database"))
    if err != nil {
        panic(err)
    }
    defer db.Close()

    if _, err := db.ExecContext(ctx, "SELECT 1"); err != nil {
        panic(err)
    }
}
Multi-container group

Use docker.Group to run several containers in an isolated Docker network with internal DNS resolution:

package main

import (
    "context"
    "time"

    "github.com/teran/go-docker-testsuite"
    "github.com/teran/go-docker-testsuite/wait"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
    defer cancel()

    app := docker.NewApplication(
        c,
        docker.HookFunc(func(ctx context.Context, ht docker.HookType, c docker.Container) error {
            // e.g. wait for readiness before moving on
            return wait.Wait(ctx, c, wait.ForLog(docker.NewSubstringMatcher("ready")))
        }),
    )

    g, err := docker.NewGroup("my-services", app1, app2)
    if err != nil {
        panic(err)
    }

    if err := g.Run(ctx); err != nil {
        panic(err)
    }
    defer g.Close(ctx)
}
Wait strategies

wait.Wait(ctx, target, strategy, opts...) polls a container until a composable readiness strategy reports ready, returning nil on success or a wrapped error on timeout or a fatal condition. Both docker.Container and docker.TestContainer satisfy the wait.Target interface directly:

// Wait until the app's HTTP /health endpoint returns 200.
if err := wait.Wait(ctx, c, wait.ForHTTPGet(8080,
    wait.WithPath("/health"),
    wait.WithResponseStatuses(200),
)); err != nil {
    panic(err)
}

Other strategies: ForLog(matcher) (log-line match), ForCommand(cmd) (run a command in-container), and ForTCPConnection(port) (pure TCP liveness probe). Combine several with ForAll / ForAny / ForAtLeast, and tune polling with WithTimeout / WithInterval. These probes run against real containers, so they require a running Docker daemon.

*testing.T binding & t.Parallel()

Bind a container (or group) to a *testing.T so its lifecycle follows the test automatically — no manual defer c.Close. TestContainer wraps a Container and registers a t.Cleanup handler on the first Run, so the container is always stopped and removed when the test finishes, whether it succeeds, calls t.Fatal, panics, or skips. Cleanup handlers run in LIFO order within the test's context. TestContainer implements the full Container interface, so it can be used anywhere a Container is expected.

func TestRedis(t *testing.T) {
    t.Parallel()

    c, err := docker.NewContainerWithT(
        t,
        "redis",
        images.Redis,
        nil,
        docker.NewEnvironment(),
        docker.NewPortBindings().PortDNAT(docker.ProtoTCP, 6379),
    )
    if err != nil {
        t.Fatal(err)
    }

    c.RunT(ctx) // fail-fast; registers t.Cleanup

    // ... use container ... // no manual defer; cleanup on success/fatal/panic/skip
}

Two ways to start a bound container:

  • Run(ctx) error — returns the error (and logs it via t.Logf) rather than failing the test. This keeps TestContainer compatible with applications, groups, and existing code that expects the Container contract.
  • RunT(ctx) — fail-fast: if the container fails to start, the test is failed immediately with t.Fatal. Prefer this in tests.

To wrap an existing container or group (created with the base NewContainer/NewContainerWithLifecycle/NewGroup constructors) use BindToT(t, c) or BindGroupToT(t, g):

c, err := docker.NewContainer("my-service", "busybox:latest", nil, nil, nil)
if err != nil {
    t.Fatal(err)
}

docker.BindToT(t, c) // lifecycle now tied to the test
_ = c.Run(ctx)

Application packages expose T-bound constructors that create the container and start it, all tied to the test. Following the standard four-constructor contract (see SPEC → "Constructor contract"), wrappers with a default image expose NewWithT(t, ctx) and NewWithImageT(t, ctx, image) (e.g. applications/postgres, applications/mongodb), while the legacy mysql/redis/vault packages keep NewWithT(t, ctx, image) for backward compatibility.

Safe t.Parallel(). Each binding owns its own lifecycle: cleanups are registered against the correct per-test *testing.T and run in that test's context, so parallel tests do not interfere with one another's teardown. To avoid collisions, container/network names and host ports are randomized per container, so concurrent tests do not clash. Keep the parallel fan-out modest and combine t.Parallel() with resource limits (WithMemoryLimit, WithCPUs, WithPidsLimit) to protect the host and CI runners.

Lifecycle hooks

Every container supports hooks at four stages:

docker.HookTypeBeforeRun   // before container starts
docker.HookTypeAfterRun    // after container starts
docker.HookTypeBeforeClose // before container stops
docker.HookTypeAfterClose  // after container stops

Pass hooks via docker.NewApplication(container, hook1, hook2, ...).

Exec and lifecycle commands

Run a command inside a running container with Container.Exec and inspect its output and exit code:

res, err := c.Exec(ctx, []string{"echo", "hello"})
if err != nil {
    // infrastructure failure (create/attach/inspect or ctx cancelled)
    panic(err)
}

if err := res.Error(); err != nil {
    // non-zero exit code (res.ExitCode, res.Stderr available for diagnostics)
    panic(err)
}

fmt.Printf("exit code: %d\n", res.ExitCode)
fmt.Printf("stdout: %s", res.Stdout)

To run initialization commands automatically, use NewContainerWithLifecycle together with WithStartupCommand (executed right after the container starts) and WithAfterReadyCommand (executed once a readiness log line appears):

c, err := docker.NewContainerWithLifecycle(
    "my-service",
    "busybox:latest",
    []string{"sh", "-c", "echo READY; sleep 300"},
    nil,
    nil,
    docker.WithStartupCommand("sh", "-c", "echo boot > /tmp/startup.txt"),
    docker.WithAfterReadyCommand(
        docker.NewSubstringMatcher("READY"),
        "sh", "-c", "echo seeded > /tmp/seeded.txt",
    ),
)
if err != nil {
    panic(err)
}
defer c.Close(ctx)

if err := c.Run(ctx); err != nil {
    panic(err)
}

For host/CI protection, cap resource usage with the With* options (see SPEC.md → "Resource limits"):

c, err := docker.NewContainer(
    "my-service",
    "busybox:latest",
    []string{"sleep", "300"},
    nil,
    nil,
    docker.WithMemoryLimit(128*1024*1024), // 128 MiB
    docker.WithCPUs(0.5),                  // half a vCPU
    docker.WithPidsLimit(256),
)
Copying files into the container (WithFiles)

Seed files into the container filesystem before it starts with WithFiles. Files are packed into a tar and pushed via the Docker SDK CopyToContainer before WithStartupCommand runs, so both the image entrypoint and startup commands can consume them. WithFiles is a LifecycleOption, so it is used through NewContainerWithLifecycle.

For small in-memory config, use the FileFromBytes helper:

c, err := docker.NewContainerWithLifecycle(
    "my-service",
    "busybox:latest",
    []string{"sleep", "300"},
    nil,
    nil,
    docker.WithFiles(
        docker.FileFromBytes("/etc/app.conf", []byte("key=value\n"), 0600, 0, 0),
    ),
)
if err != nil {
    panic(err)
}
defer c.Close(ctx)

if err := c.Run(ctx); err != nil {
    panic(err)
}

For files larger than available RAM, pass an io.Reader and its exact size — the content is streamed into the tar without buffering in memory:

f, err := os.Open("/data/big.bin")
if err != nil {
    panic(err)
}
defer f.Close()

st, err := f.Stat()
if err != nil {
    panic(err)
}

c, err := docker.NewContainerWithLifecycle(
    "my-service",
    "busybox:latest",
    []string{"sleep", "300"},
    nil,
    nil,
    docker.WithFiles(docker.File{
        Content:     f,
        Size:        st.Size(),
        Mode:        0644,
        Destination: "/data/big.bin",
        // Uid/Gid default to 0 = root:root; set them to chown the file.
    }),
)
Copying files out of the container (CopyFromContainer)

To read a file or directory back out of a running container (e.g. to verify what the application produced), use docker.CopyFromContainer. It returns a tar stream that you unpack (e.g. with archive/tar) to obtain the content:

rc, err := docker.CopyFromContainer(ctx, c, "/tmp/report.txt")
if err != nil {
    panic(err)
}
defer rc.Close()

tr := tar.NewReader(rc)
if _, err := tr.Next(); err != nil {
    panic(err)
}

content, err := io.ReadAll(tr)
if err != nil {
    panic(err)
}

fmt.Printf("content: %s", content)

docker.CopyFromContainer works on any Container (including TestContainer) and requires the container to be running. Copying a directory (e.g. /data) returns tar entries named relative to the archive root (data/...). This is the inverse of WithFiles.

Image prefix / proxy

Set the IMAGE_PREFIX environment variable to prepend a registry mirror to all image references:

# Use a local mirror instead of Docker Hub
export IMAGE_PREFIX=registry-mirror.example.com

Modules & releases

The repository is a multi-module workspace (testcontainers-go style):

  • Core module: github.com/teran/go-docker-testsuite — the docker package plus wait, images, internal.
  • Application modules: github.com/teran/go-docker-testsuite/applications/<name> — each a standalone Go module that imports only the core (leaf nodes; they never import each other).

All modules share a single version number, tagged separately with Go's prefixed-submodule tag scheme:

  • core: v1.6.0
  • application: applications/redis/v1.6.0

Because an application requires the core at a concrete version, the core tag must exist before an application that depends on it can be tagged and fetched. Release order is therefore: publish the core first, then applications (each merged and tagged in its own PR).

Tag from the repo root with the Makefile:

make tag-core v1.6.0                # tag only the core
make tag-app redis v1.6.0           # tag applications/redis (same version)

A GitHub Release is created for every module tag (release.yml triggers on both v* and applications/*/v*), so each module gets its own Release with auto-generated notes.

During development, application go.mod files may carry a local replace github.com/teran/go-docker-testsuite => ../.. so a checkout builds against the unpublished core; this replace is removed before release (make lint fails if any go.mod has a replace). Local development and CI use go.work (generated by make work) so every module resolves the core and its siblings from the workspace instead of requiring a published tag.

Examples

Each application package includes testable examples. Run them with:

# Run all examples (needs Docker):
go test -run Example ./applications/... .

Project docs

License

This project is licensed under the Apache License, Version 2.0.

Documentation

Overview

Example (Container)

This example demonstrates using the low-level Container API: creating a container from a custom image, configuring environment variables and port bindings, waiting for a log line, and making gRPC calls.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/teran/echo-grpc-server/presenter/proto"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	c, err := docker.NewContainer(
		"echo-server",
		"ghcr.io/teran/echo-grpc-server:latest",
		nil,
		docker.NewEnvironment().
			StringVar("ADDR", ":5555").
			LogLevelVar("LOG_LEVEL", logrus.TraceLevel),
		docker.NewPortBindings().
			PortDNAT(docker.ProtoTCP, 5555),
	)
	if err != nil {
		fmt.Printf("error creating container: %v\n", err)
		return
	}
	defer func() { _ = c.Close(ctx) }()

	if err := c.Run(ctx); err != nil {
		fmt.Printf("error running container: %v\n", err)
		return
	}

	if err := c.AwaitOutput(ctx, docker.NewSubstringMatcher("running GRPC echo server")); err != nil {
		fmt.Printf("error waiting for server: %v\n", err)
		return
	}
	fmt.Println("server is ready")

	hp, err := c.URL(docker.ProtoTCP, 5555)
	if err != nil {
		fmt.Printf("error getting URL: %v\n", err)
		return
	}

	conn, err := grpc.NewClient(hp.String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fmt.Printf("error dialing: %v\n", err)
		return
	}
	defer func() { _ = conn.Close() }()

	cli := proto.NewEchoServiceClient(conn)
	resp, err := cli.Echo(ctx, &proto.EchoRequest{Message: "Hello!"})
	if err != nil {
		fmt.Printf("error calling Echo: %v\n", err)
		return
	}
	fmt.Printf("echo response: %s\n", resp.GetMessage())
}
Example (Group)

This example demonstrates the Group API: running two containers on the same internal Docker network so they can reach each other by container name.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/teran/echo-grpc-server/presenter/proto"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	defer cancel()

	awaitRunFn := func(ctx context.Context, ht docker.HookType, c docker.Container) error {
		if ht == docker.HookTypeAfterRun {
			return c.AwaitOutput(ctx, docker.NewSubstringMatcher("running GRPC echo server"))
		}
		return nil
	}

	svr, err := docker.NewContainer(
		"my-server",
		"ghcr.io/teran/echo-grpc-server:latest",
		nil,
		docker.NewEnvironment().
			StringVar("ADDR", ":5555").
			LogLevelVar("LOG_LEVEL", logrus.TraceLevel),
		docker.NewPortBindings().
			PortDNAT(docker.ProtoTCP, 5555),
	)
	if err != nil {
		fmt.Printf("error creating server container: %v\n", err)
		return
	}

	client, err := docker.NewContainer(
		"my-client",
		"ghcr.io/teran/echo-grpc-server:latest",
		nil,
		docker.NewEnvironment().
			StringVar("ADDR", ":5555").
			LogLevelVar("LOG_LEVEL", logrus.TraceLevel),
		docker.NewPortBindings().
			PortDNAT(docker.ProtoTCP, 5555),
	)
	if err != nil {
		fmt.Printf("error creating client container: %v\n", err)
		return
	}

	g, err := docker.NewGroup("my-group",
		docker.NewApplication(svr, awaitRunFn),
		docker.NewApplication(client, awaitRunFn),
	)
	if err != nil {
		fmt.Printf("error creating group: %v\n", err)
		return
	}
	defer func() { _ = g.Close(ctx) }()

	if err := g.Run(ctx); err != nil {
		fmt.Printf("error running group: %v\n", err)
		return
	}
	fmt.Println("group started")

	// Connect to client and call server by its DNS name.
	hp, err := client.URL(docker.ProtoTCP, 5555)
	if err != nil {
		fmt.Printf("error getting client URL: %v\n", err)
		return
	}

	conn, err := grpc.NewClient(hp.String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fmt.Printf("error dialing: %v\n", err)
		return
	}
	defer func() { _ = conn.Close() }()

	cli := proto.NewRemoteEchoServiceClient(conn)
	resp, err := cli.RemoteEcho(ctx, &proto.RemoteEchoRequest{
		Remote:  "my-server:5555",
		Message: "Hello across containers!",
	})
	if err != nil {
		fmt.Printf("error calling RemoteEcho: %v\n", err)
		return
	}
	fmt.Printf("remote echo response: %s\n", resp.GetMessage())
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrPortNotMapped             = errors.New("port not mapped")
	ErrDockerHostIPIsNotResolved = errors.New("docker host IP address cannot be resolved")
)

Functions

func CopyFromContainer added in v1.5.0

func CopyFromContainer(ctx context.Context, c Container, srcPath string) (io.ReadCloser, error)

CopyFromContainer copies the file or directory at srcPath out of the running container c, returning a tar stream that the caller must close and unpack (e.g. with archive/tar) to obtain the content. It works on any Container that supports the operation (the concrete container and TestContainer do); a Container without this capability returns an error.

Example

This example demonstrates copying a file out of a running container with docker.CopyFromContainer: the returned reader is a tar stream, which is unpacked to read the file's content back out for verification.

package main

import (
	"archive/tar"
	"context"
	"fmt"
	"io"
	"time"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	c, err := docker.NewContainer(
		"copy-example",
		"busybox:latest",
		[]string{"sleep", "300"},
		nil,
		nil,
	)
	if err != nil {
		fmt.Printf("error creating container: %v\n", err)
		return
	}
	defer func() { _ = c.Close(ctx) }()

	if err := c.Run(ctx); err != nil {
		fmt.Printf("error running container: %v\n", err)
		return
	}

	if _, err := c.Exec(ctx, []string{"sh", "-c", "echo 'secrets' > /tmp/report.txt"}); err != nil {
		fmt.Printf("error writing file: %v\n", err)
		return
	}

	rc, err := docker.CopyFromContainer(ctx, c, "/tmp/report.txt")
	if err != nil {
		fmt.Printf("error copying from container: %v\n", err)
		return
	}
	defer func() { _ = rc.Close() }()

	tr := tar.NewReader(rc)
	if _, err := tr.Next(); err != nil {
		fmt.Printf("error reading tar header: %v\n", err)
		return
	}

	content, err := io.ReadAll(tr)
	if err != nil {
		fmt.Printf("error reading content: %v\n", err)
		return
	}

	fmt.Printf("content: %s", content)
}

func DockerIP

func DockerIP() (string, error)

DockerIP returns docker node IP address for further connectivity usage

func NewHostConfig

func NewHostConfig(pb *PortBindings, opts ...ContainerOption) (*dockerContainer.HostConfig, error)

NewHostConfig creates new HostConfig instance

func OneToOneRandomPort added in v1.1.0

func OneToOneRandomPort(proto Protocol, srcPort uint16) (string, uint16, []string, error)

func ParseRAMSize added in v1.5.0

func ParseRAMSize(s string) (int64, error)

ParseRAMSize parses a human-readable size string (e.g. "512m", "1g", "1.5g") into bytes, wrapping github.com/docker/go-units.RAMInBytes via pkg/errors.

func RandomPort added in v1.1.0

func RandomPort(proto Protocol, dstPort uint16) (string, uint16, []string, error)

RandomPortTCP makes a query to the kernel about free high range IP address NOTE: it have some probability impact so could fail in the really small amount of cases

Types

type Application

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

func NewApplication

func NewApplication(c Container, hooks ...Hook) *Application

type Binding

type Binding struct {
	HostIP   string
	HostPort string
}

Binding reflects a mapping part of the internal & external port of the container

type Container

type Container interface {
	AwaitOutput(ctx context.Context, m Matcher) error
	Close(ctx context.Context) error
	Exec(ctx context.Context, cmd []string) (*ExecResult, error)
	GetOutput(ctx context.Context, m ...Matcher) ([]string, error)
	ID() ContainerID
	Name() string
	NetworkAttach(networkID string) error
	Ping(ctx context.Context) error
	Run(ctx context.Context) error
	URL(proto Protocol, port uint16) (*HostPort, error)
}

Container exposes interface to control the container runtime

func NewContainer

func NewContainer(name, image string, cmd []string, environment Environment, ports *PortBindings, opts ...ContainerOption) (Container, error)

New creates new container instance from remote docker image

Example (ResourceLimits)

This example demonstrates capping the resources of a test container so a runaway test cannot exhaust the host or CI runner: memory, CPU and process limits are set via the With* ContainerOptions.

package main

import (
	"context"
	"fmt"
	"time"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	c, err := docker.NewContainer(
		"limits-example",
		"busybox:latest",
		[]string{"sleep", "300"},
		nil,
		nil,
		docker.WithMemoryLimit(128*1024*1024), // 128 MiB
		docker.WithCPUs(0.5),                  // half a vCPU
		docker.WithPidsLimit(256),
	)
	if err != nil {
		fmt.Printf("error creating container: %v\n", err)
		return
	}
	defer func() { _ = c.Close(ctx) }()

	if err := c.Run(ctx); err != nil {
		fmt.Printf("error running container: %v\n", err)
		return
	}

	fmt.Println("container started with resource limits")
}

func NewContainerWithClient

func NewContainerWithClient(cli *client.Client, name, image string, cmd []string, env Environment, ports *PortBindings, opts ...ContainerOption) (Container, error)

NewContainerWithClient creates new container from remote docker image and allows to pass custom docker.Client instance

func NewContainerWithLifecycle added in v1.5.0

func NewContainerWithLifecycle(name, image string, cmd []string, environment Environment, ports *PortBindings, opts ...LifecycleOption) (Container, error)

NewContainerWithLifecycle is NewContainer plus lifecycle options. Existing NewContainer is unchanged.

Example

This example demonstrates NewContainerWithLifecycle: running a startup command right after the container starts and an after-ready command once a readiness log line is observed.

package main

import (
	"context"
	"fmt"
	"time"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	c, err := docker.NewContainerWithLifecycle(
		"lifecycle-example",
		"busybox:latest",
		[]string{"sh", "-c", "echo READY; sleep 300"},
		nil,
		nil,
		docker.WithStartupCommand("sh", "-c", "echo startup > /tmp/startup.txt"),
		docker.WithAfterReadyCommand(
			docker.NewSubstringMatcher("READY"),
			"sh", "-c", "echo seeded > /tmp/seeded.txt",
		),
	)
	if err != nil {
		fmt.Printf("error creating container: %v\n", err)
		return
	}
	defer func() { _ = c.Close(ctx) }()

	if err := c.Run(ctx); err != nil {
		fmt.Printf("error running container: %v\n", err)
		return
	}

	startup, err := c.Exec(ctx, []string{"cat", "/tmp/startup.txt"})
	if err != nil {
		fmt.Printf("error reading startup marker: %v\n", err)
		return
	}

	seeded, err := c.Exec(ctx, []string{"cat", "/tmp/seeded.txt"})
	if err != nil {
		fmt.Printf("error reading seeded marker: %v\n", err)
		return
	}

	fmt.Printf("startup: %s", startup.Stdout)
	fmt.Printf("seeded: %s", seeded.Stdout)
}
Example (WithFiles)

This example demonstrates seeding a small file into a container before it starts with WithFiles / FileFromBytes: the file is copied into the container filesystem during Run, before the entrypoint runs.

package main

import (
	"context"
	"fmt"
	"time"

	docker "github.com/teran/go-docker-testsuite"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	c, err := docker.NewContainerWithLifecycle(
		"withfiles-example",
		"busybox:latest",
		[]string{"sh", "-c", "cat /etc/app.conf; sleep 300"},
		nil,
		nil,
		docker.WithFiles(
			docker.FileFromBytes("/etc/app.conf", []byte("key=value\n"), 0600, 0, 0),
		),
	)
	if err != nil {
		fmt.Printf("error creating container: %v\n", err)
		return
	}
	defer func() { _ = c.Close(ctx) }()

	if err := c.Run(ctx); err != nil {
		fmt.Printf("error running container: %v\n", err)
		return
	}

	res, err := c.Exec(ctx, []string{"cat", "/etc/app.conf"})
	if err != nil {
		fmt.Printf("error reading file: %v\n", err)
		return
	}

	fmt.Printf("content: %s", res.Stdout)
}

type ContainerFileCopier added in v1.5.0

type ContainerFileCopier interface {
	CopyFromContainer(ctx context.Context, srcPath string) (io.ReadCloser, error)
}

ContainerFileCopier is implemented by containers that can copy a file or directory out of a running container as a tar stream. It is an optional capability (not part of the Container interface): the concrete container and TestContainer satisfy it, and docker.CopyFromContainer resolves it for callers holding only the Container interface.

type ContainerID

type ContainerID = string

type ContainerInfo added in v1.1.0

type ContainerInfo interface {
	GetExternalPortMapping(Protocol, uint16) (uint16, error)
	GetDockerHostIP() (string, error)
}

type ContainerOption added in v1.3.0

type ContainerOption func(*dockerContainer.HostConfig)

ContainerOption modifies the docker HostConfig before container creation.

func WithBinds added in v1.3.0

func WithBinds(binds ...string) ContainerOption

WithBinds adds volume bind mounts (host:container[:mode]).

func WithCPUs added in v1.5.0

func WithCPUs(count float64) ContainerOption

WithCPUs sets the CPU limit as a fractional vCPU count via HostConfig.NanoCPUs (e.g. 0.5 → 500000000 nanos). Non-positive values are ignored (no-op).

func WithCapAdd added in v1.5.0

func WithCapAdd(caps ...string) ContainerOption

WithCapAdd grants additional Linux capabilities (e.g. "NET_ADMIN", "SYS_NICE") beyond the Docker default set. Values are capability names without the "CAP_" prefix, as accepted by Docker.

func WithCapDrop added in v1.5.0

func WithCapDrop(caps ...string) ContainerOption

WithCapDrop removes Linux capabilities from the container's default set for least-privilege hardening. Values are capability names without the "CAP_" prefix. Passing "ALL" drops every capability; combine with WithCapAdd to whitelist only what is needed.

func WithCpusetCpus added in v1.5.0

func WithCpusetCpus(cpus string) ContainerOption

WithCpusetCpus pins the container to specific host CPUs via HostConfig.CpusetCpus (e.g. "0-2,7").

func WithDevices added in v1.5.0

func WithDevices(devices ...string) ContainerOption

WithDevices maps host devices into the container (e.g. "/dev/kvm", "/dev/net/tun"). Each entry is "hostPath" or "hostPath:containerPath[:mode]". This is required for workloads that need direct access to host devices, such as KVM/QEMU virtualization (libvirtd) or TUN/TAP networking.

func WithHostNetwork added in v1.5.0

func WithHostNetwork() ContainerOption

WithHostNetwork runs the container on the host network namespace (HostConfig.NetworkMode "host"): 127.0.0.1 inside the container is the host's loopback, and Docker ignores port bindings.

func WithMemoryLimit added in v1.5.0

func WithMemoryLimit(bytes int64) ContainerOption

WithMemoryLimit caps the container's memory usage (bytes) via HostConfig.Memory.

func WithMemoryReservation added in v1.5.0

func WithMemoryReservation(bytes int64) ContainerOption

WithMemoryReservation sets a soft memory reservation (bytes) via HostConfig.MemoryReservation.

func WithMemorySwap added in v1.5.0

func WithMemorySwap(bytes int64) ContainerOption

WithMemorySwap sets the swap limit (bytes) via HostConfig.MemorySwap. -1 disables swap; a value equal to Memory effectively disables swap; typically callers pass 2*Memory to allow double the memory in swap.

func WithNetworkMode added in v1.5.0

func WithNetworkMode(mode NetworkMode) ContainerOption

WithNetworkMode sets the container's network mode (HostConfig.NetworkMode). Note that under NetworkModeHost (and NetworkModeNone) Docker ignores port bindings, so any configured PortDNAT mappings are effectively dropped.

func WithPidsLimit added in v1.5.0

func WithPidsLimit(limit int64) ContainerOption

WithPidsLimit caps the number of processes inside the container via HostConfig.PidsLimit.

func WithPrivileged added in v1.3.0

func WithPrivileged() ContainerOption

WithPrivileged grants the container elevated privileges.

func WithSecurityOpt added in v1.5.0

func WithSecurityOpt(opts ...string) ContainerOption

WithSecurityOpt sets Docker security options (e.g. "seccomp=unconfined", "apparmor=unconfined"). Needed by workloads such as QEMU/libvirtd whose syscalls may be blocked by Docker's default seccomp profile when running in a least-privilege (non-privileged) configuration.

func WithTmpfs added in v1.3.0

func WithTmpfs(m map[string]string) ContainerOption

WithTmpfs mounts tmpfs filesystems at the given paths.

func WithUlimit added in v1.4.0

func WithUlimit(name string, soft, hard int64) ContainerOption

WithUlimit sets an ulimit (e.g. nofile) on the container's HostConfig. Some images (e.g. Ceph) start much slower under Docker's default limits, so callers can raise the soft/hard limit.

type Environment

type Environment map[string]func(c ContainerInfo) string

Environment represents the container environment passed into runtime

func NewEnvironment

func NewEnvironment() Environment

NewEnvironment creates new Environment instance

func (Environment) BoolVar

func (e Environment) BoolVar(name string, value bool) Environment

BoolVar sets bool var to the environment

func (Environment) Eval added in v1.1.0

func (e Environment) Eval(c ContainerInfo) (es []string)

func (Environment) Int8Var

func (e Environment) Int8Var(name string, value int8) Environment

Int8Var sets int8 var to the environment

func (Environment) Int16Var

func (e Environment) Int16Var(name string, value int16) Environment

Int16Var sets int16 var to the environment

func (Environment) Int32Var

func (e Environment) Int32Var(name string, value int32) Environment

Int32Var sets int32 var to the environment

func (Environment) Int64Var

func (e Environment) Int64Var(name string, value int64) Environment

Int64Var sets int64 var to the environment

func (Environment) IntVar

func (e Environment) IntVar(name string, value int) Environment

IntVar sets int var to the environment

func (Environment) LogLevelVar

func (e Environment) LogLevelVar(name string, l log.Level) Environment

LogLevelVar sets logrus.Level var to the environment

func (Environment) StringVar

func (e Environment) StringVar(name, value string) Environment

StringVar sets string var to the environment

func (Environment) Uint8Var

func (e Environment) Uint8Var(name string, value uint8) Environment

Uint8Var sets uint8 var to the environment

func (Environment) Uint16Var

func (e Environment) Uint16Var(name string, value uint16) Environment

Uint16Var sets uint16 var to the environment

func (Environment) Uint32Var

func (e Environment) Uint32Var(name string, value uint32) Environment

Uint32Var sets uint32 var to the environment

func (Environment) Uint64Var

func (e Environment) Uint64Var(name string, value uint64) Environment

Uint64Var sets uint64 var to the environment

func (Environment) UintVar

func (e Environment) UintVar(name string, value uint) Environment

UintVar sets uint var to the environment

func (Environment) Var added in v1.1.0

func (e Environment) Var(name string, vfn func(c ContainerInfo) string) Environment

Var allows to set custom function to generate environment variable

type ExecResult added in v1.5.0

type ExecResult struct {
	Stdout   []byte
	Stderr   []byte
	ExitCode int
}

ExecResult carries the captured output and exit status of an Exec call.

func (*ExecResult) Combined added in v1.5.0

func (r *ExecResult) Combined() []byte

Combined returns Stdout followed by Stderr concatenated.

func (*ExecResult) Error added in v1.5.0

func (r *ExecResult) Error() error

Error returns a non-nil error if the command exited non-zero (includes exit code + stderr for diagnostics); nil when ExitCode == 0.

type File added in v1.5.0

type File struct {
	Content     io.Reader   // streamed file content; Size bytes must be available
	Size        int64       // exact byte length of Content (required)
	Mode        os.FileMode // permission bits; 0 defaults to 0644
	Destination string      // absolute path inside the container, e.g. "/etc/app.conf"
	Uid         int         // numeric owner uid; 0 (default) = root
	Gid         int         // numeric owner gid; 0 (default) = root
}

File describes a single file to copy into the container filesystem before the container starts. Content is streamed from an io.Reader and written into the tar verbatim, so files larger than available RAM can be copied without buffering them in memory. Size MUST equal the exact number of bytes Content will yield: it is written into the tar header and used to stream exactly Size bytes (Content is not buffered).

Mode is the Unix permission bits (0 defaults to 0644); Destination is the absolute path inside the container (parent directories are created automatically). Uid and Gid set the numeric owner of the file; both default to 0 (root:root) when omitted. Docker honours the numeric ids and only the file itself is chowned — auto-created parent directories stay root:root (0755).

func FileFromBytes added in v1.5.0

func FileFromBytes(destination string, data []byte, mode os.FileMode, uid, gid int) File

FileFromBytes builds a File from an in-memory byte slice. It wraps data in a bytes.Reader and sets Size to len(data). Use this for small configuration and seed content; use File directly with an io.Reader + Size for large files.

type Group

type Group interface {
	Run(ctx context.Context) error
	Close(ctx context.Context) error
}

func NewGroup

func NewGroup(name string, apps ...*Application) (Group, error)

func NewGroupWithClient

func NewGroupWithClient(cli *client.Client, name string, apps ...*Application) (Group, error)

type Hook

type Hook func(context.Context, HookType, Container) error

type HookType

type HookType string
const (
	HookTypeBeforeRun   HookType = "before_run"
	HookTypeAfterRun    HookType = "after_run"
	HookTypeBeforeClose HookType = "before_close"
	HookTypeAfterClose  HookType = "after_close"
)

type HostConfigSpec

type HostConfigSpec struct {
	PortBindings map[string][]Binding
}

HostConfigSpec is just a wrapper structure to pass host configuration to the container

type HostPort

type HostPort struct {
	Host string
	Port uint16
}

HostPort allows to return host & port from the URL method

func (*HostPort) String

func (hp *HostPort) String() string

String returns IP:Port pair

type LifecycleOption added in v1.5.0

type LifecycleOption func(*container)

LifecycleOption configures behavior that runs inside the container during Run(), in addition to HostConfig-based ContainerOptions. It does not modify the Docker HostConfig.

func WithAfterReadyCommand added in v1.5.0

func WithAfterReadyCommand(ready Matcher, cmd ...string) LifecycleOption

WithAfterReadyCommand runs cmd inside the container once the readiness matcher is satisfied by the container output. When ready is nil the command runs immediately after the startup command (or right after start if none).

func WithFiles added in v1.5.0

func WithFiles(files ...File) LifecycleOption

WithFiles copies the given files into the container filesystem during Run(), immediately after the container is created (and attached to the network) and before it starts — and therefore before WithStartupCommand runs.

Files are packed into a single tar and pushed via the Docker SDK CopyToContainer at the container root; parent directories are auto-created. An empty file list is a no-op.

func WithHostConfig added in v1.5.0

func WithHostConfig(opts ...ContainerOption) LifecycleOption

WithHostConfig adapts ordinary ContainerOption values so they can be supplied alongside lifecycle options. They modify the Docker HostConfig as usual.

func WithStartupCommand added in v1.5.0

func WithStartupCommand(cmd ...string) LifecycleOption

WithStartupCommand runs cmd inside the container immediately after it starts, failing Run() if the command errors or exits non-zero.

type Matcher

type Matcher func(l string) bool

Matcher allows to create any kind of matcher for container outputs

func NewExactMatcher

func NewExactMatcher(s string) Matcher

NewExactMatcher represents exact matcher i.e. the output should be exactly matched (except space chars around the word)

func NewRegexpMatcher

func NewRegexpMatcher(r *regexp.Regexp) Matcher

NewRegexpMatcher returns a matcher that succeeds when the line matches the compiled regular expression.

func NewSubstringMatcher

func NewSubstringMatcher(s string) Matcher

NewSubstringMatcher represents partial matcher

type NetworkID

type NetworkID = string

type NetworkMode added in v1.5.0

type NetworkMode string

NetworkMode enumerates Docker container network modes (roadmap #19).

const (
	// NetworkModeBridge is the default: the container sits on the Docker
	// bridge with explicit port mappings (HostConfig.NetworkMode "default").
	NetworkModeBridge NetworkMode = "bridge"

	// NetworkModeHost shares the host network namespace: 127.0.0.1 inside the
	// container IS the host's loopback, and no port mappings apply. Under this
	// mode Docker ignores port bindings.
	NetworkModeHost NetworkMode = "host"

	// NetworkModeNone disables networking entirely. Under this mode Docker
	// ignores port bindings.
	NetworkModeNone NetworkMode = "none"
)

type PortAllocator added in v1.1.0

type PortAllocator func(proto Protocol, port uint16) (string, uint16, []string, error)

type PortBindings

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

PortBindings is a full mapping of internal & external docker container ports

func NewDirectPortBinding added in v1.1.0

func NewDirectPortBinding() *PortBindings

func NewPortBindings

func NewPortBindings() *PortBindings

NewPortBindings creates new PortBindings instance

func NewPortBindingsWithPortAllocator added in v1.1.0

func NewPortBindingsWithPortAllocator(allocator PortAllocator) *PortBindings

NewPortBindingsWithTCPPortAllocator creates new PortBinding instance and allows to pass custom port allocation function

func (*PortBindings) PortDNAT

func (pb *PortBindings) PortDNAT(proto Protocol, port uint16) *PortBindings

PortDNAT adds new port to be exposed from the container

type Protocol

type Protocol string

Protocol to be NAT'ed from the container

const (
	// ProtoTCP ...
	ProtoTCP Protocol = "tcp"

	// ProtoUDP ...
	ProtoUDP Protocol = "udp"
)

func (Protocol) String

func (p Protocol) String() string

String returns the string representation of the protocol

type TestContainer added in v1.5.0

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

TestContainer binds a Container to a *testing.T so its lifecycle is tied to the test. It implements the full Container interface by delegating to the wrapped Container, while additionally:

  • registering a t.Cleanup handler on the first Run so the container is always stopped and removed when the test finishes (including on success, t.Fatal, panic and t.Skip), and
  • making Close idempotent, so overlapping cleanups (the t.Cleanup handler and an explicit wrapper Close) do not double-remove the container.

Each binding owns its own lifecycle, so TestContainer is safe to use with t.Parallel(): cleanups are registered against the correct per-test T and run in LIFO order within that test's context.

func BindToT added in v1.5.0

func BindToT(t *testing.T, c Container) *TestContainer

BindToT wraps an existing Container with a *testing.T, tying its lifecycle (run cleanup + idempotent close) to the test. Use this when the container was created with the base NewContainer/NewContainerWithLifecycle constructors but should be cleaned up automatically by the test.

func NewContainerWithLifecycleT added in v1.5.0

func NewContainerWithLifecycleT(t *testing.T, name, image string, cmd []string, env Environment, ports *PortBindings, opts ...LifecycleOption) (*TestContainer, error)

NewContainerWithLifecycleT is NewContainerWithLifecycle bound to a *testing.T. It behaves like NewContainerWithLifecycle but returns a TestContainer whose lifecycle is tied to the test.

func NewContainerWithT added in v1.5.0

func NewContainerWithT(t *testing.T, name, image string, cmd []string, env Environment, ports *PortBindings, opts ...ContainerOption) (*TestContainer, error)

NewContainerWithT is NewContainer bound to a *testing.T. It creates a container exactly like NewContainer but returns a TestContainer whose lifecycle is tied to the test.

func (*TestContainer) AwaitOutput added in v1.5.0

func (tc *TestContainer) AwaitOutput(ctx context.Context, m Matcher) error

AwaitOutput delegates to the wrapped container.

func (*TestContainer) Close added in v1.5.0

func (tc *TestContainer) Close(ctx context.Context) error

Close stops and removes the container. It is idempotent: only the first call performs the work; subsequent calls return nil.

func (*TestContainer) CopyFromContainer added in v1.5.0

func (tc *TestContainer) CopyFromContainer(ctx context.Context, srcPath string) (io.ReadCloser, error)

CopyFromContainer delegates to the wrapped container, returning a tar stream of the file or directory at srcPath. Works even when the wrapped Container holds the base interface, by resolving the optional capability.

func (*TestContainer) Exec added in v1.5.0

func (tc *TestContainer) Exec(ctx context.Context, cmd []string) (*ExecResult, error)

Exec delegates to the wrapped container, logging the outcome at a low-noise level.

func (*TestContainer) GetOutput added in v1.5.0

func (tc *TestContainer) GetOutput(ctx context.Context, ms ...Matcher) ([]string, error)

GetOutput delegates to the wrapped container.

func (*TestContainer) ID added in v1.5.0

func (tc *TestContainer) ID() ContainerID

ID returns the Docker container ID (empty until Run).

func (*TestContainer) Name added in v1.5.0

func (tc *TestContainer) Name() string

Name returns the container name.

func (*TestContainer) NetworkAttach added in v1.5.0

func (tc *TestContainer) NetworkAttach(networkID string) error

NetworkAttach delegates to the wrapped container.

func (*TestContainer) Ping added in v1.5.0

func (tc *TestContainer) Ping(ctx context.Context) error

Ping delegates to the wrapped container, logging the outcome at a low-noise level.

func (*TestContainer) Run added in v1.5.0

func (tc *TestContainer) Run(ctx context.Context) error

Run starts the container and registers the test cleanup on the first call. On error it returns the error (logging it via t.Logf) rather than failing the test, so it remains compatible with applications and groups.

func (*TestContainer) RunT added in v1.5.0

func (tc *TestContainer) RunT(ctx context.Context)

RunT starts the container, registers the test cleanup and fails the test immediately (t.Fatal) if the container fails to start.

func (*TestContainer) URL added in v1.5.0

func (tc *TestContainer) URL(proto Protocol, port uint16) (*HostPort, error)

URL delegates to the wrapped container.

type TestGroup added in v1.5.0

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

TestGroup binds a Group to a *testing.T so its lifecycle is tied to the test. It delegates Run/Close to the wrapped Group while additionally registering a t.Cleanup handler on the first Run and making Close idempotent.

The group's Close removes the member containers and then the internal network. This is protected against double-removal both by the idempotent group Close and by the idempotent per-container Close (so a group whose members are individually bound to the test is still safe to clean up from both paths). Like TestContainer, TestGroup is safe to use with t.Parallel().

func BindGroupToT added in v1.5.0

func BindGroupToT(t *testing.T, g Group) *TestGroup

BindGroupToT wraps an existing Group with a *testing.T, tying its lifecycle (run cleanup + idempotent close) to the test. Use this when the group was created with the base NewGroup constructor but should be cleaned up automatically by the test.

func NewGroupT added in v1.5.0

func NewGroupT(t *testing.T, name string, apps ...*Application) (*TestGroup, error)

NewGroupT is NewGroup bound to a *testing.T. It creates a group exactly like NewGroup but returns a TestGroup whose lifecycle is tied to the test.

func (*TestGroup) Close added in v1.5.0

func (tg *TestGroup) Close(ctx context.Context) error

Close removes the group's member containers and internal network. It is idempotent: only the first call performs the work; subsequent calls return nil.

func (*TestGroup) Run added in v1.5.0

func (tg *TestGroup) Run(ctx context.Context) error

Run starts the group and registers the test cleanup on the first call. On error it returns the error (logging it via t.Logf) rather than failing the test.

func (*TestGroup) RunT added in v1.5.0

func (tg *TestGroup) RunT(ctx context.Context)

RunT starts the group, registers the test cleanup and fails the test immediately (t.Fatal) if the group fails to start.

Directories

Path Synopsis
applications
forgejo module
netbox module
internal
tools
cmd/split_test_groups command
split_test_groups discovers every Go module in the repository and emits a CI matrix, one entry per module, so tests can be run in parallel GitHub Actions jobs.
split_test_groups discovers every Go module in the repository and emits a CI matrix, one entry per module, so tests can be run in parallel GitHub Actions jobs.
Package wait provides composable readiness wait-strategies for Docker containers.
Package wait provides composable readiness wait-strategies for Docker containers.

Jump to

Keyboard shortcuts

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