Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 24 additions & 136 deletions docs/features/docker_compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,43 +26,20 @@ Please refer to [the Ryuk configuration](../configuration/#customizing-ryuk-the-

### Usage

Use the convenience `NewDockerCompose(...)` constructor which creates a random identifier and takes a variable number
of stack files:

```go
package example_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
)

func TestSomething(t *testing.T) {
compose, err := tc.NewDockerCompose("testdata/docker-compose.yml")
require.NoError(t, err, "NewDockerComposeAPI()")

t.Cleanup(func() {
require.NoError(t, compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal), "compose.Down()")
})

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

require.NoError(t, compose.Up(ctx, tc.Wait(true)), "compose.Up()")

// do some testing here
}
```

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.

<!--codeinclude-->
[Define Compose File](../../modules/compose/compose_examples_test.go) inside_block:defineComposeFile
[Define Options](../../modules/compose/compose_examples_test.go) inside_block:defineStackWithOptions
[Start Compose Stack](../../modules/compose/compose_examples_test.go) inside_block:upComposeStack
[Get Service Names](../../modules/compose/compose_examples_test.go) inside_block:getServiceNames
[Get Service Container](../../modules/compose/compose_examples_test.go) inside_block:getServiceContainer
<!--/codeinclude-->

#### 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.
Expand All @@ -76,91 +53,26 @@ Use the advanced `NewDockerComposeWith(...)` constructor allowing you to customi
- `RemoveOrphans`: remove orphaned containers after the stack is stopped.
- `RemoveVolumes`: remove volumes after the stack is stopped.

#### Example

```go
package example_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/docker/compose/v2/pkg/api"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
)

func TestSomethingElse(t *testing.T) {
identifier := tc.StackIdentifier("some_ident")
compose, err := tc.NewDockerComposeWith(tc.WithStackFiles("./testdata/docker-compose-simple.yml"), identifier)
require.NoError(t, err, "NewDockerComposeAPIWith()")

t.Cleanup(func() {
require.NoError(t, compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal), "compose.Down()")
})

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

require.NoError(t, compose.Up(ctx, tc.WithRecreate(api.RecreateNever), tc.Wait(true)), "compose.Up()")

// do some testing here
}
```

### Interacting with compose services

To interact with service containers after a stack was started it is possible to get an `*tc.DockerContainer` instance via the `ServiceContainer(...)` function.
The function takes a **service name** (and a `context.Context`) and returns either a `*tc.DockerContainer` or an `error`.
This is different to the previous `LocalDockerCompose` API where service containers were accessed via their **container name** e.g. `mysql_1` or `mysql-1` (depending on the version of `docker compose`).
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 regular test containers you can also apply wait strategies to `docker compose` services.
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

```go
package example_test

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"
)

func TestSomethingWithWaiting(t *testing.T) {
identifier := tc.StackIdentifier("some_ident")
compose, err := tc.NewDockerComposeWith(tc.WithStackFiles("./testdata/docker-compose-simple.yml"), identifier)
require.NoError(t, err, "NewDockerComposeAPIWith()")

t.Cleanup(func() {
require.NoError(t, compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal), "compose.Down()")
})

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

err = compose.
WaitForService("nginx", wait.NewHTTPStrategy("/").WithPort("80/tcp").WithStartupTimeout(10*time.Second)).
Up(ctx, tc.Wait(true))

require.NoError(t, err, "compose.Up()")

// do some testing here
}
```
<!--codeinclude-->
[Compose Example](../../modules/compose/compose_examples_test.go) inside_block:ExampleNewDockerComposeWith_waitForService
<!--/codeinclude-->

### Compose environment

Expand All @@ -175,10 +87,11 @@ The `ComposeStack` supports this as well in two different variants:
Also have a look at [ComposeStack](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#ComposeStack) docs for
further information.

## Usage of the `docker compose` binary
## Usage of the deprecated Local `docker compose` binary

_Note:_ 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.
!!! 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
Expand All @@ -187,41 +100,16 @@ that Docker Compose needs to be present on dev and CI machines.

### Examples

```go
composeFilePaths := []string {"testdata/docker-compose.yml"}
identifier := strings.ToLower(uuid.New().String())

compose := tc.NewLocalDockerCompose(composeFilePaths, identifier)
execError := compose.
WithCommand([]string{"up", "-d"}).
WithEnv(map[string]string {
"key1": "value1",
"key2": "value2",
}).
Invoke()

err := execError.Error
if err != nil {
return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err)
}
return nil
```
<!--codeinclude-->
[Invoke Example](../../modules/compose/compose_local_examples_test.go) inside_block:ExampleLocalDockerCompose_Invoke
<!--/codeinclude-->

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.

```go
composeFilePaths := []string{"testdata/docker-compose.yml"}

compose := tc.NewLocalDockerCompose(composeFilePaths, identifierFromExistingRunningCompose)
execError := compose.Down()
err := execError.Error
if err != nil {
return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err)
}
return nil
```

<!--codeinclude-->
[Down Example](../../modules/compose/compose_local_examples_test.go) inside_block:ExampleLocalDockerCompose_Down
<!--/codeinclude-->
164 changes: 164 additions & 0 deletions modules/compose/compose_examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package compose_test

import (
"context"
"fmt"
"log"
"slices"
"strings"
"time"

"github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"
)

func ExampleNewDockerComposeWith() {
// defineComposeFile {
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"
`
// }

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

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

// upComposeStack {
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)
}
}()
// }

// getServiceNames {
serviceNames := stack.Services()
// }

// both services are started
fmt.Println(len(serviceNames))
fmt.Println(slices.Contains(serviceNames, "nginx"))
fmt.Println(slices.Contains(serviceNames, "mysql"))

// nginx container is started
// getServiceContainer {
nginxContainer, err := stack.ServiceContainer(context.Background(), "nginx")
if err != nil {
log.Printf("Failed to get container: %v", err)
return
}
// }

inspect, err := nginxContainer.Inspect(context.Background())
if err != nil {
log.Printf("Failed to inspect container: %v", err)
return
}

// the nginx container has the correct environment variables
present := map[string]string{
"bar": "BAR",
"foo": "FOO",
}
for k, v := range present {
keyVal := k + "=" + v
fmt.Println(slices.Contains(inspect.Config.Env, keyVal))
}

// Output:
// 2
// true
// true
// true
// true
}

func ExampleNewDockerComposeWith_waitForService() {
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]
}
Loading
Loading