Lab: Dockerizing a Spring Boot Application
In this lab, you’ll learn how to containerize a Spring Boot REST API using Docker, orchestrate it with Docker Compose, and then deploy it to Kubernetes. This hands-on experience will help you understand how modern applications are packaged, distributed, and scaled in production environments.
Prerequisites
- Basic knowledge of Spring Boot
- Java Development Kit (JDK) 17 or later installed
- Maven or Gradle installed
- Docker Desktop installed
- kubectl command-line tool installed
- A text editor or IDE (like IntelliJ IDEA or VS Code)
Part 1: Creating a Simple Spring Boot REST API
Let’s start by creating a simple Spring Boot application that we’ll containerize.
Step 1: Set up a new Spring Boot project
You can use Spring Initializr (https://start.spring.io/) with the following settings:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.0+
- Dependencies: Spring Web, Spring Data JPA, H2 Database
Alternatively, you can clone this starter repository: [provide repository URL if available]
Step 2: Create a simple REST API
Let’s implement a basic “Student Management” REST API with the following components:
- Create a Student entity class:
// src/main/java/com/example/demo/model/Student.java
package com.example.demo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
private String course;
// Constructors, getters, and setters
public Student() {}
public Student(String name, String course) {
this.name = name;
this.course = course;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
}
- Create a repository interface:
// src/main/java/com/example/demo/repository/StudentRepository.java
package com.example.demo.repository;
import com.example.demo.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentRepository extends JpaRepository<Student, Long> {
}
- Create a REST controller:
// src/main/java/com/example/demo/controller/StudentController.java
package com.example.demo.controller;
import com.example.demo.model.Student;
import com.example.demo.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentRepository repository;
@GetMapping
public List<Student> getAllStudents() {
return repository.findAll();
}
@PostMapping
public Student createStudent(@RequestBody Student student) {
return repository.save(student);
}
@GetMapping("/{id}")
public Student getStudentById(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new RuntimeException("Student not found"));
}
}
- Update
application.properties
:
# src/main/resources/application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
server.port=8080
Step 3: Test the application locally
- Build and run your application:
./mvnw spring-boot:run # for Maven
# or
./gradlew bootRun # for Gradle
- Test the API using curl or a tool like Postman:
# Create a new student
curl -X POST http://localhost:8080/api/students \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","course":"Computer Science"}'
# Get all students
curl http://localhost:8080/api/students
Part 2: Dockerizing the Spring Boot Application
Now that we have a working Spring Boot application, let’s containerize it.
Step 1: Create a Dockerfile
Create a file named Dockerfile
in the root directory of your project:
# Use official Java runtime as a base image
FROM eclipse-temurin:17-jdk
# Set the working directory in the container
WORKDIR /app
# Copy Maven/Gradle files first for better caching
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
# If using Gradle, replace with:
# COPY gradlew .
# COPY gradle gradle
# COPY build.gradle .
# Download dependencies (this layer will be cached unless pom.xml/build.gradle changes)
RUN ./mvnw dependency:go-offline -B
# If using Gradle, replace with:
# RUN ./gradlew dependencies
# Copy the project source
COPY src src
# Package the application
RUN ./mvnw package -DskipTests
# If using Gradle, replace with:
# RUN ./gradlew build -x test
# Use a smaller base image for runtime
FROM eclipse-temurin:17-jre
WORKDIR /app
# Copy the built JAR file from the build stage
COPY --from=0 /app/target/*.jar app.jar
# Set the startup command
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# Expose the port the app runs on
EXPOSE 8080
This Dockerfile uses a multi-stage build approach:
- The first stage uses the JDK image to build the application
- The second stage uses the smaller JRE image to run the application
- We copy just the built JAR file from the first stage, keeping the final image small
Step 2: Build and test the Docker image
- Build the Docker image:
docker build -t springboot-app:1.0 .
- Run the container:
docker run -p 8080:8080 springboot-app:1.0
- Test the API again to ensure it works in the container:
curl http://localhost:8080/api/students
Part 3: Using Docker Compose for Multi-Container Setup
Now, let’s enhance our application by adding a persistent database using Docker Compose.
Step 1: Create a Docker Compose file
Create a file named docker-compose.yml
in the root directory:
version: "3.8"
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/studentdb
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=postgres
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect
depends_on:
- db
db:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=studentdb
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
Step 2: Update pom.xml to include PostgreSQL dependency
Add the PostgreSQL dependency to your pom.xml
:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
For Gradle (build.gradle
):
dependencies {
// ... other dependencies
runtimeOnly 'org.postgresql:postgresql'
}
Step 3: Test the Docker Compose setup
- Start the services:
docker-compose up -d
- Test the API with the PostgreSQL database:
# Create a student
curl -X POST http://localhost:8080/api/students \
-H "Content-Type: application/json" \
-d '{"name":"Jane Smith","course":"Data Science"}'
# Retrieve all students
curl http://localhost:8080/api/students
- Stop the services:
docker-compose down
Part 4: Deploying to Kubernetes
Finally, let’s deploy our application to Kubernetes.
Step 1: Create Kubernetes manifests
Create a directory for Kubernetes manifests:
mkdir -p k8s
- Create a deployment manifest (
k8s/deployment.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: springboot-app
spec:
replicas: 1
selector:
matchLabels:
app: springboot-app
template:
metadata:
labels:
app: springboot-app
spec:
containers:
- name: springboot-app
image: springboot-app:1.0
ports:
- containerPort: 8080
env:
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://postgres:5432/studentdb
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: SPRING_JPA_HIBERNATE_DDL_AUTO
value: update
- name: SPRING_JPA_DATABASE_PLATFORM
value: org.hibernate.dialect.PostgreSQLDialect
- Create a service manifest (
k8s/service.yaml
):
apiVersion: v1
kind: Service
metadata:
name: springboot-app
spec:
selector:
app: springboot-app
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
- Create a secret manifest (
k8s/secret.yaml
):
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
data:
username: cG9zdGdyZXM= # base64 encoded "postgres"
password: cG9zdGdyZXM= # base64 encoded "postgres"
- Create PostgreSQL deployment and service (
k8s/postgres.yaml
):
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: POSTGRES_DB
value: studentdb
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
- Create a persistent volume claim (
k8s/pvc.yaml
):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Step 2: Deploy to Kubernetes
- Make sure your Docker image is available to Kubernetes:
- If using Minikube:
eval $(minikube docker-env)
then rebuild your image - If using a container registry: push your image and update the image reference in deployment.yaml
- Apply the Kubernetes manifests:
kubectl apply -f k8s/pvc.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
- Check the deployment status:
kubectl get pods
kubectl get services
- Test the API (the specific URL will depend on your Kubernetes setup):
# If using Minikube
minikube service springboot-app --url
# Then use that URL to test the API
curl <service-url>/api/students
Key Concepts Learned
- Dockerization of Spring Boot Applications:
- Creating efficient multi-stage Dockerfiles
- Handling dependencies and build processes
- Best practices for Java containerization
- Docker Compose for Development Environments:
- Defining multi-container setups
- Environment configuration
- Service dependencies and networking
- Volume persistence
- Kubernetes Deployment:
- Kubernetes resource types (Deployments, Services, Secrets, PVCs)
- Environment configuration in Kubernetes
- Exposing applications with Services
- Managing sensitive information with Secrets
- Data persistence with PersistentVolumeClaims
Extensions and Challenges
For advanced students who finish early:
- Add health checks to your Spring Boot application and configure Kubernetes liveness/readiness probes
- Implement a horizontal pod autoscaler
- Set up a CI/CD pipeline to automate the deployment process
- Add monitoring with Prometheus and Grafana
- Implement a canary deployment strategy
Troubleshooting Tips
- If your Spring Boot app fails to connect to PostgreSQL in Kubernetes, check that the service names match the host in your connection string
- Use
kubectl logs <pod-name>
to diagnose application startup issues - For persistent volume issues, check your cluster’s storage provisioner configuration
- If images aren’t found, ensure your Kubernetes cluster has access to your Docker images
This lab provides a solid foundation for containerizing and orchestrating Spring Boot applications in modern cloud-native environments. The skills learned here are directly applicable to real-world microservice architectures.
By Wahid Hamdi