Skip to content

Commit de3d2d3

Browse files
Improve Docker Compose docs (#9461)
Co-authored-by: Eddú Meléndez Gonzales <[email protected]>
1 parent 87bf5bf commit de3d2d3

File tree

5 files changed

+140
-113
lines changed

5 files changed

+140
-113
lines changed

core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ public void setUp() {
3737
@Test
3838
public void testProfileOption() {
3939
try (
40+
// composeContainerWithLocalCompose {
4041
ComposeContainer compose = new ComposeContainer(COMPOSE_FILE)
41-
.withOptions("--profile=cache")
4242
.withLocalCompose(true)
43+
// }
44+
.withOptions("--profile=cache")
4345
) {
4446
compose.start();
4547
assertThat(compose.listChildContainers()).hasSize(1);

core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,30 @@
1818
public class ComposeContainerTest extends BaseComposeTest {
1919

2020
@Rule
21+
// composeContainerConstructor {
2122
public ComposeContainer environment = new ComposeContainer(
2223
new File("src/test/resources/composev2/compose-test.yml")
2324
)
2425
.withExposedService("redis-1", REDIS_PORT)
2526
.withExposedService("db-1", 3306);
2627

28+
// }
29+
2730
@Override
2831
protected ComposeContainer getEnvironment() {
2932
return environment;
3033
}
3134

3235
@Test
33-
public void testGetServicePort() {
36+
public void testGetServiceHostAndPort() {
37+
// getServiceHostAndPort {
38+
String serviceHost = environment.getServiceHost("redis-1", REDIS_PORT);
3439
int serviceWithInstancePort = environment.getServicePort("redis-1", REDIS_PORT);
40+
// }
41+
42+
assertThat(serviceHost).as("Service host is not blank").isNotBlank();
3543
assertThat(serviceWithInstancePort).as("Port is set for service with instance number").isNotNull();
44+
3645
int serviceWithoutInstancePort = environment.getServicePort("redis", REDIS_PORT);
3746
assertThat(serviceWithoutInstancePort).as("Port is set for service with instance number").isNotNull();
3847
assertThat(serviceWithoutInstancePort).as("Service ports are the same").isEqualTo(serviceWithInstancePort);

core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ public void testWithFileCopyInclusionUsingFilePath() throws IOException {
4949
@Test
5050
public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException {
5151
try (
52+
// composeContainerWithCopyFiles {
5253
ComposeContainer environment = new ComposeContainer(
5354
new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml")
5455
)
5556
.withExposedService("app", 8080)
5657
.withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test")
58+
// }
5759
) {
5860
environment.start();
5961

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.testcontainers.junit;
2+
3+
import org.junit.Test;
4+
import org.testcontainers.containers.ComposeContainer;
5+
import org.testcontainers.containers.wait.strategy.Wait;
6+
7+
import java.io.File;
8+
import java.time.Duration;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
public class ComposeContainerWithWaitStrategies {
13+
14+
private static final int REDIS_PORT = 6379;
15+
16+
@Test
17+
public void testComposeContainerConstructor() {
18+
try (
19+
// composeContainerWithCombinedWaitStrategies {
20+
ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml"))
21+
.withExposedService("redis-1", REDIS_PORT, Wait.forSuccessfulCommand("redis-cli ping"))
22+
.withExposedService("db-1", 3306, Wait.forLogMessage(".*ready for connections.*\\n", 1))
23+
// }
24+
) {
25+
compose.start();
26+
containsStartedServices(compose, "redis-1", "db-1");
27+
}
28+
}
29+
30+
@Test
31+
public void testComposeContainerWaitForPortWithTimeout() {
32+
try (
33+
// composeContainerWaitForPortWithTimeout {
34+
ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml"))
35+
.withExposedService(
36+
"redis-1",
37+
REDIS_PORT,
38+
Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30))
39+
)
40+
// }
41+
) {
42+
compose.start();
43+
containsStartedServices(compose, "redis-1");
44+
}
45+
}
46+
47+
private void containsStartedServices(ComposeContainer compose, String... expectedServices) {
48+
for (String serviceName : expectedServices) {
49+
assertThat(compose.getContainerByServiceName(serviceName))
50+
.as("Container should be found by service name %s", serviceName)
51+
.isPresent();
52+
}
53+
}
54+
}

docs/modules/docker_compose.md

Lines changed: 71 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,161 +2,121 @@
22

33
## Benefits
44

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

8-
This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define
9-
services that an application may be dependent upon.
8+
This is especially useful for projects where Docker Compose is already used in development
9+
or other environments to define services that an application may be dependent upon.
1010

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

1414
## Example
1515

16-
A single class rule, pointing to a `docker-compose.yml` file, should be sufficient to launch any number of services
17-
required by your tests:
18-
```java
19-
@ClassRule
20-
public static DockerComposeContainer environment =
21-
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
22-
.withExposedService("redis_1", REDIS_PORT)
23-
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT);
24-
```
16+
A single class `ComposeContainer`, defined based on a `docker-compose.yml` file,
17+
should be sufficient to launch any number of services required by our tests:
18+
19+
<!--codeinclude-->
20+
[Create a ComposeContainer](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:composeContainerConstructor
21+
<!--/codeinclude-->
2522

26-
In this example, `compose-test.yml` should have content such as:
23+
!!! note
24+
Make sure the service names use a `-` rather than `_` as separator.
25+
26+
In this example, Docker Compose file should have content such as:
2727
```yaml
28-
redis:
29-
image: redis
30-
elasticsearch:
31-
image: elasticsearch
28+
services:
29+
redis:
30+
image: redis
31+
db:
32+
image: mysql:8.0.36
3233
```
3334
34-
Note that it is not necessary to define ports to be exposed in the YAML file; this would inhibit reuse/inclusion of the
35-
file in other contexts.
35+
Note that it is not necessary to define ports to be exposed in the YAML file,
36+
as this would inhibit the reuse/inclusion of the file in other contexts.
37+
38+
Instead, Testcontainers will spin up a small `ambassador` container,
39+
which will proxy between the Compose-managed containers and ports that are accessible to our tests.
3640

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

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

43-
The rule provides methods for discovering how your tests can interact with the containers:
45+
On the other hand, `DockerComposeContainer` utilizes Compose V1, which has been marked deprecated by Docker.
46+
47+
The two APIs are quite similar, and most examples provided on this page can be applied to both of them.
48+
49+
## Accessing a Container
50+
51+
`ComposeContainer` provides methods for discovering how your tests can interact with the containers:
4452

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

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

57-
## Startup timeout
63+
## Wait Strategies and Startup Timeouts
5864
Ordinarily Testcontainers will wait for up to 60 seconds for each exposed container's first mapped network port to start listening.
59-
6065
This simple measure provides a basic check whether a container is ready for use.
6166

62-
There are overloaded `withExposedService` methods that take a `WaitStrategy` so you can specify a timeout strategy per container.
67+
There are overloaded `withExposedService` methods that take a `WaitStrategy`
68+
where we can specify a timeout strategy per container.
6369

64-
### Waiting for startup examples
70+
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,
71+
accessible via the static factory methods from of the `Wait` class.
6572

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

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

87-
Separate wait strategies for each container:
88-
```java
89-
@ClassRule
90-
public static DockerComposeContainer environment =
91-
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
92-
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
93-
.withExposedService("elasticsearch_1", ELASTICSEARCH_PORT,
94-
Wait.forHttp("/all")
95-
.forStatusCode(200)
96-
.forStatusCode(401)
97-
.usingTls());
98-
```
80+
For example, our Redis container can wait for a successful redis-cli command,
81+
while our db service waits for a specific log message:
9982

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

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

111-
## 'Local compose' mode
11288

113-
You can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine.
114-
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.
115-
```java
116-
public static DockerComposeContainer environment =
117-
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
118-
.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
119-
.waitingFor("db_1", Wait.forLogMessage("started", 1))
120-
.withLocalCompose(true);
121-
```
89+
## The 'Local Compose' Mode
12290

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

125-
[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`.
126-
Testcontainers provides `ComposeContainer` if you want to use Compose V2.
93+
This will generally yield an experience that is closer to running _docker compose_ locally,
94+
with the caveat that Docker Compose needs to be present on dev and CI machines.
12795

128-
```java
129-
public static ComposeContainer environment =
130-
new ComposeContainer(new File("src/test/resources/compose-test.yml"))
131-
.withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort())
132-
.waitingFor("db-1", Wait.forLogMessage("started", 1));
133-
```
134-
135-
!!! note
136-
Make sure the service name use a `-` instead of `_` as separator using `ComposeContainer`.
96+
<!--codeinclude-->
97+
[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java) inside_block:composeContainerWithLocalCompose
98+
<!--/codeinclude-->
13799

138-
## Build working directory
100+
## Build Working Directory
139101

140-
You can select what files should be copied only via `withCopyFilesInContainer`:
102+
We can select what files should be copied only via `withCopyFilesInContainer`:
141103

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

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

151-
This can be used with `DockerComposeContainer` and `ComposeContainer`.
152-
You can use file and directory references.
111+
We can use file and directory references.
153112
They are always resolved relative to the directory where the compose file resides.
154113

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

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

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

0 commit comments

Comments
 (0)