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.

  1. 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>
  1. 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}
  1. 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

  1. Build and start the containers:
docker-compose up -d
  1. Check if the containers are running:
docker-compose ps
  1. View the logs of the application:
docker-compose logs -f book-catalog-app

Step 7: Test the Dockerized Application

  1. 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
  1. 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

  1. Start your Kubernetes cluster (if not already running):
# For Minikube
minikube start

# For Docker Desktop
# Enable Kubernetes in Docker Desktop settings
  1. Verify that kubectl can connect to your cluster:
kubectl cluster-info
  1. Check the nodes in your cluster:
kubectl get nodes

Step 2: Create Kubernetes YAML Configuration Files

  1. 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
  1. 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    
  1. 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=
  1. 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
  1. 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
  1. 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
  1. 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

  1. Make sure you have built the Docker image for your application:
mvn clean package
docker build -t book-catalog:latest .
  1. 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

  1. Check if the pods are running:
kubectl get pods -n book-catalog
  1. Check the services:
kubectl get services -n book-catalog
  1. Check the deployments:
kubectl get deployments -n book-catalog

Step 6: Access the Application

  1. For Minikube, get the URL:
minikube service book-catalog -n book-catalog --url
  1. For Docker Desktop, you can access the service at http://localhost.

  2. 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

  1. Scale the Book Catalog deployment to 3 replicas:
kubectl scale deployment book-catalog -n book-catalog --replicas=3
  1. Verify the scaling:
kubectl get pods -n book-catalog

Step 8: Monitor the Application

  1. 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
  1. View resource usage:
kubectl top pods -n book-catalog

Step 9: Update the Application

  1. Make a change to your application code
  2. Build a new Docker image with a new tag:
docker build -t book-catalog:v2 .
  1. Update the deployment to use the new image:
kubectl set image deployment/book-catalog book-catalog=book-catalog:v2 -n book-catalog
  1. 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:

  1. Containerization Benefits: Containers provide consistency, isolation, portability, and efficiency for Spring Boot applications.

  2. Docker Fundamentals: We learned about Docker images, containers, and how to write efficient Dockerfiles using multi-stage builds.

  3. Spring Boot Configuration: We explored how to configure Spring Boot applications for containerized environments, including externalizing configuration and implementing health checks.

  4. Docker Compose: We used Docker Compose to define and run a multi-container environment with our Spring Boot application and PostgreSQL database.

  5. Kubernetes Deployment: We deployed our containerized application to Kubernetes, implementing best practices for configuration, secrets, persistence, scaling, and monitoring.

  6. 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