Table of Contents
- Introduction to Docker
- Spring Boot Application Setup
- Basic Docker Configuration
- Multi-stage Docker Builds
- Docker Compose for Development
- Production Docker Configuration
- Docker Security Best Practices
- Monitoring and Logging
- 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:
- Use multi-stage builds for smaller, more secure images
- Implement proper health checks for container orchestration
- Follow security best practices (non-root users, regular updates)
- Use Docker Compose for development environments
- Implement proper monitoring and logging
- Optimize JVM settings for container environments
- Use .dockerignore to exclude unnecessary files
- 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.