← Back to Questions
Docker

Production-ready Docker Compose best practices

Learn Production-ready Docker Compose best practices with simple explanations, real-time examples, interview tips and practical use cases.

Production-Ready Docker Compose Best Practices

Production-ready Docker Compose best practices are the guidelines used to run multi-container applications securely, reliably, consistently, and maintainably in real environments.

Docker Compose is commonly used for local development, staging, testing, small production deployments, monitoring stacks, and single-server microservices platforms. When used carefully, it can support real production workloads on VPS, AWS EC2, Azure VM, Google Compute Engine, on-prem servers, and internal enterprise environments.

Simple Definition: A production-ready Docker Compose setup should use fixed image versions, environment variables, health checks, restart policies, named volumes, isolated networks, logging limits, resource limits, secrets handling, backups, and monitoring.

Why Docker Compose Best Practices Matter

A Docker Compose file that works locally may fail in production if it is not designed properly.

Common production problems include:

  • Containers not restarting after crash
  • Database data loss
  • Hardcoded passwords
  • Uncontrolled log growth
  • Port conflicts
  • Unhealthy containers still receiving traffic
  • Security risks due to exposed services
  • Slow troubleshooting because of poor logging
β€œDocker Compose is simple, but production Compose must be disciplined.”

Production Docker Compose Architecture

Users
  |
Nginx / Load Balancer
  |
API Gateway
  |
+----------------------+----------------------+
|                      |                      |
Portfolio Service   Interview Service     Payment Service
|                      |                      |
+----------+-----------+----------+-----------+
           |
        MySQL
           |
        Redis

Monitoring:
Prometheus + Grafana + Loki + Promtail
    

Core Best Practices

  1. Use fixed image tags
  2. Use environment variables and .env files
  3. Never hardcode secrets
  4. Use named volumes for persistent data
  5. Use isolated networks
  6. Add health checks
  7. Use restart policies
  8. Configure log rotation
  9. Limit CPU and memory
  10. Back up volumes regularly
  11. Expose only required ports
  12. Use monitoring and alerting

1. Use Fixed Image Versions

Avoid using latest in production because it can change unexpectedly.

Bad Example

image: mysql:latest
    

Good Example

image: mysql:8.0
image: redis:7.2
image: grafana/grafana:10.4.3
    

Fixed versions make rollback, debugging, and deployment consistency easier.

2. Use .env File for Configuration

Keep environment-specific values outside the Compose file.

.env Example

SPRING_PROFILES_ACTIVE=prod
MYSQL_ROOT_PASSWORD=change-this-password
DB_USERNAME=root
DB_PASSWORD=change-this-password
RAZORPAY_KEY_ID=your-key
RAZORPAY_KEY_SECRET=your-secret
    

docker-compose.yml Usage

environment:
  SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
  DB_USERNAME: ${DB_USERNAME}
  DB_PASSWORD: ${DB_PASSWORD}
    

In production, store the .env file securely and never commit it to GitHub.

3. Never Hardcode Secrets

Do not write database passwords, payment keys, JWT secrets, OAuth secrets, or internal admin secrets directly in docker-compose.yml.

Bad Example

environment:
  DB_PASSWORD: root
  JWT_SECRET: mysecret
    

Better Example

environment:
  DB_PASSWORD: ${DB_PASSWORD}
  JWT_SECRET: ${JWT_SECRET}
    

For stronger security, use Docker secrets, Vault, AWS Secrets Manager, Azure Key Vault, or Kubernetes Secrets when moving to orchestrated platforms.

4. Use Named Volumes for Persistent Data

Databases and uploaded files must not depend only on container writable layers.

Good Example

services:
  mysql:
    image: mysql:8.0
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:
    

Named volumes survive container deletion and recreation.

5. Use Bind Mounts Carefully

Bind mounts are useful when the host path must be controlled directly, such as uploads or Nginx configuration.

Example

volumes:
  - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
  - ./uploads:/uploads
    

Use :ro for read-only mounts wherever possible.

6. Use Separate Networks

Do not put every service on one open network when you can separate traffic.

Recommended Network Design

frontend-network:
  Nginx, API Gateway

backend-network:
  API Gateway, Microservices

data-network:
  Microservices, MySQL, Redis
    

Example

networks:
  frontend:
  backend:
  data:
    

This reduces unnecessary access between containers.

7. Expose Only Required Ports

In production, avoid exposing databases directly to the public internet.

Bad Example

mysql:
  ports:
    - "3306:3306"
    

Better Example

mysql:
  expose:
    - "3306"
    

Use ports only for services that need host/public access. Use expose for internal container-to-container communication.

8. Add Health Checks

Health checks help Docker detect whether a container is truly healthy, not just running.

Spring Boot Health Check

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 60s
    

MySQL Health Check

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  interval: 10s
  timeout: 5s
  retries: 5
    

Health checks are very important for production troubleshooting and controlled startup.

9. Understand depends_on Limitation

depends_on controls container startup order, but it does not guarantee application readiness.

depends_on:
  - mysql
    

This means MySQL container starts before the app, but MySQL may still not be ready to accept connections.

Better Approach

  • Add health checks
  • Add retry logic in application
  • Use connection pool retry configuration
  • Use readiness checks in orchestrated environments

10. Use Restart Policies

Restart policies help recover containers after crashes or server reboot.

Recommended

restart: unless-stopped
    

This restarts the service automatically unless it was manually stopped.

11. Configure Log Rotation

Docker logs can consume huge disk space if not limited.

Per-Service Logging Example

logging:
  driver: "json-file"
  options:
    max-size: "100m"
    max-file: "5"
    

This prevents one noisy service from filling your disk.

12. Use Centralized Logging

For production, forward logs to tools like:

  • Loki + Promtail
  • ELK Stack
  • OpenSearch
  • AWS CloudWatch

Logging Flow

Containers
  |
Docker Logs
  |
Promtail
  |
Loki
  |
Grafana
    

13. Set CPU and Memory Limits

One service should not consume all server resources.

Compose Resource Example

deploy:
  resources:
    limits:
      cpus: "1.0"
      memory: 768M
    reservations:
      memory: 256M
    

Resource control is very important for APIs, databases, background jobs, and search services.

14. Use Non-Root Containers

Your application images should run as a non-root user.

Dockerfile Example

RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
    

Docker Compose should run production images that already follow this rule.

15. Use Read-Only Filesystems Where Possible

For stateless services, use read-only root filesystems and mount only required writable paths.

read_only: true
tmpfs:
  - /tmp
    

This reduces attack surface and accidental filesystem changes.

16. Use Profiles for Optional Services

Compose profiles allow optional tools like monitoring or debugging to be enabled only when needed.

profiles:
  - monitoring
    

Run with Profile

docker compose --profile monitoring up -d
    

17. Keep Compose Files Modular

Use separate files for different environments.

docker-compose.yml
docker-compose.override.yml
docker-compose.prod.yml
docker-compose.monitoring.yml
    

Run Production Compose

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
    

18. Use Image Registries

In production, do not build images manually on the server every time. Build in CI/CD and pull versioned images from a registry.

CI/CD Pipeline
  |
Build Docker Image
  |
Push to Registry
  |
Production Server Pulls Image
    

19. Use a Reverse Proxy

Put Nginx, Traefik, or another reverse proxy in front of backend services.

Internet
  |
Nginx
  |
API Gateway
  |
Microservices
    

Only the reverse proxy should expose HTTP/HTTPS publicly.

20. Secure Docker Socket

Avoid mounting Docker socket into application containers.

Dangerous

- /var/run/docker.sock:/var/run/docker.sock
    

Docker socket access can effectively provide root-level control over the host.

21. Use Backup Strategy for Volumes

Production volumes must be backed up regularly.

Volume Backup Example

docker run --rm \
  -v mysql-data:/volume \
  -v $(pwd):/backup \
  ubuntu \
  tar czf /backup/mysql-data-backup.tar.gz /volume
    

Database Dump Example

docker exec mysql \
  mysqldump -u root -p${MYSQL_ROOT_PASSWORD} portfolio_db \
  > portfolio_db_backup.sql
    

For databases, logical backups like mysqldump are often safer than only raw volume backups.

22. Add Monitoring

Production Compose should include or connect to monitoring.

Monitoring Stack

Prometheus
Grafana
Loki
Promtail
Node Exporter
cAdvisor
    

23. Validate Compose Before Deployment

Always validate final merged configuration before production deployment.

docker compose config
    

This helps detect invalid YAML, missing variables, and final resolved values.

24. Use Safe Deployment Process

Pull New Images
  |
Run docker compose config
  |
Backup Database/Volumes
  |
docker compose up -d
  |
Check Health
  |
Check Logs
  |
Rollback if Needed
    

25. Production-Ready Docker Compose Example

services:

  api-gateway:
    image: dhanishempower/api-gateway:1.0.0
    container_name: api-gateway
    restart: unless-stopped
    ports:
      - "9090:9090"
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
      PORTFOLIO_SERVICE_URL: http://portfolio-service:8080
      INTERVIEW_SERVICE_URL: http://interview-service:8082
      PAYMENT_SERVICE_URL: http://payment-service:8084
    depends_on:
      - portfolio-service
      - interview-service
      - payment-service
    networks:
      - backend
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"

  portfolio-service:
    image: dhanishempower/portfolio-service:1.0.0
    restart: unless-stopped
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
      DB_URL: jdbc:mysql://mysql:3306/portfolio_db
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
    expose:
      - "8080"
    depends_on:
      - mysql
    networks:
      - backend
      - data
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"

  interview-service:
    image: dhanishempower/interview-service:1.0.0
    restart: unless-stopped
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
      DB_URL: jdbc:mysql://mysql:3306/interview_db
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
    expose:
      - "8082"
    depends_on:
      - mysql
    networks:
      - backend
      - data

  payment-service:
    image: dhanishempower/payment-service:1.0.0
    restart: unless-stopped
    environment:
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
      DB_URL: jdbc:mysql://mysql:3306/payment_db
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      RAZORPAY_KEY_ID: ${RAZORPAY_KEY_ID}
      RAZORPAY_KEY_SECRET: ${RAZORPAY_KEY_SECRET}
    expose:
      - "8084"
    depends_on:
      - mysql
    networks:
      - backend
      - data

  mysql:
    image: mysql:8.0
    container_name: mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
    expose:
      - "3306"
    networks:
      - data
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"

  redis:
    image: redis:7.2
    restart: unless-stopped
    expose:
      - "6379"
    networks:
      - data

networks:
  backend:
  data:

volumes:
  mysql-data:
    

Production Checklist

[ ] Fixed image tags used
[ ] No secrets hardcoded
[ ] .env file secured
[ ] Databases use named volumes
[ ] Only required ports exposed
[ ] Internal services use expose
[ ] Health checks added
[ ] Restart policies added
[ ] Log rotation configured
[ ] Resource limits defined
[ ] Networks separated
[ ] Backups automated
[ ] Monitoring configured
[ ] docker compose config validated
    

Common Production Mistakes

  • Using latest image tags
  • Hardcoding passwords
  • Exposing MySQL publicly
  • No volume backup strategy
  • No log rotation
  • No health checks
  • No restart policy
  • Putting all services on one flat network

Interview Answer

Production-ready Docker Compose best practices include using fixed image versions, secure environment variables, named volumes for persistence, isolated networks, health checks, restart policies, log rotation, resource limits, monitoring, and regular backup strategies.

In production, Docker Compose files should avoid hardcoded secrets, avoid exposing databases publicly, use secure .env files, validate configuration with docker compose config, and keep services observable through logs, metrics, and health checks.

Quick Summary Table

Best Practice Why It Matters
Fixed image tags Predictable deployments
Named volumes Persistent data
Health checks Reliable startup and monitoring
Log rotation Prevents disk full issues
Network isolation Improves security
Backups Disaster recovery

Useful Internal Links

Final Conclusion

Docker Compose can be production-friendly when it is designed carefully with security, persistence, observability, reliability, and maintainability in mind.

For small-to-medium production systems, Docker Compose with Nginx, named volumes, health checks, restart policies, log rotation, monitoring, and automated backups can provide a stable deployment model. For large-scale enterprise systems, Kubernetes is usually the next step.

Why this Docker question is important?

This interview question helps candidates understand real-time backend development concepts, practical problem solving, coding fundamentals, system design basics and production-ready application behavior.

Practice this question carefully for Java backend roles, Spring Boot developer interviews, microservices interviews, company interviews and full-stack developer preparation.

About the Author

Naresh Kumar is a Senior Java Backend Engineer with experience building enterprise applications using Java, Spring Boot, Microservices, Docker, Kubernetes and Cloud technologies.