Nginx + Tomcat Configuration with Docker Compose
Docker Compose is a tool that defines and manages multi-container applications using a single YAML file. You can run Nginx, Spring Boot (Tomcat), and PostgreSQL each as separate containers and start or stop the entire stack with a single command. This chapter explores a complete docker-compose.yml example close to a real production environment, along with in-depth explanations of each configuration setting.
docker compose vs docker-compose Difference (v1 vs v2)β
Two versions of Docker Compose exist.
| Category | docker-compose (v1) | docker compose (v2) |
|---|---|---|
| Installation | Separate Python package | Built-in Docker Engine plugin |
| Command | docker-compose up | docker compose up |
| File name | docker-compose.yml | docker-compose.yml (same) |
| Support status | EOL July 2023 | Currently officially supported |
| Performance | Relatively slower | Faster parallel processing |
Always use docker compose (with space, v2) now. Docker Desktop and modern Docker Engine include v2 by default.
Project Directory Structureβ
Project root/
βββ docker-compose.yml
βββ docker-compose.override.yml # Dev environment (auto-applied)
βββ docker-compose.prod.yml # Production environment
βββ .env # Environment variables (excluded from git)
βββ .env.example # Environment variable template (included in git)
βββ Dockerfile # Spring Boot application image
βββ nginx/
β βββ nginx.conf # Nginx main configuration
β βββ conf.d/
β βββ app.conf # Virtual host / proxy configuration
βββ db/
β βββ init/
β βββ 01-schema.sql # DB initialization scripts
βββ src/ # Spring Boot source code
Environment Variables File (.env)β
Sensitive information is separated into a .env file rather than embedded directly in code. Docker Compose automatically reads the .env file from the project root and injects environment variables.
# .env (actual values, excluded from git - add to .gitignore)
POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=SuperSecurePassword123!
POSTGRES_HOST=db
POSTGRES_PORT=5432
APP_PORT=8080
SPRING_PROFILES_ACTIVE=prod
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
# .env.example (template, included in git)
POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=change_me_in_production
POSTGRES_HOST=db
POSTGRES_PORT=5432
APP_PORT=8080
SPRING_PROFILES_ACTIVE=prod
NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
Complete docker-compose.yml Exampleβ
Below is a complete docker-compose.yml for configuring Nginx + Spring Boot + PostgreSQL. Each configuration item is annotated with comments for clarity.
version: '3.9'
services:
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 1. Nginx - Reverse Proxy / Static File Serving
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
nginx:
image: nginx:1.25-alpine # Pin specific version (avoid using latest)
container_name: nginx-proxy
ports:
- "${NGINX_HTTP_PORT:-80}:80" # host:container port mapping
- "${NGINX_HTTPS_PORT:-443}:443"
volumes:
# Config files: mounted read-only (:ro)
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
# SSL certificates: Let's Encrypt certbot integration
- certbot-conf:/etc/letsencrypt:ro
- certbot-www:/var/www/certbot:ro
# Logs: persistent storage via named volume
- nginx-logs:/var/log/nginx
# Static files: shared with Spring Boot-generated files
- static-files:/usr/share/nginx/html/static:ro
environment:
- TZ=Asia/Seoul
depends_on:
app:
condition: service_healthy # Start only after app passes health check
networks:
- frontend
restart: unless-stopped
# Resource limits (recommended for production)
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 2. Spring Boot - WAS (Web Application Server)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
app:
build:
context: . # Dockerfile location
dockerfile: Dockerfile
args:
- BUILD_VERSION=1.0.0
image: myapp:latest # Tag for built image
container_name: spring-app
# Use expose instead of ports: only exposed within container network
expose:
- "8080"
volumes:
- app-logs:/app/logs
- static-files:/app/static # Shared static files with Nginx
environment:
# Spring configuration
- SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-prod}
# DB connection (service name 'db' is automatically resolved as DNS)
- SPRING_DATASOURCE_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- SPRING_DATASOURCE_USERNAME=${POSTGRES_USER}
- SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
# JVM tuning
- JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseContainerSupport
- TZ=Asia/Seoul
depends_on:
db:
condition: service_healthy # Start only after DB passes health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s # Check every 30 seconds
timeout: 10s # Fail if no response within 10 seconds
retries: 3 # Mark unhealthy after 3 consecutive failures
start_period: 90s # Ignore failures for 90s after start (JVM warmup)
networks:
- frontend
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 1536M
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 3. PostgreSQL - Database
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
db:
image: postgres:16-alpine
container_name: postgres-db
expose:
- "5432" # No external exposure, backend network only
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- PGDATA=/var/lib/postgresql/data/pgdata
- TZ=Asia/Seoul
volumes:
- postgres-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro # Auto-run initialization SQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Network Definitions
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
networks:
frontend:
driver: bridge
name: app-frontend
backend:
driver: bridge
name: app-backend
internal: true # Completely blocks external internet access
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Volume Definitions
# ββββββββββββββββββββββββββββββββββββββββββββββββββββ
volumes:
postgres-data:
name: myapp-postgres-data
nginx-logs:
name: myapp-nginx-logs
app-logs:
name: myapp-app-logs
static-files:
name: myapp-static-files
certbot-conf:
name: myapp-certbot-conf
certbot-www:
name: myapp-certbot-www
Nginx Configuration File (Volume Injection Pattern)β
Mount configuration files via bind mount so Nginx settings can be changed without rebuilding the container image.
# nginx/conf.d/app.conf
upstream spring_app {
server app:8080; # Docker DNS: service name 'app' is auto-resolved
keepalive 32;
}
server {
listen 80;
server_name example.com www.example.com;
# Let's Encrypt certificate issuance path
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# HTTP to HTTPS redirect
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Directly serve static files
location /static/ {
root /usr/share/nginx/html;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Proxy API / dynamic requests
location / {
proxy_pass http://spring_app;
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
}
}
Key Docker Compose Commandsβ
Starting and Managing Servicesβ
# Run all services in background
docker compose up -d
# Run specific services only
docker compose up -d nginx app
# Rebuild images and run (when code changes)
docker compose up -d --build
# Force recreate (ignore cache)
docker compose up -d --force-recreate
Viewing Logsβ
# Real-time logs for all services
docker compose logs -f
# Logs for a specific service
docker compose logs -f app
# Show last 100 lines only
docker compose logs --tail=100 nginx
# Include timestamps
docker compose logs -f -t db
Status Checksβ
# Service status and port information
docker compose ps
# Detailed info (includes health check status)
docker compose ps --format json | jq .
# Resource usage
docker stats $(docker compose ps -q)
Stopping and Cleaning Upβ
# Stop services (remove containers, keep volumes)
docker compose down
# Delete everything including volumes (data reset, caution!)
docker compose down -v
# Delete everything including images
docker compose down --rmi all -v
Accessing Container Internalsβ
# Shell access to Nginx container
docker compose exec nginx sh
# Shell access to Spring Boot container
docker compose exec app bash
# PostgreSQL access
docker compose exec db psql -U ${POSTGRES_USER} -d ${POSTGRES_DB}
Scaling Up Servicesβ
# Scale app service to 3 instances
docker compose up -d --scale app=3
# Verify
docker compose ps
Important notes when scaling up:
- Fixed
container_nameprevents scale-up β must be removed - Nginx
upstreamblock automatically distributes via DNS round-robin - Direct
portsmapping causes port conflicts β useexposeonly
# Modified app service for scale-up (remove container_name, ports)
app:
build: .
expose:
- "8080"
# container_name: spring-app β Remove this line
Health Check Configuration Deep Diveβ
Health checks verify not just whether a container is running, but whether it's actually functioning correctly. Used with depends_on's condition: service_healthy, the next service only starts after the dependent service is fully ready.
# Spring Boot health check (using Spring Actuator)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s # Ignore failures during this period after container start
# PostgreSQL health check
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
# Nginx health check
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 60s
timeout: 10s
retries: 3
Health check status appears as (healthy), (unhealthy), or (starting) in docker compose ps.
Expert Tipsβ
1. Pin image versions
Specify concrete versions like image: nginx:1.25-alpine instead of image: nginx:latest to prevent unexpected failures from unplanned updates.
2. Minimize build context with .dockerignore
# .dockerignore
target/
*.log
.git/
.env
node_modules/
3. Validate Nginx config syntax
# Check config file syntax after changes
docker compose exec nginx nginx -t
# Zero-downtime reload if syntax is valid
docker compose exec nginx nginx -s reload
4. Environment variable changes require container recreation
When environment variables change, container recreation is needed. docker compose up -d automatically detects changes and recreates only the affected services.