화. 8월 12th, 2025

G:

Have you ever struggled with managing multiple Docker containers for your applications? 🤔 Running docker run commands for each service quickly becomes cumbersome and error-prone. Enter Docker Compose! This powerful tool simplifies the orchestration of multi-container Docker applications by letting you define all services, networks, and volumes in a single, easy-to-read YML file. By the end of this guide, you’ll not only understand the core concepts of Docker Compose YML but also be able to write your own production-ready configurations with practical examples! 🚀 Let’s dive in and master Docker Compose together!

What is Docker Compose? 🎯

Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. Think of it as a blueprint for your entire application stack, making it portable and easy to reproduce.

Instead of manually typing multiple `docker run` commands for your web server, database, and backend API, Docker Compose allows you to define them all in one `docker-compose.yml` file. This file then serves as the single source of truth for your application’s environment. ✨

Why Use Docker Compose? The Benefits 💡

  • Simplified Setup: Define your entire application stack in one file, reducing manual configuration.
  • Reproducibility: Ensure that your application runs consistently across different environments (development, testing, production).
  • Portability: Share your `docker-compose.yml` file with teammates, and they can spin up the full environment instantly.
  • Isolation: Each service runs in its own container, keeping dependencies separate and preventing conflicts.
  • Scalability: Easily scale services up or down (though for true horizontal scaling and orchestration, Kubernetes is often used in production).
  • Version Control: Manage your application’s infrastructure configuration using Git, just like your code.

Understanding the `docker-compose.yml` File Structure 📁

The `docker-compose.yml` file is the heart of your Docker Compose setup. It’s written in YAML (Yet Another Markup Language), which is human-readable and easy to parse. Let’s look at its fundamental structure.

Top-Level Keys

A typical `docker-compose.yml` file contains several top-level keys:

  • <b>version</b>: Specifies the Compose file format version. Newer versions offer more features. (e.g., `3.8`, `3.9`)
  • <b>services</b>: Defines the individual containers (services) that make up your application. Each service runs in an isolated environment.
  • <b>networks</b>: Defines custom networks for your services to communicate over.
  • <b>volumes</b>: Defines named volumes for persistent data storage.
  • <b>configs</b> (v3.5+): External configuration sources.
  • <b>secrets</b> (v3.5+): Sensitive data (passwords, API keys) that should not be exposed.

version: '3.8' # Docker Compose file format version

services:
  web: # Service name 1
    # Service configuration for 'web'

  db: # Service name 2
    # Service configuration for 'db'

networks:
  app-network:
    # Network configuration

volumes:
  db-data:
    # Volume configuration

Core YML Directives Explained ⚙️

Inside the `services` section, you define individual services, each with its own set of directives. These directives configure how each container is built, runs, and interacts with other services.

Common Service Directives:

  • <b>image</b>:

    Specifies the Docker image to use for the service. Docker will pull this image from Docker Hub if not found locally.

    
            services:
              web:
                image: nginx:latest # Pulls the latest Nginx image
            
  • <b>build</b>:

    Instead of pulling an image, `build` tells Docker Compose to build an image from a Dockerfile. You can specify the context path and optional `dockerfile` name.

    
            services:
              api:
                build: ./api # Builds from Dockerfile in the 'api' directory
                # OR with custom Dockerfile name:
                # build:
                #   context: ./api
                #   dockerfile: Dockerfile.prod
            
  • <b>ports</b>:

    Maps ports from the host machine to the container. Format: `HOST_PORT:CONTAINER_PORT`.

    
            services:
              web:
                image: nginx:latest
                ports:
                  - "80:80"     # Host port 80 maps to container port 80
                  - "443:443"   # Host port 443 maps to container port 443
            

    🚨 **Important:** If you only need container-to-container communication, use `expose` instead of `ports` to avoid exposing the port to the host machine.

  • <b>volumes</b>:

    Mounts host paths or named volumes into the container for persistent storage or sharing code. Format: `HOST_PATH:CONTAINER_PATH` or `VOLUME_NAME:CONTAINER_PATH`.

    
            services:
              web:
                image: nginx:latest
                volumes:
                  - ./nginx.conf:/etc/nginx/nginx.conf # Mounts a config file
                  - ./website:/usr/share/nginx/html    # Mounts a local directory
              db:
                image: postgres:13
                volumes:
                  - db_data:/var/lib/postgresql/data   # Uses a named volume
            volumes:
              db_data: # Defines the named volume
            
  • <b>environment</b>:

    Sets environment variables inside the container. Useful for database credentials, API keys, etc.

    
            services:
              api:
                image: myapp/api
                environment:
                  - NODE_ENV=production
                  - DATABASE_URL=postgres://user:password@db:5432/mydb
                  # OR
                  # DATABASE_URL: ${DB_URL} # From .env file or host env
            
  • <b>depends_on</b>:

    Expresses dependency between services. Compose will start services in dependency order. Note: This does *not* wait for the dependent service to be “ready” (e.g., database fully started and listening), only for its container to be started.

    
            services:
              web:
                image: nginx
                depends_on:
                  - api # Web service starts after API service
              api:
                image: myapp/api
                depends_on:
                  - db # API service starts after DB service
              db:
                image: postgres
            
  • <b>networks</b>:

    Connects a service to specific networks. Services on the same network can communicate by service name.

    
            services:
              web:
                image: nginx
                networks:
                  - frontend_network
              api:
                image: myapp/api
                networks:
                  - frontend_network
                  - backend_network
              db:
                image: postgres
                networks:
                  - backend_network
            networks:
              frontend_network:
              backend_network:
            
  • <b>command</b>:

    Overrides the default command specified by the image’s Dockerfile `CMD` instruction.

    
            services:
              worker:
                image: myapp/worker
                command: python worker.py --verbose # Runs a specific command
            
  • <b>restart</b>:

    Defines the container’s restart policy. Common values: `no`, `on-failure`, `always`, `unless-stopped`.

    
            services:
              web:
                image: nginx
                restart: always # Always restart if the container stops
            

Practical Examples: Building with Docker Compose 🛠️

Example 1: A Simple Web Application (Nginx + Python Flask)

Let’s create a basic web application with an Nginx web server serving static files and a Python Flask application for dynamic content.

Project Structure:


my-flask-app/
├── nginx.conf
├── Dockerfile # For Flask app
├── app.py
└── docker-compose.yml

app.py (Flask App):


from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from Flask! Running on port {os.getenv('FLASK_PORT', '5000')}."

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=os.getenv('FLASK_PORT', '5000'))

Dockerfile (for Flask):


FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_PORT=5000
EXPOSE 5000
CMD ["python", "app.py"]

Note: You’ll also need a `requirements.txt` file with `Flask`.

nginx.conf:


events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://flask_app:5000; # Requests go to the Flask app service
            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;
        }

        # Example for serving static files (if any)
        # location /static/ {
        #     alias /usr/share/nginx/html/static/;
        #     expires 30d;
        # }
    }
}

docker-compose.yml:


version: '3.8'

services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80" # Map host port 80 to container port 80
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro # Mount Nginx config file as read-only
    depends_on:
      - flask_app # Start Nginx after Flask app
    networks:
      - app_network

  flask_app:
    build: . # Build from current directory (where Dockerfile is)
    volumes:
      - .:/app # Mount current directory to /app for live changes (dev)
    environment:
      FLASK_ENV: development
      FLASK_PORT: 5000
    networks:
      - app_network

networks:
  app_network:
    driver: bridge # Default bridge network

To run this example: navigate to the `my-flask-app` directory in your terminal and run `docker-compose up –build -d`. Then open your browser to `http://localhost`. You should see “Hello from Flask!”. 🎉

Example 2: WordPress with MySQL 🌐

This is a classic example showcasing a web application with a dedicated database service.


version: '3.8'

services:
  wordpress:
    image: wordpress:latest
    ports:
      - "8000:80" # Access WordPress on host port 8000
    environment:
      WORDPRESS_DB_HOST: db:3306 # 'db' is the service name for MySQL
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: supersecretpassword
      WORDPRESS_DB_NAME: wordpress_db
    volumes:
      - wordpress_data:/var/www/html # Persistent storage for WordPress files
    depends_on:
      - db
    networks:
      - wp_network

  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: wordpress_db
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: supersecretpassword
    volumes:
      - db_data:/var/lib/mysql # Persistent storage for database data
    networks:
      - wp_network
    restart: always # Ensure MySQL restarts if it crashes

volumes:
  wordpress_data: # Define named volume for WordPress files
  db_data:        # Define named volume for MySQL data

networks:
  wp_network:
    driver: bridge # Custom network for these services

To run: `docker-compose up -d`. Then visit `http://localhost:8000` to complete the WordPress installation. 🚀

Best Practices & Tips for Docker Compose YML ✅

  • Use Specific Image Versions: Instead of `image: nginx:latest`, use `image: nginx:1.21.6` for reproducibility and stability.
  • Separate Development and Production: Use multiple Compose files (e.g., `docker-compose.yml` for production, `docker-compose.override.yml` for development overrides) to manage different environments.
    
            # To run development:
            docker-compose -f docker-compose.yml -f docker-compose.override.yml up
            
  • Utilize `.env` Files for Secrets: Store sensitive information like passwords in a `.env` file (which is excluded from version control) and reference them in your `docker-compose.yml` using `${VARIABLE_NAME}`.
    
            # .env file
            DB_PASSWORD=your_secure_password
    
            # docker-compose.yml
            environment:
              DB_PASSWORD: ${DB_PASSWORD}
            
  • Health Checks: For more robust `depends_on`, add `healthcheck` configurations to your services. This ensures a service is truly ready before dependent services try to connect.
    
            services:
              db:
                image: postgres
                healthcheck:
                  test: ["CMD-SHELL", "pg_isready -U postgres"]
                  interval: 5s
                  timeout: 5s
                  retries: 5
              app:
                image: my_app
                depends_on:
                  db:
                    condition: service_healthy # Wait until 'db' reports healthy
            
  • Named Volumes: Always prefer named volumes over host mounts for persistent data, especially in production, as they are managed by Docker and easier to back up.
  • Clear Naming Conventions: Use descriptive names for your services, networks, and volumes to improve readability.

Common Pitfalls & Troubleshooting ⚠️

  • Port Conflicts: “Bind for 0.0.0.0:80 failed: port is already allocated.” This means another process on your host is using the specified host port. Change the host port in `ports` mapping (e.g., `8080:80`).
  • Service Not Ready: If your app tries to connect to the database before the database container has fully started and initialized, you’ll get connection errors. Use `depends_on` with `condition: service_healthy` or implement retry logic in your application.
  • Incorrect Paths for Volumes/Build Context: Double-check your relative and absolute paths for `volumes` and `build` contexts.
  • YAML Syntax Errors: YAML is whitespace-sensitive. Even a single incorrect indentation can break your file. Use a YAML linter or a good IDE with YAML support.
  • Network Issues: If services can’t communicate, ensure they are on the same network. By default, Compose puts services in a single `default` network, but explicit `networks` configuration gives more control.
  • Debugging Containers: Use `docker-compose logs ` to view container logs. Use `docker-compose exec bash` (or `sh`) to get a shell inside a running container for inspection.

Conclusion 🎉

Mastering Docker Compose file writing is an essential skill for any developer working with containerized applications. By defining your application stack in a clear, version-controlled YML file, you gain immense benefits in terms of development speed, reproducibility, and deployment simplicity. We’ve covered the core structure, essential directives, practical examples, and crucial best practices to help you write robust and efficient Docker Compose configurations.

Now it’s your turn! Start experimenting with your own `docker-compose.yml` files. Begin with a simple project, add more services, integrate databases, and explore more advanced directives. The best way to learn is by doing! Happy containerizing! 🚀

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다