# Containerizing Spring Boot Applications with Docker

> Complete guide to containerizing Spring Boot applications using Docker, including multi-stage builds, Docker Compose, Kubernetes deployment, and DevOps best practices.

- **Author:** Utsab Dahal, Backend Developer — Spring Boot & DevOps
- **Filed:** 25 December 2023 (2023-12-25)
- **Section:** DevOps
- **Reading time:** 11 min read
- **Tags:** Docker, Containerization, Spring Boot, DevOps, Kubernetes
- **Canonical page:** https://dahalutsab.com.np/dispatches/containerizing-spring-boot-docker

---

Docker revolutionizes application deployment by packaging Spring Boot applications with their dependencies into lightweight, portable containers. This comprehensive guide covers containerizing Spring Boot applications using Docker, including creating Dockerfiles, multi-stage builds, orchestrating with Docker Compose, deploying to Kubernetes, and following DevOps best practices. With practical examples and code snippets, you'll learn how to streamline your deployment pipeline for scalability and reliability.

## Why Docker?

Docker provides a consistent, portable, and efficient way to deploy Spring Boot applications, making it a cornerstone of modern DevOps practices.

### Benefits

- Consistency: Ensures identical environments across development, testing, and production, eliminating "it works on my machine" issues.
- Portability: Containers run on any system with Docker, from local machines to cloud providers.
- Scalability: Easily scale applications horizontally by running multiple container instances.
- Isolation: Each container runs in its own environment, preventing conflicts between applications.
- Resource Efficiency: Containers are lightweight compared to virtual machines, optimizing resource usage.

### Docker Basics

- Image: A read-only template used to create containers, containing the application and its dependencies.
- Container: A running instance of an image, isolated from the host system.
- Dockerfile: A script with instructions to build a Docker image.
- Registry: A repository (e.g., Docker Hub) for storing and distributing Docker images.

### Real-World Use Case

Imagine a Spring Boot-based e-commerce application with microservices for user management, order processing, and inventory. Without Docker, deploying these services across different environments risks configuration drift. Docker ensures each service runs in a consistent environment, simplifying deployment and scaling during high-traffic events like flash sales.

## Creating Dockerfiles

A Dockerfile defines the steps to build a Docker image for your Spring Boot application. Below are examples of basic and optimized Dockerfiles.

### Basic Dockerfile

```dockerfile
FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/myapp-1.0.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
```

**Explanation**:
- `FROM openjdk:17-jdk-slim`: Uses a lightweight OpenJDK 17 base image.
- `WORKDIR /app`: Sets the working directory inside the container.
- `COPY target/myapp-1.0.0.jar app.jar`: Copies the compiled JAR file.
- `EXPOSE 8080`: Declares the port the application uses.
- `ENTRYPOINT ["java", "-jar", "app.jar"]`: Runs the Spring Boot application.

### Optimized Dockerfile

```dockerfile
FROM openjdk:17-jdk-slim

# Create non-root user for security
RUN addgroup --system spring && adduser --system spring --ingroup spring

# Set working directory
WORKDIR /app

# Copy jar file
COPY target/myapp-1.0.0.jar app.jar

# Change ownership
RUN chown spring:spring app.jar

# Switch to non-root user
USER spring:spring

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

# Run application
ENTRYPOINT ["java", "-jar", "app.jar"]
```

**Improvements**:
- Runs as a non-root user (`spring`) to enhance security.
- Adds a health check using Spring Boot Actuator’s `/actuator/health` endpoint to monitor container health.
- Properly sets ownership of the JAR file to avoid permission issues.

## Multi-stage Builds

Multi-stage builds reduce image size and improve security by separating the build and runtime environments.

```dockerfile
# Build stage
FROM maven:3.8.4-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# Runtime stage
FROM openjdk:17-jdk-slim
RUN addgroup --system spring && adduser --system spring --ingroup spring
WORKDIR /app
COPY --from=build /app/target/myapp-1.0.0.jar app.jar
RUN chown spring:spring app.jar
USER spring:spring
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
```

**Benefits**:
- Smaller Image Size: The build stage uses Maven to compile the application, but the runtime stage only includes the compiled JAR and OpenJDK.
- Security: Excludes build tools (e.g., Maven) from the final image, reducing attack surface.
- Efficiency: Skips tests during the build (`-DskipTests`) to speed up the process (enable tests in CI/CD for quality assurance).

## Docker Compose

Docker Compose simplifies orchestrating multiple containers, such as a Spring Boot application and a database.

### Example Docker Compose Configuration

For an e-commerce application with a user service and MySQL database:

```yaml
version: '3.8'
services:
  user-service:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://user-db:3306/user_db
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=password
      - SPRING_PROFILES_ACTIVE=prod
    depends_on:
      - user-db
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 3s
      retries: 3
  user-db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=user_db
    ports:
      - "3306:3306"
    volumes:
      - user-db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 3s
      retries: 3
volumes:
  user-db-data:
```

**Explanation**:
- user-service: Builds the Spring Boot application from the Dockerfile.
- user-db: Runs a MySQL database with a persistent volume for data storage.
- Environment Variables: Configures the Spring Boot application to connect to MySQL.
- Health Checks: Ensures both services are healthy before starting dependent services.
- Volumes: Persists MySQL data to avoid data loss on container restart.

**Running Docker Compose**:
docker-compose up -d
This starts the services in detached mode. Access the application at http://localhost:8080.

## Deployment Strategies

Deploying Dockerized Spring Boot applications to production requires robust strategies for scalability and reliability.

### Deploying to Kubernetes

Kubernetes orchestrates containers for high availability and scalability.

**Deployment YAML** (for User Service):

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: myregistry/user-service:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_DATASOURCE_URL
          value: "jdbc:mysql://user-db:3306/user_db"
        - name: SPRING_DATASOURCE_USERNAME
          value: "root"
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: password
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
```

**Service YAML** (for User Service):

```yaml
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP
```

**MySQL Deployment YAML**:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-db
spec:
  selector:
    matchLabels:
      app: user-db
  template:
    metadata:
      labels:
        app: user-db
    spec:
      containers:
      - name: user-db
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secrets
              key: password
        - name: MYSQL_DATABASE
          value: user_db
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql-pvc
```

**Persistent Volume Claim (PVC)**:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
```

**Secrets** (create with kubectl):

kubectl create secret generic mysql-secrets --from-literal=password=password

**Deploying to Kubernetes**:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f mysql-deployment.yaml
kubectl apply -f mysql-pvc.yaml

### CI/CD Integration

Integrate Docker builds into a CI/CD pipeline using GitHub Actions:

```yaml
name: Build and Push Docker Image

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
    - name: Build with Maven
      run: mvn clean package -DskipTests
    - name: Log in to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: myregistry/user-service:1.0.0
```

This pipeline builds the Spring Boot application, creates a Docker image, and pushes it to a registry.

## Monitoring and Logging

Monitoring and logging ensure the reliability of Dockerized applications in production.

### Spring Boot Actuator

Enable Actuator for health and metrics:

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```

**application.yml**:

```yaml
management:
  endpoints:
    web:
      exposure:
        include: health, metrics, prometheus
```

### Prometheus Monitoring

Scrape metrics from the /actuator/prometheus endpoint:

```yaml
scrape_configs:
  - job_name: 'user-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
    - targets: ['user-service:8080']
```

### Centralized Logging with ELK Stack

Configure Logback to send logs to an ELK Stack:

```xml
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.2</version>
</dependency>
```

**logback-spring.xml**:

```xml
<configuration>
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>logstash:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
    </appender>
    <root level="INFO">
        <appender-ref ref="LOGSTASH"/>
    </root>
</configuration>
```

**Docker Compose for ELK**:

```yaml
version: '3.8'
services:
  logstash:
    image: docker.elastic.co/logstash/logstash:8.5.0
    ports:
      - "5000:5000"
    environment:
      - xpack.monitoring.enabled=false
    volumes:
      - ./logstash-pipeline:/usr/share/logstash/pipeline
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
  kibana:
    image: docker.elastic.co/kibana/kibana:8.5.0
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
```

## Best Practices

- Minimize Image Size:
  - Use slim base images (e.g., openjdk:17-jdk-slim).
  - Leverage multi-stage builds to exclude build tools.
  - Example:
    ```dockerfile
    RUN rm -rf /app/src
    ```

- Security:
  - Run containers as non-root users.
  - Scan images for vulnerabilities using Trivy:
    trivy image myregistry/user-service:1.0.0

- Health Checks:
  - Implement health checks in Dockerfiles and Kubernetes probes.
  - Example:
    ```yaml
    livenessProbe:
      httpGet:
        path: /actuator/health
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10
    ```

- Environment Configuration:
  - Use environment variables for configuration.
  - Example:
    ```yaml
    env:
      - name: SPRING_DATASOURCE_URL
        value: "jdbc:mysql://user-db:3306/user_db"
    ```

- Logging:
  - Configure structured JSON logging:
    ```java
    @Component
    public class LogService {
        private static final Logger logger = LoggerFactory.getLogger(LogService.class);

        public void logAction(String action) {
            logger.info("{\"action\": \"{}\", \"timestamp\": \"{}\"}", action, Instant.now());
        }
    }
    ```

- CI/CD Integration:
  - Automate image building and deployment.
  - Use versioned tags (e.g., myregistry/user-service:1.0.0).

- Resource Limits:
  - Set CPU and memory limits in Kubernetes:
    ```yaml
    resources:
      limits:
        cpu: "0.5"
        memory: "512Mi"
      requests:
        cpu: "0.2"
        memory: "256Mi"
    ```

## Conclusion

Containerizing Spring Boot applications with Docker enables consistent, portable, and scalable deployments. By using multi-stage builds, Docker Compose, and Kubernetes, you can streamline your DevOps pipeline. Key takeaways:

- Create efficient Dockerfiles with multi-stage builds.
- Use Docker Compose for local development and testing.
- Deploy to Kubernetes for production-grade scalability.
- Monitor containers with Prometheus and centralize logs with ELK.
- Follow best practices like running as non-root and automating CI/CD.

**Call to Action**: Start containerizing your Spring Boot application using the provided examples. Deploy locally with Docker Compose and scale to production with Kubernetes. Share your feedback or questions in the comments or on X to join the developer community!

---

Source: https://dahalutsab.com.np/dispatches/containerizing-spring-boot-docker
From The Utsab Dahal Times — the record of Utsab Dahal, Backend Developer — Spring Boot & DevOps, Kathmandu, Nepal.
Full record: https://dahalutsab.com.np/llms-full.txt
