Ravindra BagaleCourses & study guides मराठी

Chapter 57: Docker Projects Part 1: Websites, Databases, LAMP, LEMP, WordPress, 3-Tier and Every Language with MySQL and MongoDB

57.8 Project 8: Java Spring Boot (Gradle) with MySQL and MongoDB

Why: the problem

Suppose we are at MakeMyTrip and the hotel team writes its bookings service in Java with Spring Boot, like most large Indian product companies and banks. A new developer needs the right JDK, Gradle, MySQL and MongoDB before writing one line, and the build often fails because of a different JDK version. Java images are also famous for being big and slow to start. With Docker we build the jar inside a gradle:8-jdk21 container, so nobody needs Java on the laptop. We then run it on a small eclipse-temurin:21-jre image as a non-root user, with memory limits that the JVM respects. We use Spring profiles to switch between MySQL (Spring Data JPA) and MongoDB (Spring Data MongoDB) with one environment variable. We also use the Spring Boot Actuator health endpoint for the Docker health check, and layered jars so code changes rebuild in seconds.

What: every term in plain English

  • Spring Boot — A Java framework that builds a runnable jar with a web server inside.
  • Gradle — A build tool for Java. ./gradlew bootJar makes the runnable jar.
  • JDK and JRE — The JDK compiles code. The JRE only runs it. The runtime image needs only the JRE.
  • Eclipse Temurin — Free, well-tested OpenJDK builds. eclipse-temurin:21-jre is a common base.
  • Spring profile — A named set of settings. SPRING_PROFILES_ACTIVE=mysql loads application-mysql.yml.
  • Spring Data JPA — Talks to SQL databases through Java interfaces called repositories.
  • Actuator — Adds /actuator/health and other ops endpoints.
  • MaxRAMPercentage — Tells the JVM to use a percentage of the container memory limit for the heap.
Write the Spring Boot app. Write build.gradle and two profiles. Write a multi-stage Dockerfile. Run with MySQL. Run with MongoDB. Tune memory and start time. Clean up.
Spring Boot bookings: Gradle build to JRE, then MySQL or MongoDB

How: step by step

  1. Write the Spring Boot app. Entity, repository, controller.
  2. Write build.gradle and two profiles. mysql and mongo.
  3. Write a multi-stage Dockerfile. gradle build stage, temurin JRE runtime.
  4. Run with MySQL. compose.mysql.yaml
  5. Run with MongoDB. compose.mongo.yaml
  6. Tune memory and start time. mem_limit and JAVA_OPTS.
  7. Clean up. down -v.

Architecture

gradle:8.10-jdk21 build stage (jar, layers extracted) java-bookings:1.0 on temurin 21 JRE (user spring, 768 MB limit) Profile mysql: MySQL 8.4 Profile mongo: MongoDB 7 /actuator/health/readiness used by HEALTHCHECK Named volume per database
Spring Boot bookings architecture

Folder layout

java-bookings/
  src/main/java/com/makemytrip/bookings/
    App.java
    Booking.java
    BookingStore.java
    Stores.java
  src/main/resources/
    application.yml
    application-mysql.yml
    application-mongo.yml
  build.gradle
  settings.gradle
  Dockerfile
  .env
  compose.mysql.yaml
  compose.mongo.yaml

Example 1: The Spring Boot code and Gradle build

Goal. The Spring Boot code and Gradle build.

settings.gradle

rootProject.name = 'bookings'

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.5'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.makemytrip'
version = '1.0.0'
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }

repositories { mavenCentral() }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    runtimeOnly 'com.mysql:mysql-connector-j'
}

src/main/java/com/makemytrip/bookings/Booking.java

package com.makemytrip.bookings;

import jakarta.persistence.*;

// MySQL row. MySQL gives a number id (AUTO_INCREMENT).
@Entity
@Table(name = "bookings")
public class Booking {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String hotel;
    private int nights;

    public Booking() {}
    public Booking(String hotel, int nights) { this.hotel = hotel; this.nights = nights; }
    public Long getId() { return id; }
    public String getHotel() { return hotel; }
    public int getNights() { return nights; }
}

src/main/java/com/makemytrip/bookings/BookingDoc.java

package com.makemytrip.bookings;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

// MongoDB document. MongoDB gives a text ObjectId, so the id is a String.
@Document("bookings")
public class BookingDoc {
    @Id
    private String id;
    private String hotel;
    private int nights;

    public BookingDoc() {}
    public BookingDoc(String hotel, int nights) { this.hotel = hotel; this.nights = nights; }
    public String getId() { return id; }
    public String getHotel() { return hotel; }
    public int getNights() { return nights; }
}

src/main/java/com/makemytrip/bookings/BookingIn.java

package com.makemytrip.bookings;

// What the client sends in POST /bookings.
public record BookingIn(String hotel, int nights) {}

src/main/java/com/makemytrip/bookings/BookingStore.java

package com.makemytrip.bookings;

import java.util.List;

// The controller only knows this interface. One profile gives one implementation.
public interface BookingStore {
    List<?> findAll();
    Object save(BookingIn in);
}

src/main/java/com/makemytrip/bookings/Stores.java

package com.makemytrip.bookings;

import java.util.List;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Component;

interface SqlRepo extends JpaRepository<Booking, Long> {}
interface MongoRepo extends MongoRepository<BookingDoc, String> {}

@Component
@Profile("mysql")
class SqlStore implements BookingStore {
    private final SqlRepo repo;
    SqlStore(SqlRepo repo) { this.repo = repo; }
    public List<?> findAll() { return repo.findAll(); }
    public Object save(BookingIn in) { return repo.save(new Booking(in.hotel(), in.nights())); }
}

@Component
@Profile("mongo")
class MongoStore implements BookingStore {
    private final MongoRepo repo;
    MongoStore(MongoRepo repo) { this.repo = repo; }
    public List<?> findAll() { return repo.findAll(); }
    public Object save(BookingIn in) { return repo.save(new BookingDoc(in.hotel(), in.nights())); }
}

src/main/java/com/makemytrip/bookings/App.java

package com.makemytrip.bookings;

import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

@SpringBootApplication
@RestController
public class App {
    private final BookingStore store;
    public App(BookingStore store) { this.store = store; }

    public static void main(String[] args) { SpringApplication.run(App.class, args); }

    @GetMapping("/bookings")
    public List<?> list() { return store.findAll(); }

    @PostMapping("/bookings")
    @ResponseStatus(HttpStatus.CREATED)
    public Object add(@RequestBody BookingIn b) {
        if (b.hotel() == null || b.nights() == 0)
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "hotel and nights are required");
        return store.save(b);
    }
}

What each line does

  • Booking and BookingDoc — Two small classes. MySQL uses a number id (Long). MongoDB uses a text ObjectId (String). One class cannot have both, so each database gets its own class.
  • List<?> — Java generics: a list of any type. The MySQL store returns Booking rows, the Mongo store returns BookingDoc documents.
  • @Profile("mysql") — Spring creates this store only when the mysql profile is active. The other store is skipped.
  • JpaRepository<Booking, Long> and MongoRepository<BookingDoc, String> — Spring writes the SQL and the Mongo queries for findAll and save for us. The second type is the id type.
  • BookingStore — Our own interface so the controller does not care which database is behind it.
  • toolchain 21 — Gradle compiles with Java 21 even if the machine has another JDK.
  • mysql-connector-j — The MySQL JDBC driver, needed only at run time.

Green output

$ docker run --rm -v "$PWD":/home/gradle/app -w /home/gradle/app gradle:8.10-jdk21 gradle wrapper
BUILD SUCCESSFUL in 6s
$ ls
build.gradle  gradle  gradlew  gradlew.bat  settings.gradle  src

Expected result. The Gradle wrapper (gradlew) is created by a throwaway container, so the laptop needs no Java.

Example 2: Two profiles: MySQL and MongoDB

Goal. Two profiles: MySQL and MongoDB.

src/main/resources/application.yml

server:
  port: 8080
management:
  endpoints.web.exposure.include: health,info
  endpoint.health.probes.enabled: true
spring:
  application.name: bookings

src/main/resources/application-mysql.yml

spring:
  datasource:
    url: jdbc:mysql://db:3306/bookings
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate.ddl-auto: update
    open-in-view: false
  autoconfigure.exclude:
    - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration
    - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
    - org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration

src/main/resources/application-mongo.yml

spring:
  data.mongodb.uri: ${MONGO_URL}
  autoconfigure.exclude:
    - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
    - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
    - org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration

What each line does

  • probes.enabled — Adds /actuator/health/liveness and /readiness, useful for Docker and Kubernetes.
  • ${DB_USER} — Spring reads the value from the environment of the container.
  • ddl-auto: update — Creates the bookings table on first start. Fine for practice; use Flyway in real projects.
  • autoconfigure.exclude — Turns off the database we are not using, so the app does not try to connect to it.
  • SPRING_PROFILES_ACTIVE — Set in Compose to mysql or mongo to load the right file.

Green output

$ ls src/main/resources
application-mongo.yml  application-mysql.yml  application.yml

Expected result. One jar, two profiles. The database is picked by one environment variable.

Example 3: Multi-stage Dockerfile with layered jars

Goal. Multi-stage Dockerfile with layered jars.

Dockerfile

# Stage 1: build the jar
FROM gradle:8.10-jdk21 AS build
WORKDIR /home/gradle/app
COPY settings.gradle build.gradle ./
RUN gradle dependencies --no-daemon > /dev/null
COPY src ./src
RUN gradle bootJar --no-daemon -x test
RUN java -Djarmode=tools -jar build/libs/bookings-1.0.0.jar extract --layers --launcher --destination /out

# Stage 2: run on a JRE
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 spring
WORKDIR /app
COPY --from=build /out/dependencies/ ./
COPY --from=build /out/spring-boot-loader/ ./
COPY --from=build /out/snapshot-dependencies/ ./
COPY --from=build /out/application/ ./
USER spring
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=5 \
  CMD curl -fs http://localhost:8080/actuator/health/readiness || exit 1
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]

What each line does

  • gradle dependencies first — Downloads all libraries into a cached layer before the code is copied.
  • -x test — Skips tests in the image build; run them in CI before.
  • jarmode=tools extract --layers — Splits the jar into dependencies and application layers. A code change rebuilds only the small application layer.
  • eclipse-temurin:21-jre — About 270 MB, much smaller than the 600 MB gradle JDK image.
  • MaxRAMPercentage=75 — Heap is 75 percent of the container memory limit.
  • ExitOnOutOfMemoryError — The JVM exits on OOM so Docker restarts it, instead of hanging.
  • exec java — exec makes Java PID 1 so it gets the stop signal.

Green output

$ docker build -t java-bookings:1.0 .
 => [build 6/7] RUN gradle bootJar --no-daemon -x test
 => [stage-1 7/8] COPY --from=build /out/application/ ./
 => naming to docker.io/library/java-bookings:1.0
$ docker image ls java-bookings
REPOSITORY      TAG   SIZE
java-bookings   1.0   318MB
$ touch src/main/java/com/makemytrip/bookings/App.java && docker build -t java-bookings:1.1 . 2>&1 | grep CACHED | wc -l
9

Expected result. The image is about 318 MB and a code-only change reuses 9 cached layers.

Example 4: Run with MySQL, then MongoDB, with memory limits

Goal. Run with MySQL, then MongoDB, with memory limits.

compose.mysql.yaml

services:
  api:
    build: .
    image: java-bookings:1.0
    ports: ["8080:8080"]
    environment:
      SPRING_PROFILES_ACTIVE: mysql
      DB_USER: bookings
      DB_PASSWORD: ${DB_PASSWORD}
    mem_limit: 768m
    depends_on:
      db: { condition: service_healthy }
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
      MYSQL_DATABASE: bookings
      MYSQL_USER: bookings
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes: [db-data:/var/lib/mysql]
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${ROOT_PASSWORD}"]
      interval: 5s
      retries: 20
volumes:
  db-data:

compose.mongo.yaml

services:
  api:
    image: java-bookings:1.0
    ports: ["8080:8080"]
    environment:
      SPRING_PROFILES_ACTIVE: mongo
      MONGO_URL: mongodb://root:${MONGO_PASSWORD}@mongo:27017/mmt?authSource=admin
    mem_limit: 768m
    depends_on:
      mongo: { condition: service_healthy }
  mongo:
    image: mongo:7
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    volumes: [mongo-data:/data/db]
    healthcheck:
      test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
      interval: 5s
      retries: 20
volumes:
  mongo-data:

.env

ROOT_PASSWORD=change-me-root
DB_PASSWORD=change-me-app
MONGO_PASSWORD=change-me-mongo

What each line does

  • mem_limit: 768m — The container can use at most 768 MB. With 75 percent the heap is about 576 MB.
  • SPRING_PROFILES_ACTIVE — Loads application-mysql.yml or application-mongo.yml.
  • start-period=40s — Spring Boot needs some seconds to start; failures during this time do not count.
  • /mmt in MONGO_URL — The database name used by Spring Data MongoDB.

Green output

$ docker compose -f compose.mysql.yaml -p mmt-mysql up -d --wait
 Container mmt-mysql-db-1   Healthy
 Container mmt-mysql-api-1  Healthy
$ docker compose -p mmt-mysql logs api | grep Started
api-1  | Started App in 6.842 seconds (process running for 7.6)
$ curl -s -X POST localhost:8080/bookings -H 'Content-Type: application/json' -d '{"hotel":"Taj Lands End","nights":2}'
{"id":1,"hotel":"Taj Lands End","nights":2}
$ curl -s localhost:8080/actuator/health
{"status":"UP","groups":["liveness","readiness"]}
$ docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' mmt-mysql-api-1
mmt-mysql-api-1 241MiB / 768MiB

Expected result. Spring Boot starts in about 7 seconds, stays inside the 768 MB limit, and saves bookings in MySQL. The Mongo file runs the same image.

Every command, step by step (plain docker and Compose)

  1. Make folders

    mkdir -p java-bookings/src/main/java/com/makemytrip/bookings java-bookings/src/main/resources && cd java-bookings
    

    What it does: Creates the Java package folders. Add every file from the examples.

    (no output)
    
  2. Build the image

    docker build -t java-bookings:1.0 .
    

    What it does: Compiles with Gradle in stage 1 and copies the layers into the JRE image.

    naming to docker.io/library/java-bookings:1.0
    
  3. Check the user

    docker run --rm --entrypoint id java-bookings:1.0
    

    What it does: The app runs as the spring user.

    uid=10001(spring) gid=999(spring)
    
  4. Start the MySQL stack

    docker compose -f compose.mysql.yaml -p mmt-mysql up -d --wait
    

    What it does: MySQL first, then Spring Boot.

    Container mmt-mysql-api-1 Healthy
    
  5. Watch the start

    docker compose -p mmt-mysql logs -f api
    

    What it does: Shows Spring logs. Press Ctrl+C to stop following.

    Started App in 6.842 seconds
    
  6. Add a booking

    curl -s -X POST localhost:8080/bookings -H 'Content-Type: application/json' -d '{"hotel":"Taj Lands End","nights":2}'
    

    What it does: Saves a row in MySQL.

    {"id":1,"hotel":"Taj Lands End","nights":2}
    
  7. See the table

    docker compose -p mmt-mysql exec db mysql -ubookings -pchange-me-app bookings -e 'SELECT * FROM bookings'
    

    What it does: Hibernate created the table on start.

    id  hotel           nights
    1   Taj Lands End   2
    
  8. Check health

    curl -s localhost:8080/actuator/health/readiness
    

    What it does: The same URL Docker uses for its health check.

    {"status":"UP"}
    
  9. Switch to Mongo

    docker compose -p mmt-mysql down
    docker compose -f compose.mongo.yaml -p mmt-mongo up -d --wait
    

    What it does: Same image with the mongo profile.

    Container mmt-mongo-api-1 Healthy
    
  10. Add a booking in Mongo

    curl -s -X POST localhost:8080/bookings -H 'Content-Type: application/json' -d '{"hotel":"Leela Palace","nights":3}'
    

    What it does: Saves a document. The id is now a text ObjectId, because BookingDoc uses a String id.

    {"id":"671b2f9e8c1d4a2b3c4d5e6f","hotel":"Leela Palace","nights":3}
    
  11. See memory

    docker stats --no-stream mmt-mongo-api-1
    

    What it does: Proves the JVM respects the limit.

    241MiB / 768MiB
    
  12. Clean up

    docker compose -f compose.mongo.yaml -p mmt-mongo down -v
    docker compose -f compose.mysql.yaml -p mmt-mysql down -v
    docker image rm java-bookings:1.0
    

    What it does: Removes everything from this project.

    Untagged: java-bookings:1.0
    

When it is red 1

Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago.

Cause. Spring started before MySQL was ready, or the URL host is not db.

Fix, step by step

  1. Use depends_on with condition: service_healthy.
  2. Check the datasource url is jdbc:mysql://db:3306/bookings.
  3. Run docker compose ps to see if db is healthy.

When it is red 2

Container mmt-mysql-api-1 exited with code 137 (OOMKilled)

Cause. The JVM used more memory than mem_limit.

Fix, step by step

  1. Keep -XX:MaxRAMPercentage=75 so the heap fits.
  2. Raise mem_limit to 1g if the app really needs it.
  3. Check with docker inspect --format '{{.State.OOMKilled}}'.

When it is red 3

Failed to configure a DataSource: 'url' attribute is not specified

Cause. The mongo profile is active but DataSource auto-config was not excluded, or no profile is set.

Fix, step by step

  1. Set SPRING_PROFILES_ACTIVE to mysql or mongo.
  2. Check the exclude list in application-mongo.yml.

When it is red 4

Unsupported class file major version 65

Cause. Code compiled with Java 21 but run on an older JRE.

Fix, step by step

  1. Use eclipse-temurin:21-jre in the runtime stage.
  2. Keep the Gradle toolchain at 21.

When it is red 5

container mmt-mysql-api-1 is unhealthy

Cause. The health check ran before Spring finished starting.

Fix, step by step

  1. Add --start-period=40s to HEALTHCHECK.
  2. Check the URL /actuator/health/readiness with curl inside the container.

Practice tasks

Easy Task 1: Build

Why: Start.

What to do: Build java-bookings:1.0 and check its size.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker build -t java-bookings:1.0 .
docker image ls java-bookings

Easy Task 2: MySQL

Why: Basics.

What to do: Start the MySQL stack and add a booking.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -f compose.mysql.yaml -p mmt-mysql up -d --wait
curl -s -X POST localhost:8080/bookings -H 'Content-Type: application/json' -d '{"hotel":"ITC Grand","nights":1}'

Easy Task 3: Mongo

Why: Profiles.

What to do: Run the Mongo stack with the same image.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -f compose.mongo.yaml -p mmt-mongo up -d --wait
curl -s localhost:8080/bookings

Medium Task 4: Start time

Why: Logs.

What to do: Find the Started App line and note the seconds.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -p mmt-mysql logs api | grep Started

Medium Task 5: Cache layers

Why: Speed.

What to do: Change only App.java and count CACHED steps.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker build -t java-bookings:1.1 . 2>&1 | grep -c CACHED

Medium Task 6: OOM

Why: Limits.

What to do: Set mem_limit to 200m and watch the exit code.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -p mmt-mysql up -d
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' mmt-mysql-api-1

Medium Task 7: Health

Why: Actuator.

What to do: Read liveness and readiness.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
curl -s localhost:8080/actuator/health/liveness
curl -s localhost:8080/actuator/health/readiness

Hard Task 8: Flyway

Why: Migrations.

What to do: Add Flyway and a V1 migration file.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
implementation 'org.flywaydb:flyway-mysql'
# src/main/resources/db/migration/V1__create_bookings.sql
CREATE TABLE bookings (id BIGINT AUTO_INCREMENT PRIMARY KEY, hotel VARCHAR(100), nights INT);

Hard Task 9: Smaller JRE

Why: Size.

What to do: Try eclipse-temurin:21-jre-alpine and compare sizes.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
FROM eclipse-temurin:21-jre-alpine
RUN adduser -S -u 10001 spring
docker build -t java-bookings:alpine .
docker image ls java-bookings

Easy Task 10: Clean

Why: Habit.

What to do: Remove both stacks and images.

How:
  1. Use a VM or laptop with Docker and Compose.
  2. Create the java-bookings folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -f compose.mongo.yaml -p mmt-mongo down -v
docker compose -f compose.mysql.yaml -p mmt-mysql down -v
docker image rm java-bookings:1.0 java-bookings:1.1

Homework: try at home

Replace ddl-auto: update with a Flyway migration V1__create_bookings.sql, run the MySQL stack, and prove that the table is created by Flyway (flyway_schema_history table exists).

Recap

  • Build Java inside a Gradle container; run on a JRE image.
  • Layered jars make code-only rebuilds fast.
  • MaxRAMPercentage keeps the JVM inside mem_limit.
  • Spring profiles switch MySQL and MongoDB with one variable.
  • Actuator readiness is the right health check URL.
  • Use start-period because Java takes seconds to start.

FAQ

Q. Maven or Gradle?

Both work the same way in Docker. Use maven:3.9-eclipse-temurin-21 and mvn package for Maven.

Q. Can I use Jib or Buildpacks instead of a Dockerfile?

Yes. ./gradlew jib or ./gradlew bootBuildImage build images without a Dockerfile.

Q. Why not ddl-auto in production?

It can change tables in surprising ways. Use Flyway or Liquibase migrations.

Q. How do I make start-up faster?

Use Class Data Sharing, Spring AOT, or a GraalVM native image (start in under 1 second).

Q. How do I see heap usage?

curl localhost:8080/actuator/metrics/jvm.memory.used after exposing metrics.

Q. Why does the health check use curl?

The temurin image is Ubuntu-based and has curl. On alpine use wget.

Interview questions

  1. How do you containerise a Spring Boot app well?

    Model answer: Multi-stage build, JRE runtime, layered jar, non-root user, MaxRAMPercentage, actuator health check, exec form.

  2. Why did older JVMs get OOMKilled in containers?

    Model answer: Before Java 10 the JVM read host memory, not the cgroup limit, and sized the heap too big.

  3. How do you switch config between environments in Spring?

    Model answer: Profiles plus environment variables; the image stays the same.

  4. What is the difference between liveness and readiness?

    Model answer: Liveness says the process is alive; readiness says it can serve traffic now (for example the database is reachable).

  5. How do you speed up Java image builds in CI?

    Model answer: Cache the dependency layer, use BuildKit cache mounts for Gradle, and use layered jars.

Quick quiz

Q1. Why build the jar inside Docker?

So nobody needs the JDK or Gradle installed and every build uses the same versions.

Q2. What does MaxRAMPercentage do?

Sets the Java heap as a percentage of the container memory limit.

Q3. How does one jar work with MySQL and MongoDB?

Spring profiles load different settings and turn off the unused database.

Q4. Why layered jars?

Dependencies change rarely, so their layer stays cached and code changes rebuild fast.

Q5. Why exec in the ENTRYPOINT?

So Java becomes PID 1 and stops cleanly on docker stop.

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards