Docker Compose
FluentDocker provides full support for Docker Compose V2 (docker compose command).
Step by Step
- Basics: Kernel Setup, Basic Usage, Waiting for Services
- Intermediate: Project Configuration, Multiple Compose Files, Access Containers, Environment Variables
- Advanced: Profiles, Target Specific Services, Integration Tests Example, Cleanup Options
Kernel Setup
Before using the builder, create a FluentDockerKernel. Multiple kernels per
application (or test fixture) are supported. Many apps still reuse one kernel
across builder calls for simplicity.
using FluentDocker.Kernel;
using FluentDocker.Builders;
// Create once and reuse
var kernel = FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.Build();
For async contexts (ASP.NET, xUnit IAsyncLifetime), prefer the async variant:
var kernel = await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.BuildAsync();
Basic Usage
Start Services
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml"))
.Build();
// Services are started during Build() -- no separate Start() call.
var compose = results.ComposeServices.First();
Console.WriteLine($"Project: {compose.ProjectName}");
Example docker-compose.yml
services:
web:
image: nginx:alpine
ports:
- "80"
depends_on: [api]
api:
image: myapi:latest
ports:
- "8080"
environment:
- DATABASE_URL=postgres://db:5432/mydb
depends_on: [db]
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Waiting for Services
The v3 API uses Docker Compose V2’s native --wait flag instead of per-service wait
strategies. With --wait, Compose waits for every service that has a healthcheck
defined in the compose file to report healthy before returning.
Wait for Healthy Services
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait())
.Build();
Wait with Timeout
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait()
.WithWaitTimeout(120)) // seconds
.Build();
For --wait to be effective, define healthchecks in your compose file:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
image: myapi:latest
ports:
- "8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
depends_on:
db:
condition: service_healthy
Note: If you need fine-grained wait logic (HTTP polling with custom validation, port probing, etc.) after compose services are up, you can access individual containers from
results.Containersand use the container-level wait utilities.
Project Configuration
Custom Project Name
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithProjectName("my-test-project"))
.Build();
// Containers named: my-test-project-web-1, my-test-project-api-1, etc.
Remove Orphans
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithRemoveOrphans()) // Remove containers not in compose file
.Build();
Force Recreate
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithForceRecreate()) // Recreate even if unchanged
.Build();
Multiple Compose Files
Override Files
// Base + override pattern
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFiles(
"docker-compose.yml",
"docker-compose.override.yml"))
.Build();
Environment-Specific
// Development environment
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFiles(
"docker-compose.yml",
"docker-compose.dev.yml"))
.Build();
# docker-compose.dev.yml
services:
web:
volumes:
- ./src:/app/src # Hot reload
environment:
- DEBUG=true
Access Containers
Get Specific Container
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait())
.Build();
// Find by name
var webContainer = results.Containers
.FirstOrDefault(c => c.Name.Contains("web"));
var apiContainer = results.Containers
.FirstOrDefault(c => c.Name.Contains("api"));
// Get endpoints
var webEndpoint = webContainer?.ToHostExposedEndpoint("80/tcp");
var apiEndpoint = apiContainer?.ToHostExposedEndpoint("8080/tcp");
Execute Commands
var compose = results.ComposeServices.First();
// Execute in a compose service
var output = await compose.ExecuteAsync(
"db",
new[] { "psql", "-U", "postgres", "-c",
"CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY);" });
WordPress with MySQL Example
docker-compose.yml
services:
db:
image: mariadb:10.6
environment:
MARIADB_ROOT_PASSWORD: rootpassword
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: wordpress
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
wordpress:
image: wordpress:latest
depends_on:
db:
condition: service_healthy
ports:
- "80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 15s
timeout: 5s
retries: 5
volumes:
db_data:
C# Code
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait()
.WithWaitTimeout(120))
.Build();
var wpContainer = results.Containers
.First(c => c.Name.Contains("wordpress"));
var endpoint = wpContainer.ToHostExposedEndpoint("80/tcp");
Console.WriteLine($"WordPress: http://localhost:{endpoint.Port}");
Kafka with Zookeeper Example
docker-compose.yml
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "2181"]
interval: 10s
timeout: 5s
retries: 5
kafka:
image: confluentinc/cp-kafka:7.4.0
depends_on:
zookeeper:
condition: service_healthy
ports:
- "9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
healthcheck:
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"]
interval: 15s
timeout: 10s
retries: 5
C# Code
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait()
.WithWaitTimeout(90))
.Build();
var kafkaContainer = results.Containers
.First(c => c.Name.Contains("kafka"));
var endpoint = kafkaContainer.ToHostExposedEndpoint("9092/tcp");
var bootstrapServers = $"localhost:{endpoint.Port}";
Console.WriteLine($"Kafka: {bootstrapServers}");
RabbitMQ Example
docker-compose.yml
services:
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672" # AMQP
- "15672" # Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"]
interval: 10s
timeout: 5s
retries: 5
C# Code
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithWait()
.WithWaitTimeout(60))
.Build();
var rmq = results.Containers.First(c => c.Name.Contains("rabbitmq"));
var amqp = rmq.ToHostExposedEndpoint("5672/tcp");
var mgmt = rmq.ToHostExposedEndpoint("15672/tcp");
Console.WriteLine($"AMQP: amqp://guest:guest@localhost:{amqp.Port}");
Console.WriteLine($"Management: http://localhost:{mgmt.Port}");
Build Services
Build Images
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithBuild()) // Build images before starting
.Build();
Note: For a no-cache rebuild, run
docker compose build --no-cacheseparately before the builder call, or combine.WithBuild()with.WithForceRecreate().
Environment Variables
Inline Environment
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithEnvironment("DB_PASSWORD", "secret")
.WithEnvironment("API_KEY", "abc123"))
.Build();
Bulk Environment
var env = new Dictionary<string, string>
{
["DB_PASSWORD"] = "secret",
["API_KEY"] = "abc123"
};
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithEnvironment(env))
.Build();
With .env File
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithEnvFile(".env"))
.Build();
# docker-compose.yml
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
# .env file in same directory
DB_PASSWORD=mysecret
Scaling Services
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithScale("worker", 3)) // Run 3 worker instances
.Build();
var workers = results.Containers
.Where(c => c.Name.Contains("worker"))
.ToList();
Console.WriteLine($"Workers: {workers.Count}"); // 3
Cleanup Options
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithRemoveVolumes() // Remove volumes when disposed
.WithRemoveImages()) // Remove images when disposed
.Build();
By default, compose services are torn down on dispose. Use .WithRemoveVolumes()
and .WithRemoveImages() to also remove volumes and images during teardown.
Additional Builder Methods
The compose builder also supports these options:
WithTimeout(int seconds)– sets a timeout (in seconds) for the compose operation.WithNoStart(bool)– runsdocker compose upwithout starting services (create only).WithPull(bool)– pulls images before starting services.
Profiles
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.WithProfiles("debug", "monitoring"))
.Build();
Target Specific Services
using var results = new Builder()
.WithinDriver("docker", kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.yml")
.ForServices("web", "api") // Only start web and api
.WithNoDeps()) // Skip their dependencies
.Build();
Integration Tests Example
public class IntegrationTestBase : IAsyncLifetime
{
private FluentDockerKernel _kernel;
protected BuildResults Results { get; private set; }
protected string ApiBaseUrl { get; private set; }
public async ValueTask InitializeAsync()
{
_kernel = await FluentDockerKernel.Create()
.WithDockerCli("docker", d => d.AsDefault())
.BuildAsync();
Results = await new Builder()
.WithinDriver("docker", _kernel)
.UseCompose(c => c
.WithComposeFile("docker-compose.test.yml")
.WithRemoveOrphans()
.WithWait()
.WithWaitTimeout(60))
.BuildAsync();
var apiContainer = Results.Containers
.First(c => c.Name.Contains("api"));
var endpoint = apiContainer.ToHostExposedEndpoint("8080/tcp");
ApiBaseUrl = $"http://localhost:{endpoint.Port}";
}
public async ValueTask DisposeAsync()
{
if (Results is IAsyncDisposable ad) await ad.DisposeAsync();
if (_kernel is IAsyncDisposable kd) await kd.DisposeAsync();
}
}
public class UserApiTests : IntegrationTestBase
{
[Fact]
public async Task CreateUser_ReturnsCreated()
{
var client = new HttpClient { BaseAddress = new Uri(ApiBaseUrl) };
var response = await client.PostAsJsonAsync("/users", new { name = "Test" });
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
Next Steps
- Containers - Individual container management
- Networking - Custom networks
- Volumes - Data persistence