Development Examples¶
Using docker-kotlin for local development workflows.
Development Environment Script¶
#!/usr/bin/env kotlin
@file:DependsOn("io.github.joshrotenberg:docker-kotlin-compose:0.1.0")
import io.github.joshrotenberg.dockerkotlin.compose.*
suspend fun main(args: Array<String>) {
val command = args.firstOrNull() ?: "up"
when (command) {
"up" -> startDevEnv()
"down" -> stopDevEnv()
"logs" -> showLogs()
"status" -> showStatus()
else -> println("Usage: dev.kt [up|down|logs|status]")
}
}
suspend fun startDevEnv() {
println("Starting development environment...")
ComposeUpCommand()
.file("docker-compose.yml")
.file("docker-compose.dev.yml")
.projectName("myapp-dev")
.build()
.detach()
.wait()
.execute()
println("Development environment started!")
showStatus()
}
suspend fun stopDevEnv() {
println("Stopping development environment...")
ComposeDownCommand()
.file("docker-compose.yml")
.file("docker-compose.dev.yml")
.projectName("myapp-dev")
.volumes()
.execute()
println("Development environment stopped.")
}
suspend fun showLogs() {
ComposeLogsCommand()
.file("docker-compose.yml")
.projectName("myapp-dev")
.follow()
.execute()
}
suspend fun showStatus() {
val services = ComposePsCommand()
.file("docker-compose.yml")
.projectName("myapp-dev")
.execute()
println("\nServices:")
services.forEach { service ->
println(" ${service.name}: ${service.status}")
}
}
Database Migration Runner¶
import io.github.joshrotenberg.dockerkotlin.core.Docker
import io.github.joshrotenberg.dockerkotlin.core.command.*
suspend fun runMigrations(
migrationsDir: String,
databaseUrl: String
) {
val docker = Docker()
// Run flyway migrations
val containerId = docker.run("flyway/flyway:latest") {
rm() // auto-remove
volume(migrationsDir, "/flyway/sql")
command(
"-url=$databaseUrl",
"-user=postgres",
"-password=secret",
"migrate"
)
}
println("Migrations completed")
}
// Usage
suspend fun main() {
runMigrations(
migrationsDir = "./db/migrations",
databaseUrl = "jdbc:postgresql://localhost:5432/mydb"
)
}
Local Development Stack¶
import io.github.joshrotenberg.dockerkotlin.core.Docker
import io.github.joshrotenberg.dockerkotlin.core.command.*
import io.github.joshrotenberg.dockerkotlin.redis.RedisTemplate
import kotlinx.coroutines.*
class DevStack : AutoCloseable {
private val docker = Docker()
private val containers = mutableListOf<String>()
private lateinit var redis: RedisTemplate
private lateinit var postgresId: ContainerId
private lateinit var mailhogId: ContainerId
suspend fun start() {
println("Starting development stack...")
// Create network
NetworkCreateCommand("dev-network").execute()
coroutineScope {
// Start services in parallel
val redisJob = async { startRedis() }
val postgresJob = async { startPostgres() }
val mailhogJob = async { startMailhog() }
redisJob.await()
postgresJob.await()
mailhogJob.await()
}
printInfo()
}
private suspend fun startRedis() {
redis = RedisTemplate("dev-redis") {
network = "dev-network"
networkAliases.add("redis")
port(6379)
}
redis.startAndWait()
containers.add("dev-redis")
println(" Redis started")
}
private suspend fun startPostgres() {
postgresId = docker.run("postgres:16") {
name("dev-postgres")
network("dev-network")
networkAlias("database")
port(5432, 5432)
env("POSTGRES_USER", "dev")
env("POSTGRES_PASSWORD", "dev")
env("POSTGRES_DB", "devdb")
namedVolume("dev-postgres-data", "/var/lib/postgresql/data")
detach()
}
containers.add("dev-postgres")
// Wait for ready
waitForPostgres()
println(" PostgreSQL started")
}
private suspend fun waitForPostgres() {
repeat(30) {
try {
val result = ExecCommand(postgresId.value, "pg_isready", "-U", "dev")
.execute()
if (result.exitCode == 0) return
} catch (e: Exception) {}
delay(1000)
}
}
private suspend fun startMailhog() {
mailhogId = docker.run("mailhog/mailhog:latest") {
name("dev-mailhog")
network("dev-network")
networkAlias("mail")
port(1025, 1025) // SMTP
port(8025, 8025) // Web UI
detach()
}
containers.add("dev-mailhog")
println(" MailHog started")
}
private fun printInfo() {
println("\n=== Development Stack Ready ===")
println()
println("Services:")
println(" Redis: redis://localhost:6379")
println(" PostgreSQL: postgresql://dev:dev@localhost:5432/devdb")
println(" MailHog: http://localhost:8025 (SMTP: localhost:1025)")
println()
println("Network: dev-network")
println()
}
override fun close() {
println("Stopping development stack...")
runBlocking {
containers.reversed().forEach { name ->
try {
docker.stop(name)
docker.rm(name)
} catch (e: Exception) {
// Ignore
}
}
try {
NetworkRmCommand("dev-network").execute()
} catch (e: Exception) {
// Ignore
}
}
println("Development stack stopped.")
}
}
suspend fun main() {
val stack = DevStack()
stack.start()
println("Press Enter to stop...")
readLine()
stack.close()
}
Hot Reload Development¶
import io.github.joshrotenberg.dockerkotlin.core.Docker
import io.github.joshrotenberg.dockerkotlin.core.command.*
import java.nio.file.*
suspend fun main() {
val docker = Docker()
val projectDir = Paths.get(".").toAbsolutePath().toString()
// Start with mounted source code
val containerId = docker.run("node:20-alpine") {
name("dev-app")
port(3000, 3000)
volume(projectDir, "/app")
workdir("/app")
env("NODE_ENV", "development")
command("npm", "run", "dev")
detach()
}
println("Development server started at http://localhost:3000")
println("Source code mounted from: $projectDir")
// Follow logs
LogsCommand(containerId.value)
.follow()
.execute()
}
CI Pipeline Script¶
import io.github.joshrotenberg.dockerkotlin.core.command.*
import io.github.joshrotenberg.dockerkotlin.compose.*
import kotlin.system.exitProcess
suspend fun main() {
try {
// Build
println("=== Building ===")
BuildCommand(".")
.tag("myapp:${System.getenv("CI_COMMIT_SHA") ?: "local"}")
.tag("myapp:latest")
.noCache()
.execute()
// Start test dependencies
println("\n=== Starting Test Environment ===")
ComposeUpCommand()
.file("docker-compose.test.yml")
.detach()
.wait()
.execute()
// Run tests
println("\n=== Running Tests ===")
val testResult = ComposeRunCommand("app", "npm", "test")
.file("docker-compose.test.yml")
.rm()
.execute()
// Cleanup
println("\n=== Cleanup ===")
ComposeDownCommand()
.file("docker-compose.test.yml")
.volumes()
.execute()
if (testResult.exitCode != 0) {
println("Tests failed!")
exitProcess(1)
}
// Push if on main branch
if (System.getenv("CI_BRANCH") == "main") {
println("\n=== Pushing Image ===")
val registry = System.getenv("CI_REGISTRY") ?: "registry.example.com"
TagCommand("myapp:latest", "$registry/myapp:latest").execute()
PushCommand("$registry/myapp:latest").execute()
}
println("\n=== CI Pipeline Complete ===")
} catch (e: Exception) {
println("Pipeline failed: ${e.message}")
exitProcess(1)
}
}
Database Seeding¶
import io.github.joshrotenberg.dockerkotlin.core.command.*
suspend fun seedDatabase(containerName: String, seedFile: String) {
// Copy seed file to container
CpCommand(seedFile, "$containerName:/tmp/seed.sql")
.execute()
// Execute seed file
val result = ExecCommand(
containerName,
"psql", "-U", "postgres", "-d", "mydb", "-f", "/tmp/seed.sql"
).execute()
if (result.exitCode == 0) {
println("Database seeded successfully")
} else {
println("Seeding failed: ${result.stderr}")
}
}
suspend fun main() {
seedDatabase("dev-postgres", "./db/seeds/development.sql")
}
Interactive Shell¶
import io.github.joshrotenberg.dockerkotlin.core.command.*
suspend fun shell(containerName: String) {
// Start interactive shell
ExecCommand(containerName, "/bin/bash")
.interactive()
.tty()
.execute()
}
// For containers without bash
suspend fun shellAlpine(containerName: String) {
ExecCommand(containerName, "/bin/sh")
.interactive()
.tty()
.execute()
}