Skip to content

Commit

Permalink
Improve Docker Compose docs (#9461)
Browse files Browse the repository at this point in the history
Co-authored-by: Eddú Meléndez Gonzales <[email protected]>
  • Loading branch information
etrandafir93 and eddumelendez authored Nov 26, 2024
1 parent 87bf5bf commit de3d2d3
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public void setUp() {
@Test
public void testProfileOption() {
try (
// composeContainerWithLocalCompose {
ComposeContainer compose = new ComposeContainer(COMPOSE_FILE)
.withOptions("--profile=cache")
.withLocalCompose(true)
// }
.withOptions("--profile=cache")
) {
compose.start();
assertThat(compose.listChildContainers()).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,30 @@
public class ComposeContainerTest extends BaseComposeTest {

@Rule
// composeContainerConstructor {
public ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/composev2/compose-test.yml")
)
.withExposedService("redis-1", REDIS_PORT)
.withExposedService("db-1", 3306);

// }

@Override
protected ComposeContainer getEnvironment() {
return environment;
}

@Test
public void testGetServicePort() {
public void testGetServiceHostAndPort() {
// getServiceHostAndPort {
String serviceHost = environment.getServiceHost("redis-1", REDIS_PORT);
int serviceWithInstancePort = environment.getServicePort("redis-1", REDIS_PORT);
// }

assertThat(serviceHost).as("Service host is not blank").isNotBlank();
assertThat(serviceWithInstancePort).as("Port is set for service with instance number").isNotNull();

int serviceWithoutInstancePort = environment.getServicePort("redis", REDIS_PORT);
assertThat(serviceWithoutInstancePort).as("Port is set for service with instance number").isNotNull();
assertThat(serviceWithoutInstancePort).as("Service ports are the same").isEqualTo(serviceWithInstancePort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ public void testWithFileCopyInclusionUsingFilePath() throws IOException {
@Test
public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException {
try (
// composeContainerWithCopyFiles {
ComposeContainer environment = new ComposeContainer(
new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml")
)
.withExposedService("app", 8080)
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test")
// }
) {
environment.start();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.testcontainers.junit;

import org.junit.Test;
import org.testcontainers.containers.ComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import java.io.File;
import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

public class ComposeContainerWithWaitStrategies {

private static final int REDIS_PORT = 6379;

@Test
public void testComposeContainerConstructor() {
try (
// composeContainerWithCombinedWaitStrategies {
ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml"))
.withExposedService("redis-1", REDIS_PORT, Wait.forSuccessfulCommand("redis-cli ping"))
.withExposedService("db-1", 3306, Wait.forLogMessage(".*ready for connections.*\\n", 1))
// }
) {
compose.start();
containsStartedServices(compose, "redis-1", "db-1");
}
}

@Test
public void testComposeContainerWaitForPortWithTimeout() {
try (
// composeContainerWaitForPortWithTimeout {
ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml"))
.withExposedService(
"redis-1",
REDIS_PORT,
Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30))
)
// }
) {
compose.start();
containsStartedServices(compose, "redis-1");
}
}

private void containsStartedServices(ComposeContainer compose, String... expectedServices) {
for (String serviceName : expectedServices) {
assertThat(compose.getContainerByServiceName(serviceName))
.as("Container should be found by service name %s", serviceName)
.isPresent();
}
}
}
182 changes: 71 additions & 111 deletions docs/modules/docker_compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,161 +2,121 @@

## Benefits

Similar to generic containers support, it's also possible to run a bespoke set of services
specified in a `docker-compose.yml` file.
Similar to generic container 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.
This is especially useful for projects where Docker Compose is already used in development
or other environments to define services that an application may be dependent upon.

Behind the scenes, Testcontainers actually launches a temporary Docker Compose client - in a container, of course, so
it's not necessary to have it installed on all developer/test machines.
The `ComposeContainer` leverages [Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/),
making it easy to use the same dependencies from the development environment within tests.

## Example

A single class rule, pointing to a `docker-compose.yml` file, should be sufficient to launch any number of services
required by your tests:
```java
@ClassRule
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT)
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT);
```
A single class `ComposeContainer`, defined based on a `docker-compose.yml` file,
should be sufficient to launch any number of services required by our tests:

<!--codeinclude-->
[Create a ComposeContainer](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:composeContainerConstructor
<!--/codeinclude-->

In this example, `compose-test.yml` should have content such as:
!!! note
Make sure the service names use a `-` rather than `_` as separator.

In this example, Docker Compose file should have content such as:
```yaml
redis:
image: redis
elasticsearch:
image: elasticsearch
services:
redis:
image: redis
db:
image: mysql:8.0.36
```
Note that it is not necessary to define ports to be exposed in the YAML file; this would inhibit reuse/inclusion of the
file in other contexts.
Note that it is not necessary to define ports to be exposed in the YAML file,
as this would inhibit the reuse/inclusion of the file in other contexts.
Instead, Testcontainers will spin up a small `ambassador` container,
which will proxy between the Compose-managed containers and ports that are accessible to our tests.

Instead, Testcontainers will spin up a small 'ambassador' container, which will proxy
between the Compose-managed containers and ports that are accessible to your tests. This is done using a separate, minimal
container that runs socat as a TCP proxy.
## ComposeContainer vs DockerComposeContainer

## Accessing a container from tests
So far, we discussed `ComposeContainer`, which supports docker compose [version 2](https://www.docker.com/blog/announcing-compose-v2-general-availability/).

The rule provides methods for discovering how your tests can interact with the containers:
On the other hand, `DockerComposeContainer` utilizes Compose V1, which has been marked deprecated by Docker.

The two APIs are quite similar, and most examples provided on this page can be applied to both of them.

## Accessing a Container

`ComposeContainer` provides methods for discovering how your tests can interact with the containers:

* `getServiceHost(serviceName, servicePort)` returns the IP address where the container is listening (via an ambassador
container)
* `getServicePort(serviceName, servicePort)` returns the Docker mapped port for a port that has been exposed (via an
ambassador container)

For example, with the Redis example above, the following will allow your tests to access the Redis service:
```java
String redisUrl = environment.getServiceHost("redis_1", REDIS_PORT)
+ ":" +
environment.getServicePort("redis_1", REDIS_PORT);
```
Let's use this API to create the URL that will enable our tests to access the Redis service:
<!--codeinclude-->
[Access a Service's host and port](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:getServiceHostAndPort
<!--/codeinclude-->

## Startup timeout
## Wait Strategies and Startup Timeouts
Ordinarily Testcontainers will wait for up to 60 seconds for each exposed container's first mapped network port to start listening.

This simple measure provides a basic check whether a container is ready for use.

There are overloaded `withExposedService` methods that take a `WaitStrategy` so you can specify a timeout strategy per container.
There are overloaded `withExposedService` methods that take a `WaitStrategy`
where we can specify a timeout strategy per container.

### Waiting for startup examples
We can either use the fluent API to crate a [custom strategy](../features/startup_and_waits.md) or use one of the already existing ones,
accessible via the static factory methods from of the `Wait` class.

Waiting for exposed port to start listening:
```java
@ClassRule
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT,
Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));
```
For instance, we can wait for exposed port and set a custom timeout:
<!--codeinclude-->
[Wait for the exposed port and use a custom timeout](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWaitForPortWithTimeout
<!--/codeinclude-->

Wait for arbitrary status codes on an HTTPS endpoint:
```java
@ClassRule
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT,
Wait.forHttp("/all")
.forStatusCode(200)
.forStatusCode(401)
.usingTls());
```
Needless to say, we can define different strategies for each service in our Docker Compose setup.

Separate wait strategies for each container:
```java
@ClassRule
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT,
Wait.forHttp("/all")
.forStatusCode(200)
.forStatusCode(401)
.usingTls());
```
For example, our Redis container can wait for a successful redis-cli command,
while our db service waits for a specific log message:

Alternatively, you can use `waitingFor(serviceName, waitStrategy)`,
for example if you need to wait on a log message from a service, but don't need to expose a port.
<!--codeinclude-->
[Wait for a custom command and a log message](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWithCombinedWaitStrategies
<!--/codeinclude-->

```java
@ClassRule
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
.waitingFor("db_1", Wait.forLogMessage("started", 1));
```

## 'Local compose' mode

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.
```java
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
.waitingFor("db_1", Wait.forLogMessage("started", 1))
.withLocalCompose(true);
```
## The 'Local Compose' Mode

## Compose V2
We can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine.

[Compose V2 is GA](https://www.docker.com/blog/announcing-compose-v2-general-availability/) and it relies on the `docker` command itself instead of `docker-compose`.
Testcontainers provides `ComposeContainer` if you want to use Compose V2.
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.

```java
public static ComposeContainer environment =
new ComposeContainer(new File("src/test/resources/compose-test.yml"))
.withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort())
.waitingFor("db-1", Wait.forLogMessage("started", 1));
```

!!! note
Make sure the service name use a `-` instead of `_` as separator using `ComposeContainer`.
<!--codeinclude-->
[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java) inside_block:composeContainerWithLocalCompose
<!--/codeinclude-->

## Build working directory
## Build Working Directory

You can select what files should be copied only via `withCopyFilesInContainer`:
We can select what files should be copied only via `withCopyFilesInContainer`:

```java
public static ComposeContainer environment =
new ComposeContainer(new File("compose.yml"))
.withCopyFilesInContainer(".env");
```
<!--codeinclude-->
[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java) inside_block:composeContainerWithCopyFiles
<!--/codeinclude-->

In this example, only `compose.yml` and `.env` are copied over into the container that will run the Docker Compose file.
In this example, only docker compose and env files are copied over into the container that will run the Docker Compose file.
By default, all files in the same directory as the compose file are copied over.

This can be used with `DockerComposeContainer` and `ComposeContainer`.
You can use file and directory references.
We can use file and directory references.
They are always resolved relative to the directory where the compose file resides.

!!! note
This only work with containarized Compose, not with `Local Compose` mode.
This can be used with `DockerComposeContainer` and `ComposeContainer`, but **only in the containerized Compose (not with `Local Compose` mode)**.

## Using private repositories in Docker compose
When Docker Compose is used in container mode (not local), it's needs to be made aware of Docker settings for private repositories.
When Docker Compose is used in container mode (not local), it needs to be made aware of Docker
settings for private repositories.
By default, those setting are located in `$HOME/.docker/config.json`.

There are 3 ways to specify location of the `config.json` for Docker Compose:
Expand Down

0 comments on commit de3d2d3

Please sign in to comment.