Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work.
Use Docker to run your Go integration tests against third party services on Windows, macOS, and Linux!
Dockertest supports running any Docker image from Docker Hub or from a Dockerfile.
- Pool creation - Running containers - Container configuration - Container reuse - Getting connection info - Waiting for readiness - Executing commands - Container logs - Building from Dockerfile - Networks - Cleanup - Error handling - Out of disk space - GitHub Actions - GitLab CI - Shared runners - Custom (group) runners
Why should I use Dockertest?
When developing applications, it is often necessary to use services that talk to a database system. Unit testing these services can be cumbersome because mocking database/DBAL is strenuous. Making slight changes to the schema implies rewriting at least some, if not all mocks. The same goes for API changes in the DBAL.
To avoid this, it is smarter to test these specific services against a real database that is destroyed after testing. Docker is the perfect system for running integration tests as you can spin up containers in a few seconds and kill them when the test completes.
The Dockertest library provides easy to use commands for spinning up Docker containers and using them for your tests.
Installation
go get github.com/ory/dockertest/v4
Quick Start
package myapp_test
import ( "testing" "time"
dockertest "github.com/ory/dockertest/v4" )
func TestPostgres(t *testing.T) { pool := dockertest.NewPoolT(t, "")
// Container is automatically reused across test runs based on "postgres:14". postgres := pool.RunT(t, "postgres", dockertest.WithTag("14"), dockertest.WithEnv([]string{ "POSTGRES_PASSWORD=secret", "POSTGRES_DB=testdb", }), )
hostPort := postgres.GetHostPort("5432/tcp") // Connect to postgres://postgres:secret@hostPort/testdb
// Wait for PostgreSQL to be ready err := pool.Retry(t.Context(), 30*time.Second, func() error { // try connecting... return nil }) if err != nil { t.Fatalf("Could not connect: %v", err) } }
Migration from v3
Version 4 introduces automatic container reuse, making tests significantly faster by reusing containers across test runs. Additionally, a lightweight docker client is used which reduces third party dependencies significantly.
See UPGRADE.md for the complete migration guide.
API overview
View the Go API documentation.
Pool creation
// For tests - auto-cleanup with t.Cleanup()
pool := dockertest.NewPoolT(t, "")
// With options pool := dockertest.NewPoolT(t, "", dockertest.WithMaxWait(2*time.Minute), )
// With a custom Docker client pool := dockertest.NewPoolT(t, "", dockertest.WithMobyClient(myClient), )
// For non-test code - requires manual Close() ctx := context.Background() pool, err := dockertest.NewPool(ctx, "") if err != nil { panic(err) } defer pool.Close(ctx)
Running containers
// Test helper - fails test on error
resource := pool.RunT(t, "postgres",
dockertest.WithTag("14"),
dockertest.WithEnv([]string{"POSTGRES_PASSWORD=secret"}),
dockertest.WithCmd([]string{"postgres", "-c", "log_statement=all"}),
)
// With error handling resource, err := pool.Run(ctx, "postgres", dockertest.WithTag("14"), dockertest.WithEnv([]string{"POSTGRES_PASSWORD=secret"}), ) if err != nil { panic(err) }
See Cleanup for container lifecycle management.
Container configuration
Customize container settings with configuration options:
resource := pool.RunT(t, "postgres",
dockertest.WithTag("14"),
dockertest.WithUser("postgres"),
dockertest.WithWorkingDir("/var/lib/postgresql/data"),
dockertest.WithLabels(map[string]string{
"test": "integration",
"service": "database",
}),
dockertest.WithHostname("test-db"),
dockertest.WithEnv([]string{"POSTGRES_PASSWORD=secret"}),
)
Available configuration options:
WithTag(tag string)- Set the image tag (default:"latest")WithEnv(env []string)- Set environment variablesWithCmd(cmd []string)- Override the default commandWithEntrypoint(entrypoint []string)- Override the default entrypointWithUser(user string)- Set the user to run commands as (supports "user" or
WithWorkingDir(dir string)- Set the working directoryWithLabels(labels map[string]string)- Add labels to the containerWithHostname(hostname string)- Set the container hostnameWithName(name string)- Set the container nameWithMounts(binds []string)- Set bind mounts ("host:container" or
WithPortBindings(bindings network.PortMap)- Set explicit port bindingsWithReuseID(id string)- Set a custom reuse key (default:
"repository:tag")
WithoutReuse()- Disable container reuse for this runWithContainerConfig(modifier func(*container.Config))- Modify the container
WithHostConfig(modifier func(*container.HostConfig))- Modify the host
For advanced container configuration, use WithContainerConfig:
stopTimeout := 30
resource := pool.RunT(t, "app",
dockertest.WithContainerConfig(func(cfg *container.Config) {
cfg.StopTimeout = &stopTimeout
cfg.StopSignal = "SIGTERM"
cfg.Healthcheck = &container.HealthConfig{
Test: []string{"CMD", "curl", "-f", "http://localhost/health"},
Interval: 10 * time.Second,
Timeout: 5 * time.Second,
Retries: 3,
}
}),
)
For host-level configuration, use WithHostConfig:
resource := pool.RunT(t, "postgres",
dockertest.WithTag("14"),
dockertest.WithHostConfig(func(hc *container.HostConfig) {
hc.RestartPolicy = container.RestartPolicy{
Name: container.RestartPolicyOnFailure,
MaximumRetryCount: 3,
}
}),
)
Container reuse
Containers are automatically reused based on repository:tag. Reuse is reference-counted: each Run/RunT call increments the ref count, and each Close/cleanup decrements it. The container is only removed from Docker when the last reference is released.
// First test creates container
r1 := pool.RunT(t, "postgres", dockertest.WithTag("14"))
// Second test reuses the same container r2 := pool.RunT(t, "postgres", dockertest.WithTag("14"))
// r1 and r2 point to the same container
Disable reuse if needed:
resource := pool.RunT(t, "postgres",
dockertest.WithTag("14"),
dockertest.WithoutReuse(), // Always create new container
)
Getting connection info
resource := pool.RunT(t, "postgres", dockertest.WithTag("14"))
// Get host:port (e.g., "127.0.0.1:54320") hostPort := resource.GetHostPort("5432/tcp")
// Get just the port (e.g., "54320") port := resource.GetPort("5432/tcp")
// Get just the IP (e.g., "127.0.0.1") ip := resource.GetBoundIP("5432/tcp")
// Get container ID id := resource.ID()
Waiting for readiness
Use pool.Retry to wait for a container to become ready:
err := pool.Retry(t.Context(), 30*time.Second, func() error {
return db.Ping()
})
if err != nil {
t.Fatalf("Container not ready: %v", err)
}
If timeout is 0, pool.MaxWait (default 60s) is used. The retry interval is fixed at 1 second.
For more control, use the package-level functions:
// Fixed interval retry
err := dockertest.Retry(ctx, 30time.Second, 500time.Millisecond, func() error {
return db.Ping()
})
// Exponential backoff retry err := dockertest.RetryWithBackoff(ctx, 30*time.Second, // timeout 100*time.Millisecond, // initial interval 5*time.Second, // max interval func() error { return db.Ping() }, )
Executing commands
Run commands inside a running container:
result, err := resource.Exec(ctx, []string{"pg_isready", "-U", "postgres"})
if err != nil {
t.Fatal(err)
}
if result.ExitCode != 0 {
t.Fatalf("command failed: %s", result.StdErr)
}
t.Log(result.StdOut)
Container logs
// Get all logs with stdout and stderr separated
stdout, stderr, err := resource.Logs(ctx)
if err != nil {
t.Fatal(err)
}
t.Log(stdout)
t.Log(stderr)
// Stream logs until container exits or ctx is cancelled var buf bytes.Buffer err = resource.FollowLogs(ctx, &buf, io.Discard)
Building from Dockerfile
Build a Docker image from a Dockerfile and run it:
version := "1.0.0"
resource, err := pool.BuildAndRun(ctx, "myapp:test",
&dockertest.BuildOptions{
ContextDir: "./testdata",
Dockerfile: "Dockerfile.test",
BuildArgs: map[string]*string{"VERSION": &version},
},
dockertest.WithEnv([]string{"APP_ENV=test"}),
)
if err != nil {
t.Fatal(err)
}
BuildAndRunT is the test helper variant:
resource := pool.BuildAndRunT(t, "myapp:test",
&dockertest.BuildOptions{
ContextDir: "./testdata",
},
)
Networks
Create Docker networks for container-to-container communication:
net := pool.CreateNetworkT(t, "my-network", nil)
// Connect a container err := resource.ConnectToNetwork(ctx, net)
// Get the container's IP in the network ip := resource.GetIPInNetwork(net)
// Disconnect err := resource.DisconnectFromNetwork(ctx, net)
With custom options:
net, err := pool.CreateNetwork(ctx, "my-network", &dockertest.NetworkCreateOptions{
Driver: "bridge",
Internal: true,
})
Cleanup
NewPoolT + RunT (recommended): Cleanup is fully automatic. RunT registers cleanup via t.Cleanup, and the pool is closed when the test finishes. Nothing to do.
func TestDB(t *testing.T) {
pool := dockertest.NewPoolT(t, "")
resource := pool.RunT(t, "postgres", dockertest.WithTag("14"))
// Use resource... cleanup happens automatically when t finishes.
}
NewPool + Run: Call resource.Close(ctx) to release individual containers, or pool.Close(ctx) to release everything:
ctx := context.Background()
pool, err := dockertest.NewPool(ctx, "")
if err != nil {
panic(err)
}
defer pool.Close(ctx) // releases all tracked containers and networks
resource, err := pool.Run(ctx, "postgres", dockertest.WithTag("14")) if err != nil { panic(err) } defer resource.Close(ctx) // or let pool.Close handle it
Advanced: shared pool in TestMain: Use this when you need a single pool shared across all tests in a package:
func TestMain(m *testing.M) {
ctx := context.Background()
pool, _ := dockertest.NewPool(ctx, "")
code := m.Run()
pool.Close(ctx)
os.Exit(code)
}
Error handling
resource, err := pool.Run(ctx, "postgres", dockertest.WithTag("14"))
if errors.Is(err, dockertest.ErrImagePullFailed) {
// Image could not be pulled
}
if errors.Is(err, dockertest.ErrContainerCreateFailed) {
// Container creation failed
}
if errors.Is(err, dockertest.ErrContainerStartFailed) {
// Container start failed
}
if errors.Is(err, dockertest.ErrClientClosed) {
// Pool or client has been closed
}
Examples
See the examples directory for complete examples.
Troubleshoot & FAQ
Out of disk space
Try cleaning up unused containers, images, and volumes:
docker system prune -f
Running in CI
GitHub Actions
Docker is available by default on GitHub Actions ubuntu-latest runners, so no extra services are needed:
name: Test with Docker
on: [push]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: actions/setup-go@v5 with: go-version: "1.24"
- run: go test -v ./...
GitLab CI
Shared runners
Add the Docker dind service to your job which starts in a sibling container. The database will be available on host docker. Your app should be able to change the database host through an environment variable.
stages:
- test
go-test:
stage: test
image: golang:1.24
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKERTLSCERTDIR: ""
YOURAPPDB_HOST: docker
script:
- go test ./...
In your pool.Retry callback, use $YOURAPPDB_HOST instead of localhost when connecting to the database.
Custom (group) runners
GitLab runner can be run in docker executor mode to save compatibility with shared runners:
gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token $YOUR_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:27" \
--docker-privileged
The DOCKERTLSCERTDIR: "" variable in the example above tells the Docker daemon to start on port 2375 over HTTP (TLS disabled).
