Skip to content

Using Docker Compose

Similar to generic containers support, it's also possible to run a bespoke set of services specified in a docker-compose.yml file.

This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define services that an application may be dependent upon.

Using docker compose directly

Warning

The minimal version of Go required to use this module is 1.21.

go get github.com/testcontainers/testcontainers-go/modules/compose

Because compose v2 is implemented in Go it's possible for Testcontainers for Go to use github.com/docker/compose directly and skip any process execution/docker-compose-in-a-container scenario. The ComposeStack API exposes this variant of using docker compose in an easy way.

Before using the Compose module, there is some configuration that needs to be applied first. It customizes the behaviour of the Ryuk container, which is used to clean up the resources created by the docker compose stack. Please refer to the Ryuk configuration for more information.

Usage

Use the advanced NewDockerComposeWith(...) constructor allowing you to customise the compose execution with options:

  • StackIdentifier: the identifier for the stack, which is used to name the network and containers. If not passed, a random identifier is generated.
  • WithStackFiles: specify the Docker Compose stack files to use, as a variadic argument of string paths where the stack files are located.
  • WithStackReaders: specify the Docker Compose stack files to use, as a variadic argument of io.Reader instances. It will create a temporary file in the temp dir of the given O.S., that will be removed after the Down method is called. You can use both WithComposeStackFiles and WithComposeStackReaders at the same time.
    composeContent := `services:
  nginx:
    image: nginx:stable-alpine
    environment:
      bar: ${bar}
      foo: ${foo}
    ports:
      - "8081:80"
  mysql:
    image: mysql:8.0.36
    environment:
      - MYSQL_DATABASE=db
      - MYSQL_ROOT_PASSWORD=my-secret-pw
    ports:
     - "3307:3306"
`
stack, err := compose.NewDockerComposeWith(
    compose.StackIdentifier("test"),
    compose.WithStackReaders(strings.NewReader(composeContent)),
)
if err != nil {
    log.Printf("Failed to create stack: %v", err)
    return
}
err = stack.
    WithEnv(map[string]string{
        "bar": "BAR",
        "foo": "FOO",
    }).
    WaitForService("nginx", wait.ForListeningPort("80/tcp")).
    Up(ctx, compose.Wait(true))
if err != nil {
    log.Printf("Failed to start stack: %v", err)
    return
}
defer func() {
    err = stack.Down(
        context.Background(),
        compose.RemoveOrphans(true),
        compose.RemoveVolumes(true),
        compose.RemoveImagesLocal,
    )
    if err != nil {
        log.Printf("Failed to stop stack: %v", err)
    }
}()
serviceNames := stack.Services()
nginxContainer, err := stack.ServiceContainer(context.Background(), "nginx")
if err != nil {
    log.Printf("Failed to get container: %v", err)
    return
}

Compose Up options

  • Recreate: recreate the containers. If any other value than api.RecreateNever, api.RecreateForce or api.RecreateDiverged is provided, the default value api.RecreateForce will be used.
  • RecreateDependencies: recreate dependent containers. If any other value than api.RecreateNever, api.RecreateForce or api.RecreateDiverged is provided, the default value api.RecreateForce will be used.
  • RemoveOrphans: remove orphaned containers when the stack is upped.
  • Wait: will wait until the containers reached the running|healthy state.

Compose Down options

  • RemoveImages: remove images after the stack is stopped. The RemoveImagesAll option will remove all images, while RemoveImagesLocal will remove only the images that don't have a tag.
  • RemoveOrphans: remove orphaned containers after the stack is stopped.
  • RemoveVolumes: remove volumes after the stack is stopped.

Interacting with compose services

To interact with service containers after a stack was started it is possible to get a *testcontainers.DockerContainer instance via the ServiceContainer(...) function. The function takes a service name (and a context.Context) and returns either a *testcontainers.DockerContainer or an error.

Furthermore, there's the convenience function Services() to get a list of all services defined by the current project. Note that not all of them need necessarily be correctly started as the information is based on the given compose files.

Wait strategies

Just like with the containers created by Testcontainers for Go, you can also apply wait strategies to docker compose services. The ComposeStack.WaitForService(...) function allows you to apply a wait strategy to a service by name. All wait strategies are executed in parallel to both improve startup performance by not blocking too long and to fail early if something's wrong.

Example

    composeContent := `services:
  nginx:
    image: nginx:stable-alpine
    environment:
      bar: ${bar}
      foo: ${foo}
    ports:
      - "8081:80"
`

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    stack, err := compose.NewDockerComposeWith(compose.WithStackReaders(strings.NewReader(composeContent)))
    if err != nil {
        log.Printf("Failed to create stack: %v", err)
        return
    }

    err = stack.
        WithEnv(map[string]string{
            "bar": "BAR",
        }).
        WaitForService("nginx", wait.NewHTTPStrategy("/").WithPort("80/tcp").WithStartupTimeout(10*time.Second)).
        Up(ctx, compose.Wait(true))
    if err != nil {
        log.Printf("Failed to start stack: %v", err)
        return
    }
    defer func() {
        err = stack.Down(
            context.Background(),
            compose.RemoveOrphans(true),
            compose.RemoveVolumes(true),
            compose.RemoveImagesLocal,
        )
        if err != nil {
            log.Printf("Failed to stop stack: %v", err)
        }
    }()

    serviceNames := stack.Services()

    fmt.Println(serviceNames)

    // Output:
    // [nginx]

Compose environment

docker compose supports expansion based on environment variables. The ComposeStack supports this as well in two different variants:

  • ComposeStack.WithEnv(m map[string]string) ComposeStack to parameterize stacks from your test code
  • ComposeStack.WithOsEnv() ComposeStack to parameterize tests from the OS environment e.g. in CI environments

Docs

Also have a look at ComposeStack docs for further information.

Usage of the deprecated Local docker compose binary

Warning

This API is deprecated and superseded by ComposeStack which takes advantage of compose v2 being implemented in Go as well by directly using the upstream project.

You can override Testcontainers' default behaviour and make it use a docker compose binary installed on the local machine. This will generally yield an experience that is closer to running docker compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines.

Examples

path := "/path/to/docker-compose.yml"

stack := compose.NewLocalDockerCompose([]string{path}, "my_project")

execError := stack.
    WithCommand([]string{"up", "-d"}).
    WithEnv(map[string]string{
        "bar": "BAR",
    }).
    Invoke()
if execError.Error != nil {
    _ = fmt.Errorf("Failed when running: %v", execError.Command)
}

Note that the environment variables in the env map will be applied, if possible, to the existing variables declared in the Docker Compose file.

In the following example, we demonstrate how to stop a Docker Compose created project using the convenient Down method.

path := "/path/to/docker-compose.yml"

stack := compose.NewLocalDockerCompose([]string{path}, "my_project")

execError := stack.WithCommand([]string{"up", "-d"}).Invoke()
if execError.Error != nil {
    _ = fmt.Errorf("Failed when running: %v", execError.Command)
}

execError = stack.Down()
if execError.Error != nil {
    _ = fmt.Errorf("Failed when running: %v", execError.Command)
}