Spring Boot Pre-Interview Guide Part 6: DevOps & Deployment — Spring Boot 4 · Kotlin 2.3 · Docker · GitHub Actions

Spring Boot Pre-Interview Guide Part 6: DevOps & Deployment — Spring Boot 4 · Kotlin 2.3 · Docker · GitHub Actions


Introduction

“I set up Docker but the DB connection keeps failing.” That’s the most common complaint right before submitting a pre-interview assignment. If a reviewer opens your repository and docker-compose up -d doesn’t bring the app up cleanly, the quality of the code underneath never gets seen.

Part 5 covered Security & Authentication. Part 6 focuses on the deployment layer running on top of that. When a reviewer opens a repository, the first three things they check are the Dockerfile, the docker-compose.yml, and the execution instructions in README.md. Clean versions of all three create a positive first impression before a single line of application code is read.

The target reader is a junior backend developer who knows the basics of Docker and GitHub Actions but isn’t sure which configuration choices are actually evaluated in a pre-interview assignment.

See the previous post for Security & Authentication.


TL;DR

  • Multi-stage Dockerfile is the baseline — Splitting into a gradle:8.10-jdk21 Builder stage and an eclipse-temurin:21-jre-alpine Runtime stage cuts image size by more than half. Adding a non-root user tells the reviewer you are thinking about security.
  • depends_on + healthcheck is the key combination — Only depends_on: condition: service_healthy guarantees that the DB is actually ready to accept queries. Using depends_on alone means the app can start connecting before MySQL finishes initializing.
  • GitHub Actions Gradle cache + JaCoCo — Caching ~/.gradle/caches with actions/cache@v4 significantly reduces build time. Adding Codecov upload gives you an automatic coverage report on every PR.
  • Profile-separated application.yml — Splitting into local (H2), docker (MySQL via env vars), and prod (Hikari pool settings) lets a reviewer run the project without any manual configuration.
  • Actuator health + prometheus endpoints — /actuator/health can hook directly into the Docker Compose healthcheck. /actuator/prometheus becomes a Prometheus scrape target. A custom HealthIndicator and MeterRegistry usage earns bonus credit.

1. Docker — The First Place a Reviewer Looks

1.1 Basic Dockerfile

Note: Spring Boot 4 + Kotlin 2.3 project setup (kotlin-spring, kotlin-jpa plugins, etc.) is covered in §1.1 of Part 1. Part 6 focuses on the DevOps and deployment layer that runs on top of that. The Kotlin 2.x line is backward-compatible — the same code works on 2.0 through 2.3.

A single-stage Dockerfile is the fastest to write, but it bundles the full JDK into the runtime image and results in a large image. Use it only for quick validation in early development.

FROM eclipse-temurin:21-jdk-alpine

WORKDIR /app

COPY build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

1.2 Multi-Stage Build

Separate the build stage from the runtime stage. The Builder stage produces the JAR; the Runtime stage only needs a JRE.

flowchart LR
    subgraph Builder["Builder stage (gradle:8.10-jdk21)"]
        Src["build.gradle.kts<br/>gradle/<br/>src/"]
        Deps["gradle dependencies<br/>(layer cache)"]
        Jar["bootJar<br/>--no-daemon -x test"]
        Src --> Deps --> Jar
    end

    subgraph Runtime["Runtime stage (eclipse-temurin:21-jre-alpine)"]
        AppJar["/app/app.jar"]
        User["non-root user<br/>spring:spring"]
        Entry["ENTRYPOINT<br/>java -jar app.jar"]
        AppJar --> User --> Entry
    end

    Builder -->|COPY --from=builder<br/>build/libs/*.jar| Runtime
# Build stage
FROM gradle:8.10-jdk21 AS builder

WORKDIR /app

# Copy only gradle files first for dependency caching
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle

# Download dependencies (leverage cache)
RUN gradle dependencies --no-daemon || true

# Copy source code and build
COPY src ./src
RUN gradle bootJar --no-daemon -x test

# Runtime stage
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# Copy only the built jar file
COPY --from=builder /app/build/libs/*.jar app.jar

# Security: run as non-root user
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
Aside — Image Size Comparison
MethodBase ImageEstimated Size
JDK + Full Sourceeclipse-temurin:21-jdk~500MB
JDK + JAR Onlyeclipse-temurin:21-jdk-alpine~350MB
JRE + JAR Onlyeclipse-temurin:21-jre-alpine~200MB

Tip: -alpine images are smaller, but may have compatibility issues with some native libraries.

1.3 .dockerignore

# .dockerignore
.git
.gitignore
.idea
*.iml
.gradle
build
!build/libs/*.jar
node_modules
*.md
docker-compose*.yml
Dockerfile*

1.4 Build and Run

# Build JAR (skip tests)
./gradlew bootJar -x test

# Build Docker image
docker build -t my-app:latest .

# Run container
docker run -d -p 8080:8080 --name my-app my-app:latest

# Check logs
docker logs -f my-app
Aside — JIB vs Dockerfile
MethodProsCons
DockerfileHigh flexibility, standard approachRequires Docker daemon, manual optimization
JIBNo Docker daemon needed, automatic layer optimization, fast buildsDepends on Gradle/Maven plugin

JIB Configuration Example:

// build.gradle.kts
plugins {
    id("com.google.cloud.tools.jib") version "3.4.0"
}

jib {
    from {
        image = "eclipse-temurin:21-jre-alpine"
    }
    to {
        image = "my-app"
        tags = setOf("latest", project.version.toString())
    }
    container {
        jvmFlags = listOf("-Xms512m", "-Xmx512m")
        ports = listOf("8080")
    }
}
# Build to local Docker without a Docker daemon
./gradlew jibDockerBuild

Recommended for assignments: Dockerfile is more universal and easier to understand


2. Docker Compose — Dependencies and Startup Order

2.1 Basic Configuration (App + MySQL)

Using depends_on alone only guarantees that the MySQL container process has started — it gives no assurance that MySQL is actually ready to accept queries. Pairing it with healthcheck and condition: service_healthy ensures the app starts only after the DB is truly ready.

flowchart TB
    App["app<br/>SPRING_PROFILES_ACTIVE=docker"]
    DB[("db: mysql:8.0<br/>healthcheck: mysqladmin ping")]
    Redis[("redis: redis:7-alpine")]
    Vol[("mysql_data<br/>volume")]

    App -->|depends_on:<br/>condition: service_healthy| DB
    App -.->|depends_on:<br/>condition: service_started| Redis
    DB --- Vol
# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=password
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=myapp
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mysql_data:

2.2 With Redis

Redis is ready to accept connections immediately after starting, so service_started is sufficient. Unlike MySQL, it has no initialization phase.

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=password
      - SPRING_DATA_REDIS_HOST=redis
      - SPRING_DATA_REDIS_PORT=6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=myapp
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql_data:

2.3 Development Configuration — DB Only

Use this when running the app from an IDE locally and only spinning up the DB in Docker.

# docker-compose.dev.yml
services:
  db:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=myapp
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

2.4 Common Commands

# Start all services
docker-compose up -d

# Build and start
docker-compose up -d --build

# Check logs
docker-compose logs -f app

# Start a specific service only
docker-compose up -d db

# Stop and remove services
docker-compose down

# Remove volumes as well
docker-compose down -v
Aside — depends_on, healthcheck, .env, service networking

depends_on and healthcheck:

  • depends_on alone only guarantees container startup order
  • Use healthcheck + condition: service_healthy to ensure the service is actually ready

Environment Variable Management:

# Using .env file
services:
  db:
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
# .env file
DB_PASSWORD=secure_password

Networking:

  • Services within the same docker-compose can communicate using service names
  • Example: jdbc:mysql://db:3306/myapp (db is the service name)

3. GitHub Actions — The CI Pipeline

GitHub Actions works by creating a .github/workflows/ directory in your repository and adding a YAML file. Since pre-interview assignments are almost always on GitHub, you can set up CI without any external infrastructure.

flowchart TB
    Push["push / pull_request<br/>main · develop"]
    Checkout["actions/checkout@v4"]
    Java["actions/setup-java@v4<br/>java-version: 21<br/>distribution: temurin"]
    Cache["actions/cache@v4<br/>~/.gradle/caches<br/>~/.gradle/wrapper"]
    Test["./gradlew test"]
    Coverage["./gradlew jacocoTestReport"]
    Upload["upload-artifact<br/>test-results"]
    Codecov["codecov-action@v4"]
    BuildJar["./gradlew bootJar -x test"]
    Buildx["docker/setup-buildx-action@v3"]
    LoginPush["docker/login-action@v3<br/>build-push-action@v5"]

    Push --> Checkout --> Java --> Cache --> Test --> Coverage
    Coverage --> Upload
    Coverage --> Codecov
    Coverage --> BuildJar --> Buildx --> LoginPush

3.1 Basic CI Pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Build with Gradle
        run: ./gradlew build

      - name: Run tests
        run: ./gradlew test

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: build/reports/tests/

3.2 With Test Coverage — JaCoCo

Attaching JaCoCo to CI generates a coverage report on every PR. Adding Codecov integration surfaces that coverage as a badge on the repository.

// build.gradle.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.11"
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

tasks.test {
    finalizedBy(tasks.jacocoTestReport)
}
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Build and Test with Coverage
        run: ./gradlew test jacocoTestReport

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: build/reports/tests/

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: build/reports/jacoco/test/jacocoTestReport.xml
          fail_ci_if_error: false

3.3 Docker Image Build and Push

On a merge to main or a tag push, this workflow automatically builds and pushes to Docker Hub. docker/metadata-action automatically generates image tags from the branch name, version tag, or commit SHA.

# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Build JAR
        run: ./gradlew bootJar -x test

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKER_USERNAME }}/my-app
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
Aside — GitHub Actions vs Jenkins vs GitLab CI
ToolProsCons
GitHub ActionsGitHub integration, free tier, marketplaceGitHub lock-in
JenkinsFlexibility, rich pluginsComplex setup, infrastructure required
GitLab CIGitLab integration, built-inGitLab lock-in

Recommended for assignments: If the project is managed on GitHub, GitHub Actions is the simplest option

Aside — GitHub Actions Tips

Secrets Configuration:

  • Repository → Settings → Secrets and variables → Actions
  • Store sensitive information like DOCKER_USERNAME, DOCKER_PASSWORD, etc.

Leveraging Cache:

  • Reduce build time with Gradle dependency caching
  • Use actions/cache@v4

Conditional Execution:

- name: Deploy
  if: github.ref == 'refs/heads/main'
  run: ./deploy.sh

Matrix Builds:

strategy:
  matrix:
    java: [21]
steps:
  - uses: actions/setup-java@v4
    with:
      java-version: ${{ matrix.java }}

4. Profile Management — Separating Configuration by Environment

4.1 Environment-Specific File Layout

src/main/resources/
├── application.yml           # Common settings
├── application-local.yml     # Local development
├── application-dev.yml       # Development server
├── application-docker.yml    # Docker environment
├── application-prod.yml      # Production environment
└── application-test.yml      # Testing

4.2 Common Settings (application.yml)

# application.yml
spring:
  application:
    name: my-app
  jpa:
    open-in-view: false
    properties:
      hibernate:
        default_batch_fetch_size: 100

server:
  port: 8080

logging:
  level:
    root: INFO

4.3 Environment-Specific Settings

# application-local.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

logging:
  level:
    org.hibernate.SQL: DEBUG
    com.example: DEBUG
# application-docker.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST:db}:${DB_PORT:3306}/${DB_NAME:myapp}?useSSL=false&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${DB_USERNAME:root}
    password: ${DB_PASSWORD:password}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

logging:
  level:
    root: INFO
    com.example: INFO
# application-prod.yml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  jpa:
    hibernate:
      ddl-auto: none
    show-sql: false

logging:
  level:
    root: WARN
    com.example: INFO

server:
  shutdown: graceful

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

4.4 Activating Profiles

# Command line
java -jar app.jar --spring.profiles.active=prod

# Environment variable
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

# Docker
docker run -e SPRING_PROFILES_ACTIVE=docker my-app

# Docker Compose
environment:
  - SPRING_PROFILES_ACTIVE=docker
flowchart TD
    Start{"How to set the profile?"}
    Cli["Command line<br/>--spring.profiles.active=prod"]
    Env["Environment variable<br/>SPRING_PROFILES_ACTIVE=prod"]
    Compose["docker-compose<br/>environment:"]
    K8s["Kubernetes<br/>env: from Secret/ConfigMap"]

    Start -->|"jar directly"| Cli
    Start -->|"OS-level injection"| Env
    Start -->|"Compose env"| Compose
    Start -->|"Cluster env"| K8s
Aside — Environment Variables vs application.yml
MethodProsConsWhen to Use
application.ymlVersion controlled, readableFixed at build timeDefault settings, non-sensitive info
Environment VariablesRuntime changes, sensitive info separationHard to managePasswords, API keys, etc.

Recommended Pattern:

  • Set default values in application.yml
  • Override sensitive information with environment variables
  • Provide default values using ${DB_PASSWORD:default} syntax

5. Actuator & Monitoring — Exposing Health and Metrics

5.1 Actuator Configuration

// settings.gradle.kts
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            library("spring-boot-starter-actuator", "org.springframework.boot:spring-boot-starter-actuator:3.4.0")
            library("micrometer-registry-prometheus", "io.micrometer:micrometer-registry-prometheus:1.14.0")
        }
    }
}
// build.gradle.kts
dependencies {
    implementation(libs.spring.boot.starter.actuator)
    implementation(libs.micrometer.registry.prometheus)
}
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
  endpoint:
    health:
      show-details: when_authorized
  info:
    env:
      enabled: true

info:
  app:
    name: ${spring.application.name}
    version: 1.0.0
    description: My Spring Boot Application

5.2 Custom Health Check — HealthIndicator

Spring Boot Actuator automatically provides a basic DB health check, but you can add custom health checks for business-specific concerns.

@Component
class CustomHealthIndicator(
    private val dataSource: DataSource
) : HealthIndicator {

    override fun health(): Health {
        return try {
            dataSource.connection.use { connection ->
                if (connection.isValid(1)) {
                    Health.up()
                        .withDetail("database", "Available")
                        .build()
                } else {
                    Health.down().build()
                }
            }
        } catch (e: SQLException) {
            Health.down()
                .withDetail("database", "Unavailable")
                .withException(e)
                .build()
        }
    }
}

5.3 Prometheus Metrics

The micrometer-registry-prometheus dependency was added in §5.1. Expose the endpoint in application.yml so Prometheus can scrape /actuator/prometheus.

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

5.4 Custom Metrics — MeterRegistry

Record business events as metrics. Initialize the counter and timer as val properties directly in the primary constructor — no @PostConstruct or nullable fields needed.

@Component
class OrderMetrics(
    meterRegistry: MeterRegistry
) {
    private val orderCounter: Counter = Counter.builder("orders.created")
        .description("Number of orders created")
        .register(meterRegistry)

    private val orderProcessingTimer: Timer = Timer.builder("orders.processing.time")
        .description("Order processing time")
        .register(meterRegistry)

    fun incrementOrderCount() {
        orderCounter.increment()
    }

    fun recordProcessingTime(milliseconds: Long) {
        orderProcessingTimer.record(Duration.ofMillis(milliseconds))
    }
}

5.5 Graceful Shutdown

Setting server.shutdown: graceful tells Spring Boot to wait for in-flight requests to complete before shutting down. Add @PreDestroy to attach any extra cleanup logic.

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
@Component
class GracefulShutdownHandler {

    private val log = LoggerFactory.getLogger(this::class.java)

    @PreDestroy
    fun onShutdown() {
        log.info("Application is shutting down gracefully...")
        // wait for in-progress tasks to finish
    }
}

§5.X Aside: Actuator Security

Aside — Actuator Security Configuration

Limit exposed endpoints in production:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus  # Only what's needed

Apply authentication:

@Bean
fun actuatorSecurity(http: HttpSecurity): SecurityFilterChain {
    return http
        .securityMatcher("/actuator/**")
        .authorizeHttpRequests { auth ->
            auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
        }
        .build()
}

Use a separate port:

management:
  server:
    port: 9090  # Accessible only from internal network

Recap

  • Multi-stage Dockerfile + non-root user — Reduces image size and signals security awareness. A reviewer who sees a JRE runtime stage instead of a single JDK image reads it as “this person knows about optimization.”
  • depends_on + healthcheck combination — Using depends_on without service_healthy means the app can start connecting before the DB is ready, causing connection failures. This combination is the signal that you understand Docker properly.
  • GitHub Actions Gradle cache + JaCoCo — Without caching, every build re-downloads all dependencies. JaCoCo + Codecov integration means coverage is recorded automatically on every push.
  • Profile separation + environment variable injection — Splitting into local (H2), docker, and prod files with sensitive values injected as env vars is the baseline reviewers expect.
  • Actuator health + prometheus exposure — /actuator/health can serve as the Docker Compose healthcheck target, and custom HealthIndicator plus MeterRegistry usage earns a “monitoring-aware” evaluation.

Part 7 covers event-driven architecture, asynchronous processing, and multi-module project structure. It walks through cross-module flows with Kafka and Spring Events, the practical differences between @Async and CompletableFuture, and the criteria for splitting a Gradle project into multiple modules.

Checklist

ItemCheck
Is a Dockerfile written?⬜
Does it use a multi-stage build?⬜
Can the project run locally via Docker Compose?⬜
Does it use the depends_on + healthcheck combination?⬜
Are execution instructions specified in the README?⬜
Is GitHub Actions CI configured?⬜
Are profiles separated by environment?⬜
Is sensitive information separated into environment variables?⬜
Is the Actuator health endpoint enabled?⬜

README Template

## How to Run

### Local Execution (H2)

```bash
./gradlew bootRun --args='--spring.profiles.active=local'
```

### Docker Compose Execution

```bash
# Start all services
docker-compose up -d

# Check logs
docker-compose logs -f app

# Stop
docker-compose down
```

### Access Information

- API: http://localhost:8080
- Swagger: http://localhost:8080/swagger-ui.html
- H2 Console: http://localhost:8080/h2-console (local profile)
- Actuator: http://localhost:8080/actuator/health

Appendix

Common Mistakes on Assignments — 5 Types
  1. Docker Compose fails to run

    • Missing environment variables, port conflicts
    • Always test in a clean environment
  2. Error when no profile is specified

    • Provide a default profile setting or H2 fallback
    • Configure application.yml to work with default behavior
  3. GitHub Actions build failure

    • gradlew execution permission (chmod +x)
    • Do not ignore test failures (fix the issues)
  4. Sensitive information exposure

    • Hard-coding actual passwords in application.yml
    • Pushing secrets to a public GitHub repository
  5. Using depends_on without a healthcheck

    • MySQL may still be initializing when the app tries to connect
    • condition: service_healthy + healthcheck combination is required
Blue-Green vs Rolling vs Canary Deployment
StrategyCharacteristicsProsCons
Blue-GreenSwitch between two environmentsInstant rollback, zero downtimeRequires 2x resources
RollingGradual replacementResource efficientSlow rollback, version mixing
CanaryApply to a subset onlyMinimized riskComplex implementation

For assignments: You don’t need to implement a deployment strategy, but mentioning it in the README can earn bonus points

Prometheus + Grafana Monitoring Setup

1. Spring Boot Actuator + Micrometer Configuration

Use the spring-boot-starter-actuator and micrometer-registry-prometheus dependencies added in §5.1.

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, prometheus, metrics
  endpoint:
    health:
      show-details: when_authorized
  metrics:
    tags:
      application: ${spring.application.name}

2. Add Prometheus/Grafana to Docker Compose

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker

  prometheus:
    image: prom/prometheus:v2.45.0
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'

  grafana:
    image: grafana/grafana:10.0.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

3. Prometheus Configuration File

# monitoring/prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app:8080']

4. Grafana Dashboard Setup

  1. Access http://localhost:3000 (admin/admin)
  2. Data Sources → Add data source → Prometheus
  3. URL: http://prometheus:9090
  4. Import Dashboard → ID: 4701 (JVM Micrometer) or 11378 (Spring Boot Statistics)

For assignments: Implementing monitoring earns bonus points. At minimum, it is recommended to expose the /actuator/health endpoint.

Shop on Amazon

As an Amazon Associate, I earn from qualifying purchases.