Compose DSL¶
The Compose DSL provides a type-safe way to define Docker Compose configurations directly in Kotlin or Java code. This is particularly powerful for integration testing, where you can define your test infrastructure alongside your test code.
Quick Start¶
Service Configuration¶
Basic Service¶
Environment Variables¶
Volumes¶
Networks¶
Dependencies¶
Healthcheck¶
service("api") {
image = "myapp/api:latest"
healthcheck {
testShell("curl -f http://localhost:3000/health || exit 1")
interval = "30s"
timeout = "10s"
retries = 3
startPeriod = "5s"
}
}
// Or with CMD form
service("db") {
image = "postgres:16"
healthcheck {
testCmd("pg_isready", "-U", "postgres")
interval = "10s"
retries = 5
}
}
Build Configuration¶
Deploy (Swarm Mode)¶
Logging¶
Running Compose Stacks¶
Generate YAML Only¶
val compose = dockerCompose {
service("redis") { image = "redis:7" }
}
// Get YAML string
val yaml = compose.toYaml()
// Write to file
compose.writeTo("docker-compose.yml")
compose.writeTo(File("docker-compose.yml"))
Run with Commands¶
val compose = dockerCompose {
service("redis") { image = "redis:7" }
}
// Start the stack
compose.upBlocking {
detach()
wait() // Wait for healthchecks
}
// Stop the stack
compose.downBlocking {
volumes() // Also remove volumes
}
Automatic Cleanup with use()¶
dockerCompose {
service("redis") { image = "redis:7-alpine" }
}.use("my-test-stack") {
// Stack is running here
// Do your testing...
}
// Stack is automatically stopped and cleaned up
Testing Examples¶
JUnit 5 Integration Test¶
import io.github.joshrotenberg.dockerkotlin.compose.dsl.*
import org.junit.jupiter.api.*
class UserServiceIntegrationTest {
companion object {
private val testStack = dockerCompose {
service("postgres") {
image = "postgres:16-alpine"
environment {
"POSTGRES_USER" to "test"
"POSTGRES_PASSWORD" to "test"
"POSTGRES_DB" to "testdb"
}
ports("5432:5432")
healthcheck {
testCmd("pg_isready", "-U", "test")
interval = "5s"
retries = 5
}
}
service("redis") {
image = "redis:7-alpine"
ports("6379:6379")
healthcheck {
testCmd("redis-cli", "ping")
interval = "5s"
retries = 3
}
}
}
@BeforeAll
@JvmStatic
fun startContainers() {
testStack.upBlocking {
detach()
wait() // Wait for healthchecks to pass
}
}
@AfterAll
@JvmStatic
fun stopContainers() {
testStack.downBlocking {
volumes()
}
}
}
@Test
fun `user can be created and retrieved`() {
// Both postgres and redis are ready
val userService = UserService(
dbUrl = "jdbc:postgresql://localhost:5432/testdb",
redisUrl = "redis://localhost:6379"
)
val user = userService.createUser("test@example.com")
assertNotNull(user.id)
}
@Test
fun `user is cached in redis`() {
// Test caching behavior
}
}
import io.github.joshrotenberg.dockerkotlin.compose.dsl.*;
import org.junit.jupiter.api.*;
class UserServiceIntegrationTest {
private static ComposeSpec testStack;
@BeforeAll
static void startContainers() {
testStack = ComposeBuilder.create()
.service("postgres", pg -> pg
.image("postgres:16-alpine")
.environment("POSTGRES_USER", "test")
.environment("POSTGRES_PASSWORD", "test")
.environment("POSTGRES_DB", "testdb")
.ports("5432:5432")
.healthcheck(hc -> hc
.testCmd("pg_isready", "-U", "test")
.interval("5s")
.retries(5)))
.service("redis", redis -> redis
.image("redis:7-alpine")
.ports("6379:6379")
.healthcheck(hc -> hc
.testCmd("redis-cli", "ping")
.interval("5s")
.retries(3)))
.build();
testStack.runner()
.projectName("user-service-test")
.up()
.detach()
.wait()
.executeBlocking();
}
@AfterAll
static void stopContainers() {
testStack.runner()
.projectName("user-service-test")
.down()
.volumes()
.executeBlocking();
}
@Test
void userCanBeCreatedAndRetrieved() {
// Test implementation
}
}
Per-Test Container Lifecycle¶
class IsolatedTest {
@Test
fun `each test gets fresh containers`() {
dockerCompose {
service("redis") {
image = "redis:7-alpine"
ports("6379:6379")
}
}.use("isolated-${UUID.randomUUID()}") {
// Fresh Redis for this test only
val redis = Jedis("localhost", 6379)
redis.set("key", "value")
assertEquals("value", redis.get("key"))
}
// Container cleaned up automatically
}
}
Reusable Test Fixtures¶
object TestContainers {
val postgres = dockerCompose {
service("postgres") {
image = "postgres:16-alpine"
environment {
"POSTGRES_USER" to "test"
"POSTGRES_PASSWORD" to "test"
"POSTGRES_DB" to "testdb"
}
ports("5432:5432")
healthcheck {
testCmd("pg_isready", "-U", "test")
interval = "5s"
retries = 5
}
}
}
val redis = dockerCompose {
service("redis") {
image = "redis:7-alpine"
ports("6379:6379")
}
}
val fullStack = dockerCompose {
service("postgres") {
image = "postgres:16-alpine"
environment("POSTGRES_PASSWORD", "test")
ports("5432:5432")
}
service("redis") {
image = "redis:7-alpine"
ports("6379:6379")
}
service("rabbitmq") {
image = "rabbitmq:3-management-alpine"
ports("5672:5672", "15672:15672")
}
}
}
// Usage in tests
class DatabaseTest {
companion object {
@BeforeAll
@JvmStatic
fun setup() {
TestContainers.postgres.upBlocking { detach(); wait() }
}
@AfterAll
@JvmStatic
fun teardown() {
TestContainers.postgres.downBlocking { volumes() }
}
}
}
Dynamic Configuration¶
fun createTestStack(
postgresVersion: String = "16",
redisVersion: String = "7",
enableMonitoring: Boolean = false
) = dockerCompose {
service("postgres") {
image = "postgres:$postgresVersion-alpine"
environment("POSTGRES_PASSWORD", "test")
ports("5432:5432")
}
service("redis") {
image = "redis:$redisVersion-alpine"
ports("6379:6379")
}
if (enableMonitoring) {
service("prometheus") {
image = "prom/prometheus:latest"
ports("9090:9090")
volumes("./prometheus.yml:/etc/prometheus/prometheus.yml:ro")
}
service("grafana") {
image = "grafana/grafana:latest"
ports("3000:3000")
dependsOn("prometheus")
}
}
}
// Usage
val stack = createTestStack(
postgresVersion = "15",
enableMonitoring = true
)
Full Example¶
val productionStack = dockerCompose {
name = "myapp-production"
// Frontend
service("nginx") {
image = "nginx:alpine"
ports("80:80", "443:443")
volumes(
"./nginx/nginx.conf:/etc/nginx/nginx.conf:ro",
"./nginx/certs:/etc/nginx/certs:ro"
)
networks("frontend")
dependsOn {
serviceHealthy("api")
}
restart = "unless-stopped"
logging("json-file") {
maxSize("50m")
maxFile(5)
}
}
// API
service("api") {
image = "myapp/api:latest"
environment {
"NODE_ENV" to "production"
"DATABASE_URL" to "postgres://postgres:5432/app"
"REDIS_URL" to "redis://redis:6379"
"JWT_SECRET" to System.getenv("JWT_SECRET")
}
networks("frontend", "backend")
dependsOn {
serviceHealthy("postgres")
serviceHealthy("redis")
}
healthcheck {
testShell("curl -f http://localhost:3000/health || exit 1")
interval = "30s"
timeout = "10s"
retries = 3
startPeriod = "10s"
}
deploy {
replicas = 2
resources {
limits {
cpus = "1"
memory = "1G"
}
}
restartPolicy {
condition = "on-failure"
maxAttempts = 3
}
}
}
// Database
service("postgres") {
image = "postgres:16-alpine"
environment {
"POSTGRES_USER" to "app"
"POSTGRES_PASSWORD" to System.getenv("DB_PASSWORD")
"POSTGRES_DB" to "app"
}
volumes("pgdata:/var/lib/postgresql/data")
networks("backend")
healthcheck {
testCmd("pg_isready", "-U", "app")
interval = "10s"
retries = 5
}
}
// Cache
service("redis") {
image = "redis:7-alpine"
command("redis-server", "--appendonly", "yes")
volumes("redis-data:/data")
networks("backend")
healthcheck {
testCmd("redis-cli", "ping")
interval = "10s"
retries = 3
}
}
// Networks
network("frontend") {
driver = "bridge"
}
network("backend") {
driver = "bridge"
internal = true
}
// Volumes
volume("pgdata") {
driver = "local"
}
volume("redis-data") {
driver = "local"
}
}
// Generate docker-compose.yml
productionStack.writeTo("docker-compose.prod.yml")