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:

  1. 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; }
}
  1. 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> {
}
  1. 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"));
    }
}
  1. 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

  1. Build and run your application:
./mvnw spring-boot:run   # for Maven
# or
./gradlew bootRun        # for Gradle
  1. 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:

  1. The first stage uses the JDK image to build the application
  2. The second stage uses the smaller JRE image to run the application
  3. We copy just the built JAR file from the first stage, keeping the final image small

Step 2: Build and test the Docker image

  1. Build the Docker image:
docker build -t springboot-app:1.0 .
  1. Run the container:
docker run -p 8080:8080 springboot-app:1.0
  1. 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

  1. Start the services:
docker-compose up -d
  1. 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
  1. 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
  1. 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
  1. 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
  1. 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"
  1. 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
  1. 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

  1. 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
  1. 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
  1. Check the deployment status:
kubectl get pods
kubectl get services
  1. 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

  1. Dockerization of Spring Boot Applications:
  • Creating efficient multi-stage Dockerfiles
  • Handling dependencies and build processes
  • Best practices for Java containerization
  1. Docker Compose for Development Environments:
  • Defining multi-container setups
  • Environment configuration
  • Service dependencies and networking
  • Volume persistence
  1. 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:

  1. Add health checks to your Spring Boot application and configure Kubernetes liveness/readiness probes
  2. Implement a horizontal pod autoscaler
  3. Set up a CI/CD pipeline to automate the deployment process
  4. Add monitoring with Prometheus and Grafana
  5. 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