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:
- 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. - 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
- 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. - Set up GitHub Actions: Create the
.github/workflows/ci.ymlpipeline from Section 8. Push to GitHub and verify the pipeline runs tests successfully. - Test the health check: Stop the MariaDB container while the app is running:
docker stop blog-mariadb. Check/actuator/health— it should showDOWN. Start MariaDB again and verify recovery. - Implement the custom health indicator: Add the
FileStorageHealthIndicatorfrom Section 3. Verify it appears in the health check response. - 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 packagecreates a self-contained executable JAR with embedded Tomcat. Run anywhere withjava -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
.envfor 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 |
