Chapter 73: DevOps Projects Part 3: Docker Websites, APIs, Compose, Registry and ECR
73.4 Project 20: Dockerize a Spring Boot app with a multi-stage build
Friends, Java images get huge if you ship the JDK. Today we use a multi-stage Dockerfile: build with Maven JDK, run with a slim JRE Alpine, and prove health on 8080.
मित्रांनो, JDK सोबत Java images मोठ्या होतात. आज multi-stage: Maven JDK ने build, slim JRE Alpine ने run, port 8080 health.
दोस्तों, JDK के साथ Java images बड़ी हो जाती हैं. आज multi-stage: Maven JDK से build, slim JRE Alpine से run, port 8080 health.
What you will build in this project
- A tiny Spring Boot app with /actuator/health.
- Dockerfile FROM eclipse-temurin:21-jre-alpine.
- Image spring-api:1.0.
- Container on host port 8080.
1. Start from a real company story
At PhonePe in Bengaluru, the Spring Boot service was copied by hand onto several VMs. One box had the wrong runtime version.
Errors during peak cost about ₹22 lakh in failed UPI retries.
Dev packed the app into eclipse-temurin:21-jre-alpine. The same tag ran on every box. Rollback was one previous tag.
Today we dockerize that Spring Boot app.
Ravindra Bagale's Tip
Many students copy the whole Maven cache into the final image. Use a second FROM so only the jar remains. Keep that habit.
खूप students Maven cache final image मध्ये COPY करतात. दुसरा FROM वापरा जेणेकरून फक्त jar राहील. सवय ठेवा.
बहुत students Maven cache final image में COPY करते हैं. दूसरा FROM इस्तेमाल करें ताकि सिर्फ jar रहे. आदत रखिए.
2. Start and prepare
Step 1: Make project folder
Why: We need a clean folder for the Spring Boot app. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: mkdir and cd. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 1: mkdir
Type this:
mkdir -p /tmp/spring-api/src/main/java/com/demo /tmp/spring-api/src/main/resources && echo ready
You will see:
ready
What it means: Tree.
Example 2: pom
Type this:
printf '%s\n' '<project><modelVersion>4.0.0</modelVersion><groupId>com.demo</groupId><artifactId>spring-api</artifactId><version>1.0.0</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies></project>' > /tmp/spring-api/pom.xml && wc -c /tmp/spring-api/pom.xml
You will see:
... bytes
What it means: pom ready (lab may use prebuilt jar).
Example 3: note
Type this:
echo 'Lab tip: if Maven is slow, use a prebuilt jar named app.jar in /tmp/spring-api'
You will see:
Lab tip: if Maven is slow, use a prebuilt jar named app.jar in /tmp/spring-api
What it means: Practical tip.
Step 2: Write app files
Why: The Spring Boot files are the product we ship. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: printf the source files. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 4: appjava
Type this:
printf '%s\n' 'package com.demo;' 'import org.springframework.boot.*;' 'import org.springframework.web.bind.annotation.*;' '@SpringBootApplication' '@RestController' 'public class App {' ' public static void main(String[] a){SpringApplication.run(App.class,a);}' ' @GetMapping("/api/hello") String hello(){return "{\"msg\":\"pay-api\"}";}' '}' > /tmp/spring-api/src/main/java/com/demo/App.java && wc -l /tmp/spring-api/src/main/java/com/demo/App.java
You will see:
10 ...
What it means: Controller.
Example 5: props
Type this:
printf '%s\n' 'management.endpoints.web.exposure.include=health' > /tmp/spring-api/src/main/resources/application.properties && cat /tmp/spring-api/src/main/resources/application.properties
You will see:
management.endpoints.web.exposure.include=health
What it means: Actuator health.
Example 6: tree
Type this:
find /tmp/spring-api -type f | wc -l
You will see:
3
What it means: Files.
Step 3: Local syntax check
Why: Broken syntax must fail before Docker. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: Language check command. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 7: mvn note
Type this:
echo 'mvn -q -DskipTests package # produces target/*.jar'
You will see:
mvn -q -DskipTests package # produces target/*.jar
What it means: Build step.
Example 8: jar note
Type this:
echo 'Or copy a prebuilt app.jar for the lab'
You will see:
Or copy a prebuilt app.jar for the lab
What it means: Fallback.
Example 9: ready
Type this:
ls /tmp/spring-api | head
You will see:
pom.xml
src
What it means: Ready for Docker.
3. Build the main pieces
Step 4: Write Dockerfile
Why: Dockerfile pins eclipse-temurin:21-jre-alpine. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: FROM pin + COPY + CMD. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 10: df
Type this:
printf '%s\n' 'FROM eclipse-temurin:21-jdk-alpine AS build' 'WORKDIR /src' 'COPY pom.xml .' 'COPY src ./src' 'RUN apk add --no-cache maven && mvn -q -DskipTests package && cp target/*.jar /app.jar' 'FROM eclipse-temurin:21-jre-alpine' 'WORKDIR /app' 'COPY --from=build /app.jar app.jar' 'EXPOSE 8080' 'USER nobody' 'CMD ["java","-jar","app.jar"]' > /tmp/spring-api/Dockerfile && head -3 /tmp/spring-api/Dockerfile
You will see:
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /src
COPY pom.xml .
What it means: Dockerfile written.
Example 11: from
Type this:
head -1 /tmp/spring-api/Dockerfile
You will see:
FROM eclipse-temurin:21-jdk-alpine AS build
What it means: Pinned base.
Example 12: ls
Type this:
ls /tmp/spring-api/Dockerfile
You will see:
/tmp/spring-api/Dockerfile
What it means: Present.
Step 5: Write dockerignore
Why: Context must stay lean. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: .dockerignore lists junk. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 13: ignore
Type this:
printf '%s\n' '.git' '*.md' '__pycache__' 'node_modules' 'target' 'bin' 'obj' > /tmp/spring-api/.dockerignore && cat /tmp/spring-api/.dockerignore
You will see:
.git
*.md
__pycache__
node_modules
target
bin
obj
What it means: Lean context.
Example 14: count
Type this:
wc -l /tmp/spring-api/.dockerignore
You will see:
7 /tmp/spring-api/.dockerignore
What it means: Seven lines.
Example 15: mode
Type this:
stat -c '%a' /tmp/spring-api/.dockerignore
You will see:
644
What it means: Safe.
Step 6: Build the image
Why: build creates the image layers. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: docker build -t spring-api:1.0. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 16: build
Type this:
docker build -t spring-api:1.0 /tmp/spring-api
You will see:
Successfully tagged spring-api:1.0
What it means: Built.
Example 17: images
Type this:
docker images spring-api --format '{{.Repository}}:{{.Tag}}'
You will see:
spring-api:1.0
What it means: Listed.
Example 18: hist
Type this:
docker history spring-api:1.0 | head -3
You will see:
IMAGE CREATED CREATED BY SIZE
What it means: Layers.
4. Automate and verify
Step 7: Run the container
Why: run starts a live container. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: docker run -d --name spring-api. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 19: run
Type this:
docker rm -f spring-api >/dev/null 2>&1; docker run -d --name spring-api -p 8080:8080 spring-api:1.0 && echo started
You will see:
started
What it means: Detached.
Example 20: ps
Type this:
docker ps --filter name=spring-api --format '{{.Names}} {{.Ports}}'
You will see:
spring-api 0.0.0.0:8080->8080/tcp
What it means: Mapped.
Example 21: sleep
Type this:
sleep 2; echo up
You will see:
up
What it means: Warmup.
Step 8: Map the port
Why: Host port 8080 reaches the app. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: -p 8080:8080. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 22: port
Type this:
ss -ltn | grep ':8080 ' | head -1 || echo listening_via_docker
You will see:
LISTEN ... :8080
What it means: Port open.
Example 23: fmt
Type this:
docker port spring-api
You will see:
8080/tcp -> 0.0.0.0:8080
What it means: Publish map.
Example 24: names
Type this:
docker inspect -f '{{.Name}}' spring-api
You will see:
/spring-api
What it means: Name.
Step 9: curl health
Why: /actuator/health proves ready. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: curl returns 200. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 25: health
Type this:
curl -s http://127.0.0.1:8080/actuator/health
You will see:
{"status":"UP"}
What it means: Actuator UP.
Example 26: code
Type this:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/actuator/health
You will see:
200
What it means: OK.
Example 27: hello
Type this:
curl -s http://127.0.0.1:8080/api/hello
You will see:
{"msg":"pay-api"}
What it means: Route.
5. Harden and accept
Step 10: Read logs
Why: Logs show listen lines. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: docker logs tail. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 28: logs
Type this:
docker logs spring-api 2>&1 | tail -5
You will see:
listen or Started
What it means: App logged.
Example 29: follow
Type this:
echo 'docker logs -f spring-api'
You will see:
docker logs -f spring-api
What it means: Follow tip.
Example 30: inspect
Type this:
docker inspect -f '{{.State.Status}}' spring-api
You will see:
running
What it means: Running.
Step 11: Exec inside
Why: exec is for quick checks. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: docker exec. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 31: exec
Type this:
docker exec spring-api sh -c 'echo inside'
You will see:
inside
What it means: Shell works.
Example 32: pwd
Type this:
docker exec spring-api pwd
You will see:
/app
What it means: WORKDIR.
Example 33: id
Type this:
docker exec spring-api id | head -c 40; echo
You will see:
uid=...
What it means: User inside.
Step 12: Acceptance checklist
Why: Dev signs off when these pass. We do this before the next step so mistakes stay small and easy to undo. Read the output; the names and numbers tell you if the machine is healthy.
What: All green checks. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 34: smoke
Type this:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/actuator/health
You will see:
200
What it means: Smoke.
Example 35: mode
Type this:
stat -c '%a' /tmp/spring-api/Dockerfile
You will see:
644
What it means: Safe.
Example 36: Sign-off
Type this:
echo 'Dev: PASS'
You will see:
Dev: PASS
What it means: Done.
Check yourself
Q1. Base image?
eclipse-temurin:21-jre-alpine
Q2. Host port?
8080
Q3. Health path?
/actuator/health
Q4. Who fixed PhonePe?
Dev
Q5. Build command?
docker build
Q6. Why pin?
So builds stay repeatable
Friends, you dockerized a Spring Boot app with a pinned image. Did you understand? Let us try it once more. Change the health text, rebuild as 1.1, and switch the container.
मित्रांनो, तुम्ही pinned image ने Spring Boot app dockerize केली. health बदलून 1.1 rebuild करा.
दोस्तों, आपने pinned image से Spring Boot app dockerize की. health बदल 1.1 rebuild करें.
Lab: try at home
- Write the app files.
- Write Dockerfile with pinned tag.
- Build 1.0.
- Run and curl.
- Tag 1.1 after a text change.
Recap
- Pin base image tags.
- COPY only what you need.
- -p maps host to container.
- Same image on every host.
- Logs prove listen.