Kotlin Project Deployment Explained
The code is written — now the last mile. Charlie containerizes the OrderProcessor, orchestrates the CI/CD pipeline, configures monitoring and alerting, making deployment from git push to production fully automated.
1. What You'll Learn
- Docker multi-stage builds:
gradle:jdk→ lightweight JRE image - Docker Compose: OrderProcessor + PostgreSQL + Redis
- CI/CD: GitHub Actions build, test, push image
- Monitoring: Micrometer + Prometheus + Grafana metrics dashboard
- Charlie's hands-on: one-click deployment + health checks + production readiness checklist
2. An Architect's Real Story
(1) Pain Point: The Manual Deployment Pipeline
Charlie's team deployed manually: SSH to server → git pull → gradle build → java -jar → systemctl restart. One deployment took 30 minutes, with 2-3 human errors per month.
(2) Fully Automated CI/CD Solution
TEXT
Before: git push → SSH → build → deploy (30 min, error-prone)
After: git push → GitHub Actions → Docker build → deploy (5 min, zero-touch)
Containerization + CI/CD = deployment goes from 30 minutes of manual effort to a 5-minute fully automated pipeline.
3. Docker Multi-Stage Build
(1) Dockerfile
DOCKERFILE
# Stage 1: Build
FROM gradle:8.5-jdk17 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
COPY src ./src
RUN gradle bootJar --no-daemon -x test
# Stage 2: Runtime (lightweight JRE)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
# Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
(2) Multi-Stage vs Single-Stage Comparison
| Dimension | Single-Stage | Multi-Stage |
|---|---|---|
| Image size | ~800MB (JDK + source) | ~150MB (JRE only) |
| Security | Source in image | Source not in runtime image |
| Build caching | No layering | Independent layer caching per stage |
| Build time | Full rebuild every time | Dependency layer cache reuse |
4. Docker Compose Orchestration
(1) docker-compose.yml
YAML
version: '3.8'
services:
order-processor:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=r2dbc:postgresql://postgres:5432/orderdb
- SPRING_DATASOURCE_USERNAME=order_user
- SPRING_DATASOURCE_PASSWORD=order_pass
- SPRING_REDIS_HOST=redis
- MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,prometheus
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- order-net
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_DB=orderdb
- POSTGRES_USER=order_user
- POSTGRES_PASSWORD=order_pass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U order_user -d orderdb"]
interval: 5s
timeout: 5s
retries: 5
networks:
- order-net
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
networks:
- order-net
prometheus:
image: prom/prometheus:latest
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- order-net
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- order-net
volumes:
pgdata:
networks:
order-net:
driver: bridge
(2) One-Click Deployment
BASH
# Start all services
docker-compose up -d
# Check status
docker-compose ps
# View logs
docker-compose logs -f order-processor
# Stop all
docker-compose down
5. CI/CD Pipeline
(1) GitHub Actions
YAML
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: gradle-${{ hashFiles('**/*.gradle.kts') }}
- name: Run tests
run: ./gradlew test
- name: Build JAR
run: ./gradlew bootJar
- name: Build Docker image
run: docker build -t order-processor:${{ github.sha }} .
- name: Push to registry
if: github.ref == 'refs/heads/main'
run: |
docker tag order-processor:${{ github.sha }} registry.example.com/order-processor:latest
docker push registry.example.com/order-processor:latest
- name: Deploy
if: github.ref == 'refs/heads/main'
run: |
ssh deploy@prod-server "docker pull registry.example.com/order-processor:latest && docker-compose up -d"
(2) CI/CD Pipeline Diagram
flowchart TD
A[git push] --> B[GitHub Actions]
B --> C[Checkout Code]
C --> D[Setup JDK 17]
D --> E[Cache Gradle]
E --> F[Run Tests]
F --> G{Tests Pass?}
G -->|Yes| H[Build JAR]
G -->|No| I[Notify Team]
H --> J[Build Docker Image]
J --> K{Main Branch?}
K -->|Yes| L[Push to Registry]
K -->|No| M[Stop]
L --> N[Deploy to Production]
N --> O[Health Check]
O --> P{Healthy?}
P -->|Yes| Q[Live]
P -->|No| R[Rollback]
6. Monitoring
(1) Spring Boot Actuator Configuration
KOTLIN
// application.yml
// management:
// endpoints:
// web:
// exposure:
// include: health,info,prometheus,metrics
// metrics:
// export:
// prometheus:
// enabled: true
// endpoint:
// health:
// show-details: always
(2) Micrometer Custom Metrics
KOTLIN
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
class OrderMetrics(registry: MeterRegistry) {
private val ordersCreated = Counter.builder("orders.created.total")
.description("Total orders created")
.register(registry)
private val orderProcessingTime = Timer.builder("orders.processing.time")
.description("Order processing time")
.register(registry)
fun recordOrderCreated() { ordersCreated.increment() }
fun <T> recordProcessingTime(block: () -> T): T {
return orderProcessingTime.recordCallable { block() } ?: block()
}
}
(3) Monitoring Metrics
| Metric | Type | Alert Threshold |
|---|---|---|
orders_created_total |
Counter | — |
orders_processing_time |
Timer | P99 > 2s |
jvm_memory_used_bytes |
Gauge | > 80% |
db_connection_pool_active |
Gauge | > 90% |
http_server_requests_seconds |
Timer | P99 > 5s |
disk_free_bytes / disk_total_bytes |
Gauge | Free < 10% |
7. Health Checks and Production Readiness
(1) Health Check Endpoint
KOTLIN
// Spring Boot Actuator health endpoint
// GET /actuator/health
// {
// "status": "UP",
// "components": {
// "db": { "status": "UP" },
// "redis": { "status": "UP" },
// "diskSpace": { "status": "UP" }
// }
// }
(2) Production Readiness Checklist
| Category | Check Item | Status |
|---|---|---|
| Security | Non-root user running | ☐ |
| Security | No hardcoded secrets | ☐ |
| Security | HTTPS configured | ☐ |
| Reliability | Health check endpoint | ☐ |
| Reliability | Graceful shutdown (SIGTERM) | ☐ |
| Reliability | Database connection pool configured | ☐ |
| Observability | Structured logging output | ☐ |
| Observability | Prometheus metrics exposed | ☐ |
| Observability | Alert rules configured | ☐ |
| Performance | JVM heap size configured | ☐ |
| Performance | GC strategy selected | ☐ |
| Deployment | Docker image < 200MB | ☐ |
| Deployment | CI/CD pipeline | ☐ |
| Deployment | Rollback strategy | ☐ |
8. Complete Example: One-Click Deployment Demo
KOTLIN
// ============================================
// OrderProcessor - Deployment Simulation
// Feature: Docker + CI/CD + Health check demo
// ============================================
import kotlin.system.measureTimeMillis
data class DeployResult(val service: String, val status: String, val time: Long)
class DeploySimulator {
private val services = mutableListOf<DeployResult>()
private var deployed = false
fun build(): DeploySimulator {
print(" Building Docker image...")
val time = measureTimeMillis { Thread.sleep(800) }
println(" Done (${time}ms)")
return this
}
fun test(): DeploySimulator {
print(" Running tests...")
val time = measureTimeMillis { Thread.sleep(300) }
println(" Passed (${time}ms)")
return this
}
fun push(): DeploySimulator {
print(" Pushing to registry...")
val time = measureTimeMillis { Thread.sleep(500) }
println(" Done (${time}ms)")
return this
}
fun deploy(service: String, port: Int): DeploySimulator {
print(" Deploying $service on port $port...")
val time = measureTimeMillis { Thread.sleep(400) }
services.add(DeployResult(service, "RUNNING", time))
println(" Running (${time}ms)")
return this
}
fun healthCheck(): DeploySimulator {
print(" Health check...")
val time = measureTimeMillis { Thread.sleep(200) }
val allHealthy = services.all { it.status == "RUNNING" }
println(if (allHealthy) " ALL HEALTHY" else " UNHEALTHY DETECTED")
return this
}
fun summary() {
println("\n=== Deployment Summary ===")
services.forEach { s ->
println(" ${s.service}: ${s.status} (${s.time}ms)")
}
println("\n Total services: ${services.size}")
println(" Health: ${if (services.all { it.status == "RUNNING" }) "ALL GREEN" else "ISSUES DETECTED"}")
deployed = true
}
fun isDeployed() = deployed
}
fun main() {
println("=== OrderProcessor CI/CD Pipeline ===\n")
println("[1/6] Build Stage:")
DeploySimulator()
.build()
.test()
println("\n[2/6] Push Stage:")
DeploySimulator().push()
println("\n[3/6] Deploy Stage:")
val deployer = DeploySimulator()
.deploy("postgres", 5432)
.deploy("redis", 6379)
.deploy("order-processor", 8080)
.deploy("prometheus", 9090)
.deploy("grafana", 3000)
println("\n[4/6] Health Check:")
deployer.healthCheck()
println("\n[5/6] Smoke Test:")
println(" GET /actuator/health -> 200 OK")
println(" GET /api/v1/orders -> 200 OK")
println("\n[6/6] Production Ready Checklist:")
val checks = listOf(
"Non-root user" to true,
"No hardcoded secrets" to true,
"Health endpoint exposed" to true,
"Prometheus metrics enabled" to true,
"Graceful shutdown configured" to true,
"Docker image < 200MB" to true,
"CI/CD pipeline active" to true,
"Rollback strategy defined" to true
)
checks.forEach { (item, passed) ->
println(" ${if (passed) "✅" else "❌"} $item")
}
val passCount = checks.count { it.second }
println("\n Result: $passCount/${checks.size} checks passed")
if (passCount == checks.size) {
println("\n 🚀 OrderProcessor is LIVE!")
}
}
Output:
TEXT
=== OrderProcessor CI/CD Pipeline ===
[1/6] Build Stage:
Building Docker image... Done (804ms)
Running tests... Passed (301ms)
[2/6] Push Stage:
Pushing to registry... Done (502ms)
[3/6] Deploy Stage:
Deploying postgres on port 5432... Running (401ms)
Deploying redis on port 6379... Running (401ms)
Deploying order-processor on port 8080... Running (401ms)
Deploying prometheus on port 9090... Running (401ms)
Deploying grafana on port 3000... Running (401ms)
[4/6] Health Check:
Health check... ALL HEALTHY
[5/6] Smoke Test:
GET /actuator/health -> 200 OK
GET /api/v1/orders -> 200 OK
[6/6] Production Ready Checklist:
✅ Non-root user
✅ No hardcoded secrets
✅ Health endpoint exposed
✅ Prometheus metrics enabled
✅ Graceful shutdown configured
✅ Docker image < 200MB
✅ CI/CD pipeline active
✅ Rollback strategy defined
Result: 8/8 checks passed
🚀 OrderProcessor is LIVE!
❓ FAQ
Q What are the benefits of Docker multi-stage builds?
A The final image only contains runtime dependencies (JRE) — no source code or build tools. Image shrinks from 800MB to 150MB, with a smaller attack surface and faster startup.
Q How to achieve zero-downtime deployment?
A Blue-green deployment or rolling updates. Blue-green maintains two environments and switches between them. Rolling updates replace instances one by one. Kubernetes natively supports rolling updates.
Q How to rollback a failed deployment?
A Tag each Docker image build with the Git SHA, then rollback by simply
docker run registry.example.com/order-processor:<previous-sha>. Automated CI/CD rollback is even better.Q How to manage secrets?
A Never store secrets in environment variables or config files. Use Vault, AWS Secrets Manager, or Kubernetes Secrets. CI/CD injects them from the secret manager.
Q How to monitor a JVM application?
A Spring Boot Actuator + Micrometer + Prometheus + Grafana is the standard combo. JVM-specific metrics: heap memory, GC count/time, thread count.
Q How to configure JVM parameters for production?
A Set
-Xms and -Xmx to the same value (avoids heap resizing overhead). Use G1GC (-XX:+UseG1GC). In containers, add -XX:+UseContainerSupport.📖 Summary
- Docker multi-stage builds: build stage with JDK, runtime stage with JRE — image shrinks from 800MB to 150MB
- Docker Compose one-click orchestration: app + database + cache + monitoring
- CI/CD pipeline: git push → test → build → push → deploy, fully automated in 5 minutes
- Micrometer + Prometheus + Grafana: the trinity of metric collection, storage, and visualization
- Production readiness checklist: security, reliability, observability, performance, deployment — 8 essential checks
- Health checks are the production baseline:
/actuator/healthendpoint + Docker HEALTHCHECK
📝 Exercises
- Beginner (⭐): Write a Dockerfile for the OrderProcessor (single-stage is fine), based on
eclipse-temurin:17-jre. Hint:COPY build/libs/*.jar app.jar - Intermediate (⭐⭐): Write a docker-compose.yml containing OrderProcessor + PostgreSQL with health checks configured. Hint:
depends_onwithcondition: service_healthy - Advanced (⭐⭐⭐): Write a complete GitHub Actions CI/CD pipeline including build, test, Docker push, and deploy steps. Hint:
on: push: branches: [main]



