Lab - Dockerizing a Spring Boot REST API and Kubernetes Deployment
Lab 1: Dockerizing a Spring Boot REST API
Objective
Create a Dockerfile for a Spring Boot REST API and deploy it using Docker Compose
Prerequisites
- Java 17 or later
- Maven 3.6+
- Docker installed on your system
- Docker Compose installed on your system
- Book Catalog application from Module 4 with JPA integration
Step 1: Prepare the Spring Boot Application
Let’s first make sure our Book Catalog application from Module 4 is ready for containerization.
- Open the
pom.xml
file and add the Spring Boot Maven plugin build configuration:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${project.artifactId}:${project.version}</name>
</image>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
- Update the
application.properties
file to use environment variables for database configuration:
# Database Configuration
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:h2:mem:bookdb}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:sa}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:}
spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER:org.h2.Driver}
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# H2 Console Configuration (for development)
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
# Server port
server.port=${SERVER_PORT:8080}
- Build the application to verify everything works:
mvn clean package
Step 2: Create a Dockerfile
Create a new file called Dockerfile
in the root directory of your project with the following content:
# Stage 1: Build the application
FROM maven:3.8.6-eclipse-temurin-17-alpine AS build
WORKDIR /app
COPY pom.xml .
# Download all dependencies for caching
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: Create the runtime image
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Add a non-root user for security
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
# Copy JAR from the build stage
COPY --from=build /app/target/*.jar app.jar
# Environment variables
ENV SPRING_DATASOURCE_URL=jdbc:h2:mem:bookdb \
SPRING_DATASOURCE_USERNAME=sa \
SPRING_DATASOURCE_PASSWORD= \
SPRING_DATASOURCE_DRIVER=org.h2.Driver \
SERVER_PORT=8080
# Expose the application port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
# Run the application
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Step 3: Add Docker Compose Configuration
Create a new file called docker-compose.yml
in the root directory:
version: "3.8"
services:
book-catalog-app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/bookdb
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=postgres
- SPRING_DATASOURCE_DRIVER=org.postgresql.Driver
depends_on:
- postgres
networks:
- book-network
volumes:
- app-data:/app/data
restart: unless-stopped
postgres:
image: postgres:14-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=bookdb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- book-network
restart: unless-stopped
networks:
book-network:
driver: bridge
volumes:
postgres-data:
app-data:
Step 4: Update Dependencies for PostgreSQL
Since we’re now using PostgreSQL in our Docker Compose setup, we need to add the PostgreSQL driver to our pom.xml
:
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Step 5: Add Spring Boot Actuator for Health Checks
Add Spring Boot Actuator to our pom.xml
to support the health check endpoint:
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Update the application.properties
to expose the health endpoint:
# Actuator Configuration
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=always
Step 6: Build and Run with Docker Compose
- Build and start the containers:
docker-compose up -d
- Check if the containers are running:
docker-compose ps
- View the logs of the application:
docker-compose logs -f book-catalog-app
Step 7: Test the Dockerized Application
- Try accessing the API endpoints:
# Get all books
curl http://localhost:8080/api/books
# Add a new book
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{"title": "Docker for Spring Boot Developers", "author": "Docker Guru", "isbn": "1234567890123", "price": 29.99}'
# Verify the book was added
curl http://localhost:8080/api/books
- Check the health endpoint:
curl http://localhost:8080/actuator/health
Step 8: Scale the Service
Docker Compose allows you to scale services easily:
docker-compose up -d --scale book-catalog-app=3
Note: This will fail because all instances try to use the same port. To properly scale, we would need a load balancer.
Step 9: Using Environment-Specific Configurations
Create a docker-compose.dev.yml
file to override configurations for development:
version: "3.8"
services:
book-catalog-app:
environment:
- SPRING_PROFILES_ACTIVE=dev
- SPRING_DATASOURCE_URL=jdbc:h2:mem:bookdb
- SPRING_DATASOURCE_DRIVER=org.h2.Driver
ports:
- "8081:8080"
postgres:
ports:
- "5433:5432"
Run using the development compose file:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
Step 10: Implementing Docker Networking Best Practices
Update the docker-compose.yml
file to implement better networking practices:
version: "3.8"
services:
book-catalog-app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/bookdb
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=postgres
- SPRING_DATASOURCE_DRIVER=org.postgresql.Driver
depends_on:
- postgres
networks:
- backend
- frontend
volumes:
- app-data:/app/data
restart: unless-stopped
postgres:
image: postgres:14-alpine
expose:
- "5432" # Only exposed within the network, not to the host
environment:
- POSTGRES_DB=bookdb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
backend:
driver: bridge
frontend:
driver: bridge
volumes:
postgres-data:
app-data:
Step 11: Clean Up
When you’re done experimenting, stop and remove the containers:
docker-compose down -v # -v also removes the volumes
Lab 2: Kubernetes Deployment
Objective
Deploy the Dockerized app to Kubernetes
Prerequisites
- Docker installed and running
- Minikube or Docker Desktop with Kubernetes enabled
- kubectl command-line tool installed
- Dockerized Book Catalog application from Lab 1
Step 1: Verify Kubernetes Cluster
- Start your Kubernetes cluster (if not already running):
# For Minikube
minikube start
# For Docker Desktop
# Enable Kubernetes in Docker Desktop settings
- Verify that kubectl can connect to your cluster:
kubectl cluster-info
- Check the nodes in your cluster:
kubectl get nodes
Step 2: Create Kubernetes YAML Configuration Files
- First, create a namespace for our application:
Create a file named namespace.yaml
:
apiVersion: v1
kind: Namespace
metadata:
name: book-catalog
Apply it:
kubectl apply -f namespace.yaml
- Create a ConfigMap for application configuration:
Create a file named configmap.yaml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: book-catalog-config
namespace: book-catalog
data:
application.properties: |
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# Server configuration
server.port=8080
# Actuator Configuration
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=always
- Create a Secret for sensitive configuration:
Create a file named secret.yaml
:
apiVersion: v1
kind: Secret
metadata:
name: book-catalog-secret
namespace: book-catalog
type: Opaque
data:
# Values need to be base64 encoded
# echo -n "postgres" | base64
db-username: cG9zdGdyZXM=
# echo -n "postgres" | base64
db-password: cG9zdGdyZXM=
- Create a Persistent Volume Claim for the database:
Create a file named postgres-pvc.yaml
:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: book-catalog
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
- Create a Deployment and Service for PostgreSQL:
Create a file named postgres-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: book-catalog
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:14-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: bookdb
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: book-catalog-secret
key: db-username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: book-catalog-secret
key: db-password
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: book-catalog
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
- Create a Deployment for the Book Catalog application:
Create a file named book-catalog-deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: book-catalog
namespace: book-catalog
spec:
replicas: 2
selector:
matchLabels:
app: book-catalog
template:
metadata:
labels:
app: book-catalog
spec:
containers:
- name: book-catalog
image: book-catalog:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://postgres:5432/bookdb
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: book-catalog-secret
key: db-username
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: book-catalog-secret
key: db-password
- name: SPRING_DATASOURCE_DRIVER
value: org.postgresql.Driver
volumeMounts:
- name: config-volume
mountPath: /app/config
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: config-volume
configMap:
name: book-catalog-config
- Create a Service for the Book Catalog application:
Create a file named book-catalog-service.yaml
:
apiVersion: v1
kind: Service
metadata:
name: book-catalog
namespace: book-catalog
spec:
selector:
app: book-catalog
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
Step 3: Build and Push the Docker Image to Minikube
- Make sure you have built the Docker image for your application:
mvn clean package
docker build -t book-catalog:latest .
- For Minikube, you need to use the Minikube Docker daemon:
# For Minikube
eval $(minikube docker-env)
docker build -t book-catalog:latest .
Step 4: Apply Kubernetes Configurations
Apply all the Kubernetes configuration files:
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f postgres-pvc.yaml
kubectl apply -f postgres-deployment.yaml
kubectl apply -f book-catalog-deployment.yaml
kubectl apply -f book-catalog-service.yaml
Step 5: Verify Deployments and Services
- Check if the pods are running:
kubectl get pods -n book-catalog
- Check the services:
kubectl get services -n book-catalog
- Check the deployments:
kubectl get deployments -n book-catalog
Step 6: Access the Application
- For Minikube, get the URL:
minikube service book-catalog -n book-catalog --url
-
For Docker Desktop, you can access the service at http://localhost.
-
Test the endpoints:
# Get the service URL
export SERVICE_URL=$(minikube service book-catalog -n book-catalog --url)
# Get all books
curl $SERVICE_URL/api/books
# Add a new book
curl -X POST $SERVICE_URL/api/books \
-H "Content-Type: application/json" \
-d '{"title": "Kubernetes for Spring Boot Developers", "author": "K8s Guru", "isbn": "9876543210123", "price": 39.99}'
# Verify the book was added
curl $SERVICE_URL/api/books
Step 7: Scale the Deployment
- Scale the Book Catalog deployment to 3 replicas:
kubectl scale deployment book-catalog -n book-catalog --replicas=3
- Verify the scaling:
kubectl get pods -n book-catalog
Step 8: Monitor the Application
- View the logs of a specific pod:
# Get the pod name
export POD_NAME=$(kubectl get pods -n book-catalog -l app=book-catalog -o jsonpath="{.items[0].metadata.name}")
# View the logs
kubectl logs -f $POD_NAME -n book-catalog
- View resource usage:
kubectl top pods -n book-catalog
Step 9: Update the Application
- Make a change to your application code
- Build a new Docker image with a new tag:
docker build -t book-catalog:v2 .
- Update the deployment to use the new image:
kubectl set image deployment/book-catalog book-catalog=book-catalog:v2 -n book-catalog
- Watch the rollout:
kubectl rollout status deployment/book-catalog -n book-catalog
Step 10: Clean Up
When you’re done, clean up all resources:
kubectl delete namespace book-catalog
Summary and Key Takeaways
In this module, we explored how to containerize Spring Boot applications using Docker and how to deploy them to Kubernetes. Here are the key takeaways:
-
Containerization Benefits: Containers provide consistency, isolation, portability, and efficiency for Spring Boot applications.
-
Docker Fundamentals: We learned about Docker images, containers, and how to write efficient Dockerfiles using multi-stage builds.
-
Spring Boot Configuration: We explored how to configure Spring Boot applications for containerized environments, including externalizing configuration and implementing health checks.
-
Docker Compose: We used Docker Compose to define and run a multi-container environment with our Spring Boot application and PostgreSQL database.
-
Kubernetes Deployment: We deployed our containerized application to Kubernetes, implementing best practices for configuration, secrets, persistence, scaling, and monitoring.
-
Production Readiness: We implemented health checks, resource limits, persistent storage, and rolling updates to ensure our application is ready for production.
In the next module, we’ll focus on monitoring containerized Spring Boot applications using Prometheus and Grafana.
By Wahid Hamdi