화. 8월 12th, 2025

G: Welcome, aspiring Docker maestros! 🐳 Have you ever felt overwhelmed trying to run multiple Docker containers that need to talk to each other? Maybe you’re building a web application with a frontend, a backend API, and a database, all running in separate containers. Manually starting, linking, and managing them can quickly turn into a tangled mess! 😵

Fear not, because this is where Docker Compose swoops in like a superhero! 🦸‍♂️ Docker Compose is a powerful tool that simplifies the definition and running of multi-container Docker applications. With a single configuration file and a single command, you can bring up an entire application stack. Ready to make your development life a whole lot easier? Let’s dive in! 🚀


💡 What is Docker Compose and Why Do You Need It?

Imagine your application isn’t just one piece of software, but a collection of services working together – like an orchestra! 🎶 You might have:

  • A web server: (e.g., Nginx, Apache) to serve your static files or act as a reverse proxy.
  • A backend API: (e.g., Node.js, Python Flask, Java Spring Boot) that handles business logic.
  • A database: (e.g., PostgreSQL, MySQL, MongoDB) to store your data.
  • A caching service: (e.g., Redis) for faster data retrieval.

Without Docker Compose, you’d need to:

  1. Pull each image.
  2. Run each container, carefully linking them with docker run --link or network aliases.
  3. Expose ports correctly.
  4. Manage volumes for persistent data.
  5. Set environment variables.

This quickly becomes cumbersome and error-prone. Docker Compose solves this by allowing you to define your entire application stack in a single YAML file (docker-compose.yml). This file acts as a blueprint, telling Docker Compose exactly how to build, configure, and connect your services.

Key Benefits:

  • Simplicity: Define complex multi-container apps in a single file.
  • Reproducibility: Ensure everyone on your team (and your CI/CD pipeline) runs the exact same environment.
  • Isolation: Each service runs in its own isolated container.
  • Portability: Your application works the same way on any machine with Docker installed.
  • Development Workflow: Start, stop, and rebuild your entire stack with one command.

🛠️ Prerequisites Before We Start

Before we embark on our Docker Compose journey, make sure you have the following installed:

  1. Docker Desktop: This includes Docker Engine, Docker CLI, and Docker Compose (often docker compose as part of the main Docker CLI, or docker-compose as a standalone binary depending on your version).
  2. Basic understanding of Docker concepts:
    • Images: Read-only templates used to create containers.
    • Containers: Runnable instances of images.
    • Volumes: For persisting data outside containers.
    • Ports: For mapping container ports to host ports.

If you’re new to Docker, a quick tutorial on these basics will be immensely helpful!


📖 Core Concepts of docker-compose.yml

The heart of Docker Compose is the docker-compose.yml file. It’s a YAML file that defines your services, networks, and volumes. Let’s break down its most common sections:

  • version: Specifies the Compose file format version (e.g., ‘3.8’). Higher versions often introduce new features. Always good to use a recent stable one.
  • services: This is where you define each individual component (container) of your application. Each service will typically map to one container.
    • image: The Docker image to use (e.g., nginx:latest, postgres:13).
    • build: Instead of an existing image, specify a path to a directory containing a Dockerfile. Docker Compose will build the image from scratch.
    • ports: Maps ports from the host machine to the container (e.g., "80:80" maps host port 80 to container port 80).
    • volumes: Mounts paths from your host machine into the container, or creates named volumes for persistent data (e.g., ./app:/usr/src/app or db_data:/var/lib/postgresql/data).
    • environment: Sets environment variables inside the container (e.g., DB_HOST: db).
    • depends_on: Specifies dependencies between services. This helps ensure services start in the correct order (e.g., web depends on db).
    • networks: Defines which networks a service should connect to.
    • container_name: Assigns a specific name to the container instead of a random one.
  • networks: Defines custom networks for your services to communicate on. Services on the same network can communicate by their service names.
  • volumes: Declares named volumes for data persistence that can be used by multiple services.

📝 Step-by-Step: Your First Docker Compose Project

Let’s build a simple multi-container application: a static Nginx web server.

Step 1: Create Your Project Directory

First, create a new directory for your project and navigate into it.

mkdir my-nginx-app
cd my-nginx-app

Step 2: Create a Simple HTML File

Inside my-nginx-app, create a subdirectory called html and put a simple index.html file inside it. This will be served by Nginx.

mkdir html
# Create html/index.html

html/index.html:

<!DOCTYPE html>
<html>
<head>

<title>Hello Docker Compose!</title>

<style>
        body { font-family: sans-serif; text-align: center; margin-top: 50px; background-color: #f0f8ff; color: #333; }
        h1 { color: #007bff; }
        p { font-size: 1.2em; }
    </style>
</head>
<body>

<h1>🎉 Hello from Docker Compose! 🎉</h1>

<p>This page is served by Nginx in a Docker container, orchestrated by Docker Compose.</p>

<p>Isn't this amazing? 🐳🚀</p>
</body>
</html>

Step 3: Create Your docker-compose.yml File

Now, in the root of your my-nginx-app directory, create a file named docker-compose.yml.

docker-compose.yml:

# Specify the Compose file format version
version: '3.8'

# Define the services (containers) for your application
services:
  # Define a service named 'web'
  web:
    # Use the official Nginx image from Docker Hub
    image: nginx:latest
    # Map port 80 on your host machine to port 80 inside the container
    ports:
      - "80:80"
    # Mount the 'html' directory from your host to Nginx's default web directory
    # This means Nginx will serve the index.html we created!
    volumes:
      - ./html:/usr/share/nginx/html
    # Restart the container if it stops for any reason, unless explicitly stopped
    restart: always

Explanation of the docker-compose.yml:

  • version: '3.8': We’re using Compose file format version 3.8.
  • services:: This is where we list all the independent pieces of our application.
  • web:: We’ve defined one service and named it web. This name is used internally by Docker Compose.
  • image: nginx:latest: We tell Docker Compose to use the nginx:latest Docker image. If it’s not on your machine, Docker will pull it from Docker Hub.
  • ports: - "80:80": This is crucial! It maps port 80 on your host machine to port 80 inside the web container. So, when you visit http://localhost:80 (or just http://localhost), your request goes to the Nginx server running inside the container.
  • volumes: - ./html:/usr/share/nginx/html: This creates a bind mount. It takes the html directory from your host machine (where docker-compose.yml is located) and mounts it directly into the /usr/share/nginx/html directory inside the Nginx container. This is where Nginx looks for web files by default. Any changes you make to html/index.html on your host will instantly reflect inside the container! ✨
  • restart: always: This ensures that if the Nginx container crashes or your Docker daemon restarts, the web service will automatically restart.

Step 4: Run Your Docker Compose Application

Now for the magic command! Make sure you are in the my-nginx-app directory (where docker-compose.yml is located).

docker compose up -d

Let’s break down this command:

  • docker compose: This is the command to invoke Docker Compose. (Older versions might use docker-compose).
  • up: This command builds (if build is specified) and starts all the services defined in your docker-compose.yml file.
  • -d: This flag stands for “detached mode.” It runs the containers in the background, so your terminal prompt is returned to you. If you omit -d, you’ll see the logs of all containers in your terminal.

You should see output similar to this:

[+] Running 1/1
 ⠿ Container my-nginx-app-web-1  Started

Step 5: Verify Your Application

Open your web browser and navigate to http://localhost.

🎉 Voila! You should see your “Hello from Docker Compose!” message. Your Nginx web server is running in a Docker container, orchestrated effortlessly by Docker Compose!

Step 6: Stop and Clean Up Your Application

When you’re done, you can stop and remove your containers, networks, and volumes (if defined) using one simple command:

docker compose down

This command:

  • Stops all running containers for the project.
  • Removes the containers.
  • Removes any networks created by up.
  • (If you used named volumes and passed -v or --volumes flag, it would remove those too).

You should see output like:

[+] Running 2/2
 ⠿ Container my-nginx-app-web-1  Removed
 ⠿ Network my-nginx-app_default  Removed

🚀 A More Complex Example: Web App with Database and Caching

Let’s level up! We’ll create a simple Flask (Python web framework) application that connects to a PostgreSQL database and uses Redis for caching. This will showcase build, depends_on, networks, volumes, and environment variables.

Project Structure:

my-web-app/
├── app/
│   ├── app.py
│   └── Dockerfile
│   └── requirements.txt
├── docker-compose.yml

app/requirements.txt:

Flask
psycopg2-binary
redis

app/app.py:

from flask import Flask, jsonify
import psycopg2
import redis
import os
import time

app = Flask(__name__)

# Wait for DB and Redis to be ready (simple retry logic)
def wait_for_services():
    # Wait for PostgreSQL
    db_ready = False
    while not db_ready:
        try:
            conn = psycopg2.connect(
                host=os.environ.get("DB_HOST", "db"),
                database=os.environ.get("POSTGRES_DB"),
                user=os.environ.get("POSTGRES_USER"),
                password=os.environ.get("POSTGRES_PASSWORD")
            )
            conn.close()
            db_ready = True
            print("PostgreSQL is ready!")
        except Exception as e:
            print(f"PostgreSQL not ready, retrying... ({e})")
            time.sleep(2)

    # Wait for Redis
    redis_ready = False
    while not redis_ready:
        try:
            r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379, decode_responses=True)
            r.ping()
            redis_ready = True
            print("Redis is ready!")
        except Exception as e:
            print(f"Redis not ready, retrying... ({e})")
            time.sleep(2)

wait_for_services()

# Connect to PostgreSQL
conn = psycopg2.connect(
    host=os.environ.get("DB_HOST", "db"),
    database=os.environ.get("POSTGRES_DB"),
    user=os.environ.get("POSTGRES_USER"),
    password=os.environ.get("POSTGRES_PASSWORD")
)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, text VARCHAR(255))")
conn.commit()
cursor.close()
conn.close()

# Connect to Redis
r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379, decode_responses=True)

@app.route('/')
def hello():
    return "Hello from Flask! Check /info for details on DB and Redis."

@app.route('/info')
def info():
    db_status = "Disconnected"
    redis_status = "Disconnected"
    message_count = 0
    cached_message = r.get("my_cached_message")

    try:
        conn = psycopg2.connect(
            host=os.environ.get("DB_HOST", "db"),
            database=os.environ.get("POSTGRES_DB"),
            user=os.environ.get("POSTGRES_USER"),
            password=os.environ.get("POSTGRES_PASSWORD")
        )
        cursor = conn.cursor()
        cursor.execute("SELECT COUNT(*) FROM messages")
        message_count = cursor.fetchone()[0]
        db_status = "Connected"
        conn.close()
    except Exception as e:
        db_status = f"Failed to connect: {e}"

    try:
        r.ping()
        redis_status = "Connected"
        r.set("my_cached_message", "This is a cached message!", ex=60) # Cache for 60 seconds
    except Exception as e:
        redis_status = f"Failed to connect: {e}"

    return jsonify({
        "message": "Application Info",
        "database_status": db_status,
        "database_message_count": message_count,
        "redis_status": redis_status,
        "cached_message": cached_message if cached_message else "No message cached yet, or expired."
    })

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

(Note: The wait_for_services function is a simple retry mechanism. In production, you’d use more robust health checks or Docker’s healthcheck feature.)

app/Dockerfile:

# Use a lightweight Python base image
FROM python:3.9-slim-buster

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the Flask application
COPY . .

# Expose the port the Flask app runs on
EXPOSE 5000

# Command to run the Flask application
CMD ["python", "app.py"]

docker-compose.yml:

version: '3.8'

services:
  # 1. Flask Application Service
  web:
    build: ./app # Build image from Dockerfile in the 'app' directory
    ports:
      - "5000:5000" # Map host port 5000 to container port 5000
    environment: # Environment variables for the Flask app
      - DB_HOST=db # Hostname for the database (service name 'db')
      - REDIS_HOST=redis # Hostname for Redis (service name 'redis')
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    depends_on: # Ensures 'db' and 'redis' start before 'web'
      - db
      - redis
    volumes: # Mounts your local app code into the container for live updates (during dev)
      - ./app:/app
    networks: # Connects to the custom 'app-network'
      - app-network
    restart: on-failure # Restart if container exits with a non-zero status

  # 2. PostgreSQL Database Service
  db:
    image: postgres:13 # Use a specific version of PostgreSQL
    environment: # Database environment variables (important for Postgres image)
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    volumes: # Persistent volume for database data
      - db_data:/var/lib/postgresql/data
    networks: # Connects to the custom 'app-network'
      - app-network
    restart: always

  # 3. Redis Caching Service
  redis:
    image: redis:latest # Use the latest Redis image
    networks: # Connects to the custom 'app-network'
      - app-network
    restart: always

# Define custom networks
networks:
  app-network:
    driver: bridge # Default driver for inter-container communication

# Define named volumes for data persistence
volumes:
  db_data: # Data for the PostgreSQL database

Explanation of this Complex docker-compose.yml:

  • web service:
    • build: ./app: Instead of image, Docker Compose will build a Docker image from the Dockerfile located in the ./app directory. This is great for your own custom applications.
    • ports: - "5000:5000": Exposes the Flask app on your host machine.
    • environment: Crucially passes database and Redis connection details as environment variables. Notice DB_HOST and REDIS_HOST point to the service names (db and redis) – Docker Compose handles internal DNS resolution for services on the same network!
    • depends_on: - db - redis: This is a hint to Docker Compose that the web service needs db and redis to be started before web attempts to start. (Important: This only ensures start order, not readiness. For true readiness, you’d use health checks or retry logic within your app, like the wait_for_services function in app.py).
    • volumes: - ./app:/app: Mounts your local app directory into the container. This is fantastic for development, as changes to your app.py or Dockerfile will immediately reflect in the running container without rebuilding the image (unless Dockerfile is changed).
    • networks: - app-network: Explicitly puts this service on our custom app-network.
  • db service:
    • image: postgres:13: Uses the official PostgreSQL image.
    • environment: Sets the necessary environment variables for PostgreSQL to initialize the database and user. These are specific to the postgres image.
    • volumes: - db_data:/var/lib/postgresql/data: This uses a named volume called db_data. Named volumes are managed by Docker and are the recommended way to persist data. Even if you docker compose down, db_data will persist unless you explicitly remove it with docker compose down --volumes.
    • networks: - app-network: Connects to our custom network.
  • redis service:
    • image: redis:latest: Uses the official Redis image.
    • networks: - app-network: Connects to our custom network.
  • networks: section:
    • app-network: driver: bridge: Defines a custom bridge network named app-network. All services connected to this network can communicate with each other using their service names. This provides better isolation and organization than the default network.
  • volumes: section:
    • db_data:: Declares the named volume db_data which is used by the db service.

Running This Complex App:

  1. Create the my-web-app directory and subdirectories as described.

  2. Save the app.py, Dockerfile, requirements.txt, and docker-compose.yml files in their respective locations.

  3. Navigate to the my-web-app directory in your terminal.

  4. Run:

    docker compose up -d --build

    The --build flag forces Docker Compose to rebuild images that have a build instruction (like our web service) even if an image with the same name already exists. This is useful if you change your Dockerfile.

  5. Open your browser to http://localhost:5000. You should see “Hello from Flask! Check /info for details on DB and Redis.”

  6. Now go to http://localhost:5000/info. You should see a JSON response indicating the status of your database and Redis connection, and the number of messages. Try refreshing a few times and observe the cached message!

  7. When done, remember to clean up:

    docker compose down

    If you want to remove the database data volume as well (meaning your database will be empty next time you up), add the -v flag:

    docker compose down -v

🔍 Troubleshooting Common Issues

Even with the best guides, things can go wrong. Here are some common troubleshooting tips:

  • Containers not starting/crashing immediately:
    • Check logs: docker compose logs [service_name] (e.g., docker compose logs web). This is your best friend! It will show you what’s happening inside the container.
    • Remove -d: Run docker compose up without -d to see real-time logs directly in your terminal.
    • Check docker compose ps: See the status of your services. Are they Up, Exit 0, or Exit 1?
  • Port conflicts: “Port already in use” error.
    • Make sure no other application on your host is using the same port you’re trying to map (e.g., if you have another web server on port 80).
    • Change the host port mapping (e.g., "8080:80" instead of "80:80").
  • Service not found/cannot connect:
    • Ensure all services are on the same network (implicitly or explicitly).
    • Make sure you’re using the correct service_name as the hostname within your application code (e.g., DB_HOST=db).
  • Missing files or wrong paths:
    • Double-check your volumes paths. . usually refers to the directory where docker-compose.yml is located.
    • Make sure the file exists in the correct location relative to the container’s path.
  • Build issues (build context):
    • Ensure your Dockerfile is valid and requirements.txt (or similar) is present if your Dockerfile expects it.
    • Run docker compose build [service_name] to build a specific service and see detailed output.

🌟 Best Practices for Docker Compose

To make your Docker Compose experience even smoother:

  • Version Control Your docker-compose.yml: Treat it like code. Commit it to Git along with your application code. This ensures everyone has the exact same development environment.
  • Use Named Volumes for Persistent Data: For databases or any data you want to keep even after containers are removed, use named volumes (like db_data in our example). Avoid bind mounts for production data.
  • Separate Development and Production: For production, you might want more robust orchestrators (like Kubernetes) or different Docker Compose files (docker-compose.prod.yml) for different configurations (e.g., no code mounting, different resource limits).
  • Use .env Files for Sensitive Data: Don’t hardcode passwords or API keys directly in docker-compose.yml. Instead, use environment variables and load them from a .env file.
    • Create a .env file in the same directory as docker-compose.yml:
      POSTGRES_PASSWORD=my_secure_password
    • In your docker-compose.yml, refer to them:
      environment:
        POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    • Add .env to your .gitignore! 🔒
  • Keep Services Modular: Each service should ideally do one thing and do it well. This makes debugging easier and allows for independent scaling if needed.
  • Utilize depends_on (for start order) and healthcheck (for readiness): depends_on is good for simple start ordering. For robust applications, use Docker’s healthcheck within your Dockerfile or docker-compose.yml to define when a service is truly “ready” to receive connections.
  • Explore extends: For larger projects, you can use extends to share common configurations between multiple Compose files, reducing redundancy.

맺음말 (Conclusion)

Congratulations! 🥳 You’ve just taken a massive leap in simplifying your development workflow with Docker Compose. From running a simple static web page to orchestrating a multi-service web application with a database and cache, you now have the foundational knowledge to tackle more complex projects.

Docker Compose is an indispensable tool for developers working with containerized applications, especially during local development and testing. Keep experimenting, keep building, and soon you’ll be orchestrating containers like a pro! 🐳✨

What will you build next with your newfound Docker Compose superpowers? Let me know in the comments! 👇

답글 남기기

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