Skip to main content

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.

Categorydocker-compose (v1)docker compose (v2)
InstallationSeparate Python packageBuilt-in Docker Engine plugin
Commanddocker-compose updocker compose up
File namedocker-compose.ymldocker-compose.yml (same)
Support statusEOL July 2023Currently officially supported
PerformanceRelatively slowerFaster 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_name prevents scale-up β†’ must be removed
  • Nginx upstream block automatically distributes via DNS round-robin
  • Direct ports mapping causes port conflicts β†’ use expose only
# 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.