Dockerizing Spring Boot Applications

Table of Contents

  1. Introduction to Docker
  2. Spring Boot Application Setup
  3. Basic Docker Configuration
  4. Multi-stage Docker Builds
  5. Docker Compose for Development
  6. Production Docker Configuration
  7. Docker Security Best Practices
  8. Monitoring and Logging
  9. Kubernetes Deployment

Introduction to Docker

Docker is a platform that enables developers to package applications and their dependencies into containers. This ensures consistency across different environments and simplifies deployment processes.

Benefits of Dockerizing Spring Boot:

  • Consistency: Same environment across development, testing, and production
  • Isolation: Applications run in isolated containers
  • Portability: Run anywhere Docker is installed
  • Scalability: Easy to scale horizontally
  • Version Control: Track container versions

Spring Boot Application Setup

1. Basic Spring Boot Application

// Application.java
@SpringBootApplication
@RestController
public class DockerDemoApplication {
private final Environment environment;
public DockerDemoApplication(Environment environment) {
this.environment = environment;
}
public static void main(String[] args) {
SpringApplication.run(DockerDemoApplication.class, args);
}
@GetMapping("/")
public String home() {
String appName = environment.getProperty("app.name", "Dockerized Spring Boot");
String version = environment.getProperty("app.version", "1.0.0");
String hostname = getHostname();
return String.format("""
Welcome to %s (v%s)
Running on: %s
Java Version: %s
""", 
appName, version, hostname, 
System.getProperty("java.version"));
}
@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("timestamp", Instant.now().toString());
status.put("service", "docker-demo-app");
return ResponseEntity.ok(status);
}
@GetMapping("/env")
public Map<String, String> environment() {
Map<String, String> env = new HashMap<>();
env.put("JAVA_HOME", System.getenv("JAVA_HOME"));
env.put("SPRING_PROFILES_ACTIVE", 
environment.getProperty("spring.profiles.active", "default"));
env.put("HOSTNAME", getHostname());
return env;
}
private String getHostname() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return "unknown";
}
}
}

2. Application Configuration

# application.yml
spring:
application:
name: docker-demo-app
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
username: ${DB_USERNAME:app_user}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 20
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
app:
name: "Dockerized Spring Boot App"
version: "1.0.0"
description: "Spring Boot application running in Docker"
server:
port: ${SERVER_PORT:8080}
servlet:
context-path: /
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
enabled: true
logging:
level:
com.example: INFO
org.springframework.web: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file:
name: /var/log/app/application.log
# application-dev.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
logging.level.com.example=DEBUG
# application-prod.properties
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
logging.level.com.example=INFO
management.endpoints.web.exposure.include=health,info

3. Health Check Component

// DatabaseHealthIndicator.java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(1000)) {
return Health.up()
.withDetail("database", "Connected")
.withDetail("validationQuery", "SUCCESS")
.build();
} else {
return Health.down()
.withDetail("database", "Not connected")
.build();
}
} catch (SQLException e) {
return Health.down(e)
.withDetail("database", "Connection failed")
.build();
}
}
}
// CustomHealthIndicator.java
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// Check custom application health
double systemLoad = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
long freeMemory = Runtime.getRuntime().freeMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
Health.Builder status = systemLoad < 0.8 ? Health.up() : Health.down();
return status
.withDetail("systemLoad", systemLoad)
.withDetail("freeMemory", formatBytes(freeMemory))
.withDetail("totalMemory", formatBytes(totalMemory))
.withDetail("memoryUsage", 
String.format("%.2f%%", (1 - (double) freeMemory / totalMemory) * 100))
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
private String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
else if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024.0));
else return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
}
}

Basic Docker Configuration

1. Simple Dockerfile

# Basic Dockerfile for Spring Boot
FROM openjdk:17-jdk-slim
# Set working directory
WORKDIR /app
# Create non-root user for security
RUN groupadd -r spring && useradd -r -g spring spring
RUN chown -R spring:spring /app
USER spring
# Copy JAR file
COPY target/docker-demo-app-1.0.0.jar app.jar
# Expose port
EXPOSE 8080
# Set JVM options
ENV JAVA_OPTS="-Xmx512m -Xms256m -Djava.security.egd=file:/dev/./urandom"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Run the application
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

2. Optimized Dockerfile

# Multi-stage Dockerfile for optimized Spring Boot application
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app
# Copy pom.xml and download dependencies
COPY pom.xml .
RUN mvn dependency:go-offline
# Copy source code and build application
COPY src ./src
RUN mvn clean package -DskipTests
# Runtime stage
FROM openjdk:17-jdk-slim
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create application user
RUN groupadd -r spring && useradd -r -g spring spring
# Create application directory
WORKDIR /app
# Copy JAR from builder stage
COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar
# Create log directory
RUN mkdir -p /var/log/app && chown spring:spring /var/log/app
# Switch to non-root user
USER spring
# Expose application port
EXPOSE 8080
# JVM configuration
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
# Health check configuration
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Start application
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

3. .dockerignore File

# .dockerignore
.git
.gitignore
README.md
**/target/
**/.mvn/
**/mvnw
**/mvnw.cmd
**/.gitignore
**/HELP.md
**/*.iml
**/.idea/
**/*.log
**/logs/
Dockerfile
docker-compose.yml
**/node_modules/
**/dist/
**/build/
**/.gradle/
**/gradle/
**/*.jar
!target/*.jar

Multi-stage Docker Builds

1. Advanced Multi-stage Dockerfile

# Multi-stage build with different JDK versions and optimizations
FROM maven:3.8.6-openjdk-17 AS build
WORKDIR /workspace/app
# Copy pom.xml
COPY pom.xml .
# Download dependencies in docker cache layer
RUN mvn dependency:go-offline -B
# Copy source code
COPY src src
# Build application
RUN mvn clean package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
# Production stage
FROM openjdk:17-jre-slim
# Install necessary packages
RUN apt-get update && \
apt-get install -y --no-install-recommends curl tzdata && \
rm -rf /var/lib/apt/lists/* && \
apt-get clean
# Set timezone
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Create spring user
RUN groupadd -r spring && useradd -r -g spring spring
WORKDIR /application
# Copy extracted layers
COPY --from=build --chown=spring:spring /workspace/app/target/extracted/dependencies/ ./
COPY --from=build --chown=spring:spring /workspace/app/target/extracted/spring-boot-loader/ ./
COPY --from=build --chown=spring:spring /workspace/app/target/extracted/snapshot-dependencies/ ./
COPY --from=build --chown=spring:spring /workspace/app/target/extracted/application/ ./
# Create log directory
RUN mkdir -p /var/log/app && chown spring:spring /var/log/app
USER spring
EXPOSE 8080
# JVM configuration optimized for containers
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -Djava.security.egd=file:/dev/./urandom"
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} org.springframework.boot.loader.JarLauncher"]

2. Jib Configuration (Alternative to Dockerfile)

<!-- Maven Jib Plugin -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<from>
<image>openjdk:17-jre-slim</image>
</from>
<to>
<image>my-registry/docker-demo-app:${project.version}</image>
</to>
<container>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
<user>1000</user>
<ports>
<port>8080</port>
</ports>
<environment>
<JAVA_OPTS>-Xmx512m -Xms256m</JAVA_OPTS>
<SPRING_PROFILES_ACTIVE>prod</SPRING_PROFILES_ACTIVE>
</environment>
<labels>
<version>${project.version}</version>
<maintainer>[email protected]</maintainer>
</labels>
<format>OCI</format>
</container>
</configuration>
</plugin>
// Jib build commands
// mvn compile jib:build -Dimage=my-registry/docker-demo-app:latest
// mvn compile jib:dockerBuild -Dimage=docker-demo-app:latest

Docker Compose for Development

1. Development Docker Compose

# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: spring-boot-app
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=dev
- JAVA_OPTS=-Xmx512m -Xms256m
volumes:
- ./logs:/var/log/app
- ./config:/app/config
depends_on:
- postgres
- redis
networks:
- app-network
postgres:
image: postgres:14-alpine
container_name: postgres-db
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-network
redis:
image: redis:7-alpine
container_name: redis-cache
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-network
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin
environment:
- [email protected]
- PGADMIN_DEFAULT_PASSWORD=admin
ports:
- "5050:80"
depends_on:
- postgres
networks:
- app-network
redis-commander:
image: rediscommander/redis-commander:latest
container_name: redis-commander
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- "8081:8081"
depends_on:
- redis
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge

2. Development with Hot Reload

# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: spring-boot-app-dev
ports:
- "8080:8080"
- "5005:5005"  # Debug port
environment:
- SPRING_PROFILES_ACTIVE=dev
- JAVA_OPTS=-Xmx512m -Xms256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
- SPRING_DEVTOOLS_REMOTE_SECRET=mysecret
volumes:
- .:/app
- ~/.m2:/root/.m2
working_dir: /app
command: mvn spring-boot:run
networks:
- app-network
postgres:
image: postgres:14-alpine
container_name: postgres-db-dev
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=app_user
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data_dev:/var/lib/postgresql/data
networks:
- app-network
volumes:
postgres_data_dev:
networks:
app-network:
driver: bridge
# Dockerfile.dev
FROM maven:3.8.6-openjdk-17
WORKDIR /app
# Install curl for health checks
RUN apt-get update && apt-get install -y curl
EXPOSE 8080
EXPOSE 5005
CMD ["mvn", "spring-boot:run"]

Production Docker Configuration

1. Production Docker Compose

# docker-compose.prod.yml
version: '3.8'
services:
app:
image: my-registry/docker-demo-app:${TAG:-latest}
container_name: spring-boot-app-prod
restart: unless-stopped
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxRAMPercentage=75.0
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=appdb
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
env_file:
- .env.production
volumes:
- app_logs:/var/log/app
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- postgres
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app-network
postgres:
image: postgres:14-alpine
container_name: postgres-db-prod
restart: unless-stopped
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=${DB_USERNAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data_prod:/var/lib/postgresql/data
- ./postgres/backup:/backup
command: >
postgres -c shared_preload_libraries=pg_stat_statements
-c pg_stat_statements.track=all
-c max_connections=200
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d appdb"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app-network
redis:
image: redis:7-alpine
container_name: redis-cache-prod
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis_data_prod:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app-network
nginx:
image: nginx:alpine
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- app-network
volumes:
postgres_data_prod:
redis_data_prod:
app_logs:
networks:
app-network:
driver: bridge

2. Environment Files

# .env.production
# Database Configuration
DB_USERNAME=app_user_prod
DB_PASSWORD=secure_password_123
DB_NAME=appdb
DB_HOST=postgres
DB_PORT=5432
# Redis Configuration
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis_secure_password
# Application Configuration
SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=8080
JAVA_OPTS=-Xmx512m -Xms256m -XX:+UseG1GC
# Docker Configuration
TAG=1.0.0

3. Nginx Configuration

# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
upstream spring_app {
server app:8080;
}
server {
listen 80;
server_name localhost;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name localhost;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Proxy settings
location / {
proxy_pass http://spring_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Health check endpoint
location /health {
proxy_pass http://spring_app/actuator/health;
access_log off;
}
# Metrics endpoint (internal only)
location /metrics {
proxy_pass http://spring_app/actuator/metrics;
allow 127.0.0.1;
deny all;
access_log off;
}
}
}

Docker Security Best Practices

1. Secure Dockerfile

# Secure Dockerfile with security best practices
FROM openjdk:17-jdk-slim AS builder
# Security: Use no-root user during build
RUN groupadd -r builder && useradd -r -g builder builder
USER builder
WORKDIR /home/builder
COPY --chown=builder:builder pom.xml .
RUN mvn dependency:go-offline -B
COPY --chown=builder:builder src ./src
RUN mvn clean package -DskipTests
# Production stage
FROM openjdk:17-jre-slim
# Security: Install security updates
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/* && \
apt-get clean
# Security: Create non-root user
RUN groupadd -r spring && useradd -r -g spring spring -s /bin/false
# Security: Set secure permissions
WORKDIR /app
RUN chown spring:spring /app && chmod 755 /app
# Security: Copy files as non-root user
COPY --from=builder --chown=spring:spring /home/builder/target/*.jar app.jar
# Security: Create log directory with secure permissions
RUN mkdir -p /var/log/app && \
chown spring:spring /var/log/app && \
chmod 755 /var/log/app
# Security: Switch to non-root user
USER spring
# Security: Don't run as root
EXPOSE 8080
# Security: Set secure JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8"
# Security: Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Security: Use exec form for entrypoint
ENTRYPOINT exec java $JAVA_OPTS -jar app.jar

2. Security Scanning and Hardening

# docker-compose.security.yml
version: '3.8'
services:
trivy:
image: aquasec/trivy:latest
container_name: trivy-scanner
command: >
sh -c "trivy filesystem --exit-code 1 --no-progress /app"
volumes:
- .:/app:ro
networks:
- app-network
hadolint:
image: hadolint/hadolint:latest
container_name: hadolint-linter
command: hadolint Dockerfile
volumes:
- .:/app:ro
working_dir: /app
networks:
- app-network
grype:
image: anchore/grype:latest
container_name: grype-scanner
command: >
sh -c "grype dir:/app --fail-on high"
volumes:
- .:/app:ro
networks:
- app-network

3. Security Configuration in Application

// SecurityConfiguration.java
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.headers(headers -> headers
.contentSecurityPolicy("default-src 'self'")
.frameOptions().deny()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"https://mydomain.com",
"https://www.mydomain.com"
));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

Monitoring and Logging

1. Docker Logging Configuration

# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/console_templates'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
networks:
- app-network
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/datasources:/etc/grafana/provisioning/datasources
depends_on:
- prometheus
networks:
- app-network
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
networks:
- app-network
volumes:
prometheus_data:
grafana_data:
networks:
app-network:
driver: bridge

2. Prometheus Configuration

# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets: ['app:8080']
labels:
application: 'docker-demo-app'
environment: 'production'
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093

3. Application Monitoring Configuration

// MonitoringConfiguration.java
@Configuration
public class MonitoringConfiguration {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "docker-demo-app",
"environment", System.getenv().getOrDefault("SPRING_PROFILES_ACTIVE", "default")
);
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
// CustomMetricsService.java
@Service
public class CustomMetricsService {
private final Counter customCounter;
private final Timer customTimer;
private final Gauge customGauge;
public CustomMetricsService(MeterRegistry registry) {
this.customCounter = Counter.builder("app.custom.counter")
.description("Custom business events counter")
.register(registry);
this.customTimer = Timer.builder("app.custom.timer")
.description("Custom operation timer")
.register(registry);
this.customGauge = Gauge.builder("app.custom.gauge")
.description("Custom gauge")
.register(registry, this, service -> service.getGaugeValue());
}
public void recordEvent() {
customCounter.increment();
}
public void recordTimedOperation(Runnable operation) {
customTimer.record(operation);
}
private double getGaugeValue() {
// Return some business metric
return Math.random() * 100;
}
}

Kubernetes Deployment

1. Kubernetes Deployment Manifest

# k8s/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
labels:
app: spring-boot-app
spec:
replicas: 3
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8080"
spec:
containers:
- name: spring-boot-app
image: my-registry/docker-demo-app:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m -XX:+UseContainerSupport"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: app-secrets
key: db-host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumeMounts:
- name: app-logs
mountPath: /var/log/app
volumes:
- name: app-logs
emptyDir: {}
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
---
apiVersion: v1
kind: Service
metadata:
name: spring-boot-app-service
spec:
selector:
app: spring-boot-app
ports:
- port: 80
targetPort: 8080
type: LoadBalancer

2. Kubernetes Configuration Files

# k8s/configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
application.yml: |
spring:
application:
name: docker-demo-app
jpa:
show-sql: false
hibernate:
ddl-auto: validate
logging:
level:
com.example: INFO
file:
name: /var/log/app/application.log
---
# k8s/secrets.yml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
db-password: c2VjdXJlX3Bhc3N3b3JkXzEyMw==  # base64 encoded
redis-password: cmVkaXNfc2VjdXJlX3Bhc3N3b3Jk
jwt-secret: anNvbi13ZWItdG9rZW4tc2VjcmV0

3. Build and Deployment Scripts

#!/bin/bash
# build-and-deploy.sh
set -e
# Configuration
APP_NAME="docker-demo-app"
VERSION="1.0.0"
REGISTRY="my-registry"
K8S_NAMESPACE="default"
echo "Building Docker image..."
docker build -t $REGISTRY/$APP_NAME:$VERSION .
docker build -t $REGISTRY/$APP_NAME:latest .
echo "Pushing images to registry..."
docker push $REGISTRY/$APP_NAME:$VERSION
docker push $REGISTRY/$APP_NAME:latest
echo "Deploying to Kubernetes..."
kubectl apply -f k8s/configmap.yml
kubectl apply -f k8s/secrets.yml
kubectl apply -f k8s/deployment.yml
echo "Rolling update..."
kubectl rollout restart deployment/spring-boot-app -n $K8S_NAMESPACE
echo "Waiting for deployment to complete..."
kubectl rollout status deployment/spring-boot-app -n $K8S_NAMESPACE
echo "Deployment completed successfully!"
#!/bin/bash
# docker-management.sh
# Build with different tags
docker_build() {
local version=$1
docker build -t my-registry/docker-demo-app:$version .
docker build -t my-registry/docker-demo-app:latest .
}
# Security scan
docker_scan() {
docker scan my-registry/docker-demo-app:latest
}
# Clean up unused images
docker_cleanup() {
docker image prune -f
docker container prune -f
}
# View logs
docker_logs() {
docker-compose logs -f app
}
# SSH into container
docker_ssh() {
docker exec -it spring-boot-app /bin/sh
}
# Performance stats
docker_stats() {
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
}

Conclusion

Dockerizing Spring Boot applications provides numerous benefits for development, testing, and production deployment. Key takeaways:

  1. Use multi-stage builds for smaller, more secure images
  2. Implement proper health checks for container orchestration
  3. Follow security best practices (non-root users, regular updates)
  4. Use Docker Compose for development environments
  5. Implement proper monitoring and logging
  6. Optimize JVM settings for container environments
  7. Use .dockerignore to exclude unnecessary files
  8. Consider using Jib for simplified container builds

By following these patterns and best practices, you can create robust, scalable, and maintainable Dockerized Spring Boot applications that run consistently across different environments.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper