Skip to content

Commit 1914893

Browse files
DDtKeyclaude
andcommitted
docs: improve docker-compose documentation
Updates docker_compose.md with: - Async-only requirement prominently noted at top - Minimal example section (like Java docs) - Installation/requirements section - Better service access examples showing full RawContainer API - Lifecycle section with explicit down() vs auto-drop - Build and pull configuration examples - Clearer structure aligned with Java testcontainers docs Key additions: - Minimal redis example at the start - Note that compose is tokio-only (no blocking support yet) - Examples showing service() returns full container with exec/logs - Explicit down() consuming self explained 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3d49cbf commit 1914893

File tree

2 files changed

+135
-49
lines changed

2 files changed

+135
-49
lines changed

docs/features/docker_compose.md

Lines changed: 126 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,49 @@
22

33
Testcontainers for Rust supports running multi-container applications defined in Docker Compose files. This is useful when your tests need multiple interconnected services or when you want to reuse existing docker-compose configurations from your development environment.
44

5+
> **Note:** Docker Compose support is currently only available for async runtimes (tokio). Synchronous/blocking support may be added in a future release.
6+
7+
## Installation
8+
9+
Add the `docker-compose` feature to your dependencies:
10+
11+
```toml
12+
[dev-dependencies]
13+
testcontainers = { version = "0.25", features = ["docker-compose"] }
14+
```
15+
16+
## Minimal Example
17+
18+
```rust
19+
use testcontainers::compose::DockerCompose;
20+
21+
#[tokio::test]
22+
async fn test_redis() -> Result<(), Box<dyn std::error::Error>> {
23+
let mut compose = DockerCompose::with_local_client(&["tests/docker-compose.yml"]);
24+
compose.up().await?;
25+
26+
let redis = compose.service("redis").expect("redis service");
27+
let port = redis.get_host_port_ipv4(6379).await?;
28+
29+
// Use redis at localhost:{port}
30+
let client = redis::Client::open(format!("redis://localhost:{}", port))?;
31+
let mut con = client.get_connection()?;
32+
redis::cmd("PING").query::<String>(&mut con)?;
33+
34+
Ok(())
35+
}
36+
```
37+
38+
With `docker-compose.yml`:
39+
40+
```yaml
41+
services:
42+
redis:
43+
image: redis:7-alpine
44+
ports:
45+
- "6379"
46+
```
47+
548
## Basic Usage
649
750
Use [`DockerCompose`](https://docs.rs/testcontainers/latest/testcontainers/compose/struct.DockerCompose.html) to start services defined in your compose files:
@@ -15,10 +58,11 @@ async fn test_with_compose() -> Result<(), Box<dyn std::error::Error>> {
1558
1659
compose.up().await?;
1760
18-
// Services are now running and accessible
19-
let web_port = compose.get_host_port_ipv4("web", 8080).await?;
20-
let response = reqwest::get(format!("http://localhost:{}", web_port)).await?;
61+
// Access service by name
62+
let web = compose.service("web").expect("web service");
63+
let port = web.get_host_port_ipv4(8080).await?;
2164
65+
let response = reqwest::get(format!("http://localhost:{}", port)).await?;
2266
assert!(response.status().is_success());
2367
2468
Ok(())
@@ -28,16 +72,19 @@ async fn test_with_compose() -> Result<(), Box<dyn std::error::Error>> {
2872

2973
## Accessing Services
3074

31-
After calling `up()`, you can access individual services by name:
75+
After calling `up()`, you can access individual services by name. The `service()` method returns a reference to the container, providing full access to the container API:
3276

33-
### Get Service Container ID
77+
### Get Service Container
3478

3579
```rust
3680
compose.up().await?;
3781
38-
if let Some(container_id) = compose.service("redis") {
39-
println!("Redis container ID: {}", container_id);
40-
}
82+
let redis = compose.service("redis").expect("redis service exists");
83+
84+
// Full container API available
85+
let port = redis.get_host_port_ipv4(6379).await?;
86+
let logs = redis.stdout(true);
87+
redis.exec(ExecCommand::new(["redis-cli", "PING"])).await?;
4188
```
4289

4390
### List All Services
@@ -50,19 +97,29 @@ for service_name in compose.services() {
5097
}
5198
```
5299

53-
### Get Mapped Ports
100+
### Access Ports, Logs, and Execute Commands
54101

55-
Access the host ports mapped to your services:
102+
Services return a container reference with the full API:
56103

57104
```rust
58105
compose.up().await?;
59106
60-
// IPv4
61-
let redis_port = compose.get_host_port_ipv4("redis", 6379).await?;
62-
let client = redis::Client::open(format!("redis://localhost:{}", redis_port))?;
107+
let redis = compose.service("redis").expect("redis service");
108+
109+
// Get mapped ports
110+
let port = redis.get_host_port_ipv4(6379).await?;
111+
let ipv6_port = redis.get_host_port_ipv6(6379).await?;
112+
113+
// Stream logs
114+
let stdout = redis.stdout(true);
115+
let stderr = redis.stderr(false);
63116
64-
// IPv6
65-
let ipv6_port = compose.get_host_port_ipv6("web", 8080).await?;
117+
// Execute commands
118+
let result = redis.exec(ExecCommand::new(["redis-cli", "PING"])).await?;
119+
120+
// Get container info
121+
let container_id = redis.id();
122+
let host = redis.get_host().await?;
66123
```
67124

68125
## Client Modes
@@ -123,9 +180,26 @@ let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"])
123180
compose.up().await?;
124181
```
125182

126-
### Cleanup Options
183+
### Lifecycle and Cleanup
184+
185+
You can either let the stack automatically clean up on drop, or explicitly tear it down:
186+
187+
```rust
188+
// Option 1: Automatic cleanup (default behavior)
189+
{
190+
let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"]);
191+
compose.up().await?;
192+
// Use services...
193+
} // Automatically cleaned up here
194+
195+
// Option 2: Explicit teardown
196+
let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"]);
197+
compose.up().await?;
198+
// Use services...
199+
compose.down().await?; // Explicit cleanup, consumes compose
200+
```
127201

128-
Control what gets removed when the compose stack is dropped:
202+
Control what gets removed during cleanup:
129203

130204
```rust
131205
let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"]);
@@ -139,6 +213,18 @@ compose.with_remove_images(false);
139213
compose.up().await?;
140214
```
141215

216+
### Build and Pull Options
217+
218+
Configure whether to build or pull images before starting:
219+
220+
```rust
221+
let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"])
222+
.with_build(true) // Build images defined in compose file
223+
.with_pull(true); // Pull latest images before starting
224+
225+
compose.up().await?;
226+
```
227+
142228
## Multiple Compose Files
143229

144230
You can use multiple compose files (e.g., base + override):
@@ -298,24 +384,34 @@ If `up()` returns an error:
298384

299385
## Limitations
300386

301-
### Wait Strategies
387+
### Synchronous API
302388

303-
Custom per-service wait strategies (beyond compose's `--wait`) are not yet implemented. For now, use compose file healthchecks or add explicit waits:
304-
305-
```rust
306-
compose.up().await?;
307-
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
308-
```
389+
Docker Compose support is currently only available for async runtimes (tokio). If you need synchronous/blocking support, please open an issue on GitHub.
309390

310391
### Containerised Client Environment Variables
311392

312393
Environment variable support for the containerised client is not yet implemented. Use the local client if you need env vars.
313394

314-
## Feature Flag
395+
## Requirements
315396

316-
Docker Compose support requires the `docker-compose` feature:
397+
Docker Compose support requires:
317398

318-
```toml
319-
[dependencies]
320-
testcontainers = { version = "0.25", features = ["docker-compose"] }
321-
```
399+
1. **Feature flag:**
400+
```toml
401+
[dev-dependencies]
402+
testcontainers = { version = "0.25", features = ["docker-compose"] }
403+
```
404+
405+
2. **Async runtime:** Currently only tokio is supported
406+
```toml
407+
[dev-dependencies]
408+
tokio = { version = "1", features = ["macros"] }
409+
```
410+
411+
3. **Local Docker Compose CLI** (for local client mode):
412+
```bash
413+
docker compose version
414+
# Docker Compose version v2.20.0 or later
415+
```
416+
417+
Or use containerised client mode (no local installation needed)

testcontainers/src/compose/mod.rs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use std::{collections::HashMap, path::Path, sync::Arc};
33
use crate::{
44
compose::client::ComposeInterface,
55
core::{
6-
async_container::raw::RawContainer, async_drop, client::Client, wait::WaitStrategy,
7-
WaitFor,
6+
async_container::raw::RawContainer, async_drop, client::Client, wait::WaitStrategy, WaitFor,
87
},
98
};
109

@@ -175,7 +174,7 @@ impl DockerCompose {
175174
self
176175
}
177176

178-
/// Remove volumes when dropping the docker compose or not
177+
/// Remove volumes when dropping the docker compose or not (removed by default)
179178
pub fn with_remove_volumes(&mut self, remove_volumes: bool) -> &mut Self {
180179
self.remove_volumes = remove_volumes;
181180
self
@@ -241,20 +240,6 @@ mod tests {
241240

242241
use super::*;
243242

244-
// #[tokio::test]
245-
// async fn test_containerised_docker_compose() {
246-
// let path_to_compose = PathBuf::from(format!(
247-
// "{}/tests/test-compose.yml",
248-
// env!("CARGO_MANIFEST_DIR")
249-
// ));
250-
// let docker_compose =
251-
// DockerCompose::with_containerised_client(&[path_to_compose.as_path()]).await;
252-
// docker_compose.up().await;
253-
// tokio::time::sleep(std::time::Duration::from_secs(1)).await;
254-
// let res = reqwest::get("http://localhost:8081/").await.unwrap();
255-
// assert!(res.status().is_success());
256-
// }
257-
258243
#[tokio::test]
259244
async fn test_local_docker_compose() -> anyhow::Result<()> {
260245
let _ = pretty_env_logger::try_init();
@@ -265,13 +250,18 @@ mod tests {
265250
));
266251

267252
let mut compose = DockerCompose::with_local_client(&[path_to_compose.as_path()])
268-
.with_wait_for_service("redis", WaitFor::message_on_stdout("Ready to accept connections"));
253+
.with_wait_for_service(
254+
"redis",
255+
WaitFor::message_on_stdout("Ready to accept connections"),
256+
);
269257

270258
compose.up().await?;
271259

272260
println!("Services: {:?}", compose.services());
273261

274-
let _redis = compose.service("redis").expect("Redis service should exist");
262+
let _redis = compose
263+
.service("redis")
264+
.expect("Redis service should exist");
275265
let web = compose.service("web1").expect("Web service should exist");
276266

277267
let web_port = web.get_host_port_ipv4(80).await?;

0 commit comments

Comments
 (0)