Deployment & Production Readiness

Building a Production JAR with Maven

The Executable JAR

Spring Boot packages your entire application — code, dependencies, embedded Tomcat — into a single executable JAR file. This JAR is everything you need to run the application on any machine with Java installed.

# Build the JAR (skipping tests for speed — in CI/CD, never skip tests)
./mvnw clean package -DskipTests

# The JAR is created in the target/ directory
ls -lh target/blog-api-0.0.1-SNAPSHOT.jar
# -rw-r--r-- 1 user user 65M Jan 15 10:00 blog-api-0.0.1-SNAPSHOT.jar

What is Inside the JAR

The Spring Boot JAR is a “fat JAR” (uber-JAR) containing:

blog-api-0.0.1-SNAPSHOT.jar
├── BOOT-INF/
│   ├── classes/          ← Your compiled code + resources
│   │   ├── com/example/blogapi/
│   │   ├── application.properties
│   │   ├── db/migration/
│   │   └── ...
│   └── lib/              ← All dependency JARs (Spring, Hibernate, Jackson, etc.)
│       ├── spring-boot-3.3.0.jar
│       ├── hibernate-core-6.4.jar
│       ├── mariadb-java-client-3.3.jar
│       └── ... (100+ JARs)
├── META-INF/
│   └── MANIFEST.MF       ← Points to Spring Boot's launcher
└── org/springframework/boot/loader/   ← Spring Boot's class loader

Running the JAR

# Run with default settings
java -jar target/blog-api-0.0.1-SNAPSHOT.jar

# Run with a specific profile
java -jar target/blog-api-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

# Run with custom JVM settings
java -Xms256m -Xmx512m -jar target/blog-api-0.0.1-SNAPSHOT.jar

# Run with overridden properties
java -jar target/blog-api-0.0.1-SNAPSHOT.jar \
  --server.port=9090 \
  --spring.datasource.url=jdbc:mariadb://db-server:3306/blogdb

Build a Smaller JAR with Layers

Spring Boot 3.x supports layered JARs that optimize Docker image builds:

# Extract layers from the JAR (used in multi-stage Docker builds)
java -Djarmode=layertools -jar target/blog-api-0.0.1-SNAPSHOT.jar extract

This creates four directories — dependencies, spring-boot-loader, snapshot-dependencies, and application — which Docker can cache as separate layers. We will use this in Section 4.


Externalizing Configuration for Different Environments

The Configuration Hierarchy

Spring Boot reads configuration from multiple sources, in this priority order (highest wins):

1. Command-line arguments                   (--server.port=9090)
2. OS environment variables                  (SERVER_PORT=9090)
3. application-{profile}.properties          (application-prod.properties)
4. application.properties                    (default settings)

This means you can override any setting without changing the JAR. The same JAR runs in development, staging, and production — only the configuration differs.

Production Configuration

File: src/main/resources/application-prod.properties

# ============================================
# Production Profile Configuration
# ============================================

# Server
server.port=8080

# Database — use environment variables for secrets
spring.datasource.url=${DB_URL:jdbc:mariadb://mariadb:3306/blogdb}
spring.datasource.username=${DB_USERNAME:bloguser}
spring.datasource.password=${DB_PASSWORD}

# JPA — validate only, Flyway manages schema
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.open-in-view=false

# Flyway
spring.flyway.enabled=true
spring.flyway.clean-disabled=true

# Logging — minimal in production
logging.level.root=WARN
logging.level.com.example.blogapi=INFO
logging.level.org.hibernate.SQL=WARN
logging.file.name=/var/log/blog-api/application.log
logging.logback.rollingpolicy.max-file-size=50MB
logging.logback.rollingpolicy.max-history=30

# JWT — use environment variable for the secret
jwt.secret=${JWT_SECRET}
jwt.access-token-expiration=3600000
jwt.refresh-token-expiration=604800000

# File uploads
app.upload.dir=/var/data/blog-api/uploads

# Cache
spring.cache.type=caffeine

# Actuator — expose only necessary endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=when-authorized

# Disable Swagger in production (optional)
springdoc.swagger-ui.enabled=false

The ${DB_PASSWORD} syntax reads from an environment variable. If the variable is not set, Spring Boot fails to start — which is the correct behavior for a required secret.

The ${DB_URL:jdbc:mariadb://mariadb:3306/blogdb} syntax provides a default value after the colon. If DB_URL is not set, it falls back to the default.


Spring Boot Actuator — Health Checks & Metrics

What is Actuator?

Spring Boot Actuator exposes operational endpoints for monitoring and managing your application in production. It answers questions like: “Is the app running? Is the database connected? How much memory is used?”

Add the Dependency

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

Available Endpoints

Endpoint Description
/actuator/health Application health status (UP/DOWN)
/actuator/info Application metadata (version, description)
/actuator/metrics Application metrics (memory, CPU, HTTP requests)
/actuator/env Environment properties (redacted secrets)
/actuator/loggers View and change log levels at runtime
/actuator/flyway Flyway migration status
/actuator/caches Cache statistics

Configuration

# Expose specific endpoints over HTTP
management.endpoints.web.exposure.include=health,info,metrics,flyway,caches

# Health endpoint shows detailed component status
management.endpoint.health.show-details=when-authorized

# Add build info to /actuator/info
management.info.env.enabled=true
info.app.name=Blog API
info.app.version=1.0.0
info.app.description=Blog platform API

# Health checks include database and disk space by default
management.health.db.enabled=true
management.health.diskspace.enabled=true

Health Check Response

curl http://localhost:8080/actuator/health
{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "MariaDB",
        "validationQuery": "SELECT 1"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 107374182400,
        "free": 54321098765,
        "threshold": 10485760
      }
    },
    "ping": {
      "status": "UP"
    }
  }
}

If the database is unreachable, the status becomes DOWN. Load balancers and orchestrators (Kubernetes) use this endpoint to route traffic away from unhealthy instances.

Securing Actuator Endpoints

Add to SecurityConfig:

// Allow health and info publicly, require admin for everything else
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")

Custom Health Indicator

Create health checks for your own components:

@Component
public class FileStorageHealthIndicator implements HealthIndicator {

    @Value("${app.upload.dir:uploads}")
    private String uploadDir;

    @Override
    public Health health() {
        Path path = Path.of(uploadDir);
        if (Files.exists(path) && Files.isWritable(path)) {
            long freeSpace = path.toFile().getFreeSpace() / (1024 * 1024);
            return Health.up()
                    .withDetail("uploadDir", uploadDir)
                    .withDetail("freeSpaceMB", freeSpace)
                    .build();
        }
        return Health.down()
                .withDetail("uploadDir", uploadDir)
                .withDetail("error", "Directory not accessible")
                .build();
    }
}

Dockerizing the Spring Boot Application

Why Docker?

Docker packages your application and its runtime environment into a container — a lightweight, portable unit that runs identically on any machine with Docker installed. No more “it works on my machine” problems.

The Dockerfile

File: Dockerfile (in project root)

# ============================================
# Stage 1: Build the application
# ============================================
# Use a Maven image with Java 17 to compile the project
FROM maven:3.9-eclipse-temurin-17 AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy the Maven project files first (for dependency caching)
COPY pom.xml .

# Download dependencies (this layer is cached until pom.xml changes)
RUN mvn dependency:go-offline -B

# Copy the source code
COPY src ./src

# Build the JAR (skip tests — they run in CI/CD, not during Docker build)
RUN mvn clean package -DskipTests -B

# ============================================
# Stage 2: Create the runtime image
# ============================================
# Use a slim JRE image (much smaller than the Maven image)
FROM eclipse-temurin:17-jre-alpine

# Create a non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set working directory
WORKDIR /app

# Copy the JAR from the builder stage
COPY --from=builder /app/target/*.jar app.jar

# Create directories for uploads and logs
RUN mkdir -p /var/data/blog-api/uploads /var/log/blog-api \
    && chown -R appuser:appgroup /var/data/blog-api /var/log/blog-api /app

# Switch to non-root user
USER appuser

# Expose the application port
EXPOSE 8080

# Health check — Docker checks this to know if the container is healthy
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# JVM options for containers:
# -XX:MaxRAMPercentage=75 — use up to 75% of container memory for the JVM heap
# -XX:+UseG1GC — use the G1 garbage collector (good for containers)
ENTRYPOINT ["java", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:+UseG1GC", \
    "-Djava.security.egd=file:/dev/./urandom", \
    "-jar", "app.jar"]

Understanding the Multi-Stage Build

This Dockerfile uses two stages:

  1. Builder stage (maven:3.9-eclipse-temurin-17) — A large image (~800MB) with Maven and JDK. Compiles your code and produces the JAR. This image is NOT included in the final output.
  2. Runtime stage (eclipse-temurin:17-jre-alpine) — A tiny image (~150MB) with only the JRE. Copies the JAR from the builder stage. This is what actually runs.

The result: a final Docker image of ~200MB instead of ~800MB.

Build and Run the Image

# Build the Docker image
docker build -t blog-api:latest .

# Run the container
docker run -p 8080:8080 \
  -e DB_URL=jdbc:mariadb://host.docker.internal:3306/blogdb \
  -e DB_USERNAME=bloguser \
  -e DB_PASSWORD=blogpass \
  -e JWT_SECRET=my-super-secret-key-that-is-at-least-32-characters-long \
  -e SPRING_PROFILES_ACTIVE=prod \
  blog-api:latest

host.docker.internal is a special DNS name that resolves to the host machine from inside a Docker container (works on Mac and Windows). On Linux, use --network host or the host’s IP address.

.dockerignore

Create a .dockerignore file to exclude unnecessary files from the build context:

target/
.git/
.gitignore
.idea/
*.iml
*.md
docker-compose.yml
uploads/
logs/

Docker Compose — Spring Boot + MariaDB

Docker Compose orchestrates multiple containers. We will run the blog API and MariaDB together with a single command.

The docker-compose.yml

File: docker-compose.yml (in project root)

version: '3.8'

services:

  # ============================================
  # MariaDB Database
  # ============================================
  mariadb:
    image: mariadb:11
    container_name: blog-mariadb
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass}
      MARIADB_DATABASE: blogdb
      MARIADB_USER: ${DB_USERNAME:-bloguser}
      MARIADB_PASSWORD: ${DB_PASSWORD:-blogpass}
    ports:
      - "3306:3306"
    volumes:
      # Persist database data across container restarts
      - mariadb_data:/var/lib/mysql
    networks:
      - blog-network
    # Health check — ensures MariaDB is ready before the app starts
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  # ============================================
  # Spring Boot Application
  # ============================================
  blog-api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: blog-api
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_URL: jdbc:mariadb://mariadb:3306/blogdb
      DB_USERNAME: ${DB_USERNAME:-bloguser}
      DB_PASSWORD: ${DB_PASSWORD:-blogpass}
      JWT_SECRET: ${JWT_SECRET:-my-super-secret-key-that-is-at-least-32-characters-long-for-hs256}
    ports:
      - "8080:8080"
    volumes:
      # Persist uploaded files
      - uploads_data:/var/data/blog-api/uploads
      # Persist logs
      - ./logs:/var/log/blog-api
    networks:
      - blog-network
    # Wait for MariaDB to be healthy before starting
    depends_on:
      mariadb:
        condition: service_healthy
    restart: unless-stopped

# ============================================
# Named Volumes
# ============================================
volumes:
  mariadb_data:     # Database files
  uploads_data:     # Uploaded images

# ============================================
# Network
# ============================================
networks:
  blog-network:
    driver: bridge

Understanding Key Concepts

depends_on with condition: service_healthy: The blog API container waits until MariaDB’s health check passes before starting. Without this, the app might start before the database is ready — causing connection errors.

networks: blog-network: Both containers share a Docker network. The app connects to MariaDB using the service name mariadb as the hostname: jdbc:mariadb://mariadb:3306/blogdb. Docker’s internal DNS resolves mariadb to the container’s IP address.

Named volumes: mariadb_data and uploads_data persist data outside the containers. Deleting and recreating containers does not lose data.

restart: unless-stopped: If the container crashes, Docker restarts it automatically. It stops only if you manually stop it.

Running with Docker Compose

# Start everything in the background
docker compose up -d

# View logs (follow mode)
docker compose logs -f blog-api

# View both services' logs
docker compose logs -f

# Check status
docker compose ps

# Stop everything (data preserved in volumes)
docker compose down

# Stop and delete volumes (WARNING: data lost!)
docker compose down -v

# Rebuild after code changes
docker compose up -d --build

The .env File

Create a .env file for local development secrets (Git-ignored):

File: .env

DB_ROOT_PASSWORD=rootpass
DB_USERNAME=bloguser
DB_PASSWORD=blogpass
JWT_SECRET=my-super-secret-key-that-is-at-least-32-characters-long-for-hs256

Docker Compose reads .env automatically and substitutes the ${VARIABLE} placeholders in docker-compose.yml.

Add .env to .gitignore — never commit secrets to version control.


Environment Variables and Secrets Management

The Golden Rule

Never store secrets in code, configuration files, or Git. Use environment variables, secret managers, or encrypted vaults.

What Counts as a Secret?

  • Database passwords
  • JWT signing keys
  • API keys for external services (email, cloud storage)
  • OAuth client secrets
  • Encryption keys

Secret Management Options

Development: .env file (Git-ignored) or IDE environment variables.

Staging/Production:

Tool Description Best For
Environment variables Set on the host OS or container Simple deployments
Docker Secrets Built into Docker Swarm Docker Swarm deployments
AWS Secrets Manager Cloud-managed secrets AWS deployments
HashiCorp Vault Self-hosted secret management Enterprise environments
Kubernetes Secrets Built into Kubernetes Kubernetes deployments

For our blog API, environment variables via .env (development) and cloud secret managers (production) are the practical choices.


Nginx as Reverse Proxy (Brief Overview)

Why Nginx?

In production, you rarely expose Spring Boot directly to the internet. A reverse proxy sits in front of your application and handles:

  • SSL/TLS termination — Nginx handles HTTPS; Spring Boot only sees HTTP
  • Load balancing — Distribute traffic across multiple app instances
  • Static file serving — Serve images and assets faster than Spring Boot
  • Rate limiting — Protect against DoS attacks
  • Compression — Gzip responses to reduce bandwidth
Internet → Nginx (port 443 HTTPS) → Spring Boot (port 8080 HTTP)

Nginx Configuration

File: nginx/nginx.conf

server {
    listen 80;
    server_name blog.example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name blog.example.com;

    # SSL certificate (from Let's Encrypt or your CA)
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # Max upload size (must match Spring Boot's config)
    client_max_body_size 20M;

    # Proxy all requests to Spring Boot
    location / {
        proxy_pass http://blog-api:8080;
        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;
    }

    # Serve uploaded files directly (faster than proxying through Spring Boot)
    location /api/files/ {
        alias /var/data/blog-api/uploads/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Actuator endpoints — restrict to internal IPs
    location /actuator/ {
        proxy_pass http://blog-api:8080;
        allow 10.0.0.0/8;
        deny all;
    }
}

Adding Nginx to Docker Compose

# Add to docker-compose.yml services:
  nginx:
    image: nginx:alpine
    container_name: blog-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - uploads_data:/var/data/blog-api/uploads:ro
    networks:
      - blog-network
    depends_on:
      - blog-api
    restart: unless-stopped

CI/CD Pipeline Concepts (GitHub Actions Example)

What is CI/CD?

Continuous Integration (CI): Automatically build and test your code on every push. Catches bugs early.

Continuous Deployment (CD): Automatically deploy to staging/production after tests pass. Reduces manual work and human error.

GitHub Actions Pipeline

File: .github/workflows/ci.yml

name: Blog API CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:

  # ============================================
  # Job 1: Build and Test
  # ============================================
  test:
    runs-on: ubuntu-latest

    services:
      mariadb:
        image: mariadb:11
        env:
          MARIADB_ROOT_PASSWORD: testroot
          MARIADB_DATABASE: testdb
          MARIADB_USER: testuser
          MARIADB_PASSWORD: testpass
        ports:
          - 3306:3306
        options: >-
          --health-cmd="healthcheck.sh --connect --innodb_initialized"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Java 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Run tests
        run: ./mvnw verify -B
        env:
          SPRING_DATASOURCE_URL: jdbc:mariadb://localhost:3306/testdb
          SPRING_DATASOURCE_USERNAME: testuser
          SPRING_DATASOURCE_PASSWORD: testpass
          JWT_SECRET: ci-test-secret-key-that-is-at-least-32-characters

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: target/surefire-reports/

  # ============================================
  # Job 2: Build and Push Docker Image
  # ============================================
  build-image:
    needs: test  # Only runs after tests pass
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # Only on main branch

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            myregistry/blog-api:latest
            myregistry/blog-api:${{ github.sha }}

Pipeline Flow

Developer pushes code → GitHub Actions triggered
    ↓
Job 1: Test
    - Spin up MariaDB in a container
    - Build the project
    - Run all tests (unit + integration)
    - Upload test report
    ↓ (only if tests pass)
Job 2: Build Image
    - Build Docker image
    - Push to Docker Hub / container registry
    ↓ (deployment — manual or automatic)
Deploy to production server
    - Pull the new image
    - docker compose up -d

Production Checklist — Security, Logging, Monitoring

Security Checklist

☐ HTTPS enabled (via Nginx or cloud load balancer)
☐ CORS configured for known origins only
☐ CSRF disabled (stateless API) or enabled (session-based)
☐ JWT secret is strong (256+ bits) and stored as environment variable
☐ Passwords hashed with BCrypt (never plain text)
☐ SQL injection prevented (JPA parameterized queries)
☐ File upload validation (type, size, filename sanitization)
☐ Rate limiting configured (Nginx or Spring filter)
☐ Actuator endpoints restricted to internal IPs
☐ spring.jpa.show-sql=false in production
☐ Swagger UI disabled in production
☐ Error responses do not leak stack traces or internal details
☐ Database user has minimal permissions (no DROP, no GRANT)
☐ Dependencies scanned for vulnerabilities (mvn dependency-check:check)

Logging Checklist

☐ Root log level: WARN
☐ Application log level: INFO
☐ Hibernate SQL logging: OFF
☐ Logs written to file with rotation (50MB, 30 days)
☐ Structured log format (JSON) for log aggregation tools
☐ Sensitive data never logged (passwords, tokens, credit cards)
☐ Request IDs (correlation IDs) for tracing requests across services
☐ Log slow requests (>500ms) as warnings

Monitoring Checklist

☐ Actuator /health endpoint exposed for load balancer
☐ Database connectivity checked in health endpoint
☐ Disk space checked in health endpoint
☐ Application metrics exported (Prometheus, Datadog, etc.)
☐ Alert on: application DOWN, error rate spike, response time degradation
☐ HikariCP pool metrics monitored (active connections, pending requests)
☐ Cache hit rate monitored (Caffeine stats)
☐ JVM memory and GC metrics monitored

Hands-on: Dockerize and Deploy the Blog Application

Step 1: Create All Required Files

Verify you have these files in your project root:

blog-api/
├── Dockerfile
├── docker-compose.yml
├── .env                    ← Git-ignored, local secrets
├── .dockerignore
├── nginx/
│   └── nginx.conf
├── src/
│   └── main/resources/
│       ├── application.properties
│       └── application-prod.properties
└── pom.xml

Step 2: Build and Start

# Build and start all services
docker compose up -d --build

# Watch the logs
docker compose logs -f blog-api

# Expected output:
# blog-mariadb  | MariaDB init process done. Ready for start up.
# blog-api      | Flyway: Successfully applied 13 migrations
# blog-api      | Tomcat started on port 8080
# blog-api      | Started BlogApiApplication in 8.5 seconds

Step 3: Verify Everything Works

# Health check
curl http://localhost:8080/actuator/health
# {"status":"UP","components":{"db":{"status":"UP"},"diskSpace":{"status":"UP"}}}

# Register a user
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","email":"admin@blog.com","password":"adminpass123","fullName":"Admin"}'

# Login
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"adminpass123"}'

# Create a post (use the token from login response)
TOKEN="eyJ..."
curl -X POST http://localhost:8080/api/posts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"First Post from Docker","content":"Deployed and running in containers!","authorId":1}'

# Get the post
curl http://localhost:8080/api/posts/1

Step 4: Stop and Restart (Data Persists)

# Stop everything
docker compose down

# Start again
docker compose up -d

# Verify data survived the restart
curl http://localhost:8080/api/posts/1
# The post is still there — volumes preserved the data

Step 5: Deploy to a Remote Server

# On the remote server:
# 1. Install Docker and Docker Compose
# 2. Clone your repository (or copy docker-compose.yml + .env)
# 3. Create .env with production secrets

# .env on the production server:
DB_ROOT_PASSWORD=<strong-random-password>
DB_USERNAME=bloguser
DB_PASSWORD=<strong-random-password>
JWT_SECRET=<strong-random-256-bit-key>

# 4. Start the application
docker compose up -d

# 5. Set up SSL with Let's Encrypt (if using Nginx)
# certbot --nginx -d blog.example.com

Step 6: Exercises

  1. Add Nginx to Docker Compose: Follow the Nginx configuration from Section 7 and add it to your docker-compose.yml. Verify that you can access the API through Nginx on port 80.
  2. Set up GitHub Actions: Create the .github/workflows/ci.yml pipeline from Section 8. Push to GitHub and verify the pipeline runs tests successfully.
  3. Test the health check: Stop the MariaDB container while the app is running: docker stop blog-mariadb. Check /actuator/health — it should show DOWN. Start MariaDB again and verify recovery.
  4. Implement the custom health indicator: Add the FileStorageHealthIndicator from Section 3. Verify it appears in the health check response.
  5. Optimize the Docker image: Measure the current image size with docker images blog-api. Then rewrite the Dockerfile using Spring Boot layered JARs (extract + copy layers separately). Compare the image size and rebuild time.

Summary

This lecture prepared your blog API for production deployment:

  • Production JAR: ./mvnw clean package creates a self-contained executable JAR with embedded Tomcat. Run anywhere with java -jar.
  • Externalized configuration: Properties hierarchy (command line > env vars > profile properties > defaults) lets the same JAR run in any environment.
  • Spring Boot Actuator: Health checks (/actuator/health), metrics, and runtime management. Load balancers use health checks to route traffic. Custom health indicators for application-specific checks.
  • Docker: Multi-stage Dockerfile — build with Maven, run with slim JRE. Non-root user, health check, JVM container tuning. Image ~200MB.
  • Docker Compose: Single command deploys app + database with networking, volumes, health-check dependencies, and restart policies.
  • Secrets management: Never in code or Git. Use .env for development, cloud secret managers for production. Environment variables bridge the gap.
  • Nginx reverse proxy: SSL termination, static file serving, rate limiting, and actuator endpoint protection. Sits between the internet and your application.
  • CI/CD: GitHub Actions pipeline — test on every push, build Docker image on main branch, deploy automatically or manually.
  • Production checklist: Security (HTTPS, strong secrets, minimal permissions), logging (file rotation, no sensitive data), monitoring (health checks, metrics, alerts).

What is Next

In Lecture 18, we will bring everything together in the Capstone Project — reviewing the full system architecture, implementing any remaining features, and creating a deployment-ready blog platform.


Quick Reference

Concept Description
./mvnw clean package Build an executable JAR with all dependencies
Fat JAR / Uber JAR Single JAR containing code + all dependencies + embedded server
java -jar app.jar Run the application from the JAR
--spring.profiles.active=prod Activate a configuration profile
${DB_PASSWORD} Read value from environment variable
Spring Boot Actuator Production monitoring endpoints (health, metrics, info)
/actuator/health Application health status — used by load balancers
HealthIndicator Custom health check for application components
Multi-stage Dockerfile Build in one stage, run in another — smaller image
eclipse-temurin:17-jre-alpine Slim JRE Docker image (~150MB)
docker compose up -d Start all services in background
depends_on: condition: service_healthy Wait for dependency to be healthy
Named volumes Persist data outside containers
.env file Local secrets for Docker Compose (Git-ignored)
Nginx reverse proxy SSL, load balancing, static files, rate limiting
proxy_pass Nginx directive to forward requests to backend
GitHub Actions CI/CD pipeline — test, build, deploy on every push
HEALTHCHECK Docker instruction to verify container health
-XX:MaxRAMPercentage=75.0 JVM uses up to 75% of container memory

Leave a Reply

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