토. 8월 16th, 2025

G: Setting up a local development environment can often feel like an archaeological dig ⛏️ – unearthing ancient dependencies, battling conflicting versions, and spending hours just to get a basic “Hello World” running. If you’ve ever thought, “There has to be a better way!”, you’re in luck!

Enter Docker Compose. 🚀 It’s the ultimate tool to tame the chaos of local development, allowing you to define and run multi-container Docker applications with a single command. Say goodbye to “works on my machine!” woes and hello to instant, consistent, and portable development setups.

Let’s dive in and see how Docker Compose can transform your workflow!


💡 What is Docker Compose? The Orchestration Maestro

At its core, Docker Compose is a tool for defining and running multi-container Docker applications. Instead of running each Docker container individually with complex docker run commands, you define all your services, networks, and volumes in a single YAML file called docker-compose.yml.

Think of it like a blueprint for your entire application stack. Once you have this blueprint, you can spin up, scale, and tear down your complete environment with just one command.

Key Benefits for Local Development:

  • Consistency: Your local environment will exactly mirror your staging or production environments (or at least be very, very close!). No more “it worked on my machine.” ✅
  • Isolation: Each service runs in its own isolated container, preventing conflicts between dependencies or different projects on your machine. 📦
  • Simplicity: Define complex multi-service applications in a human-readable YAML file. Spin them up with a single command. ✨
  • Portability: Share your docker-compose.yml with your team, and everyone gets the exact same environment setup in minutes. Perfect for onboarding new developers! 🤝
  • Version Control: Commit your docker-compose.yml to Git alongside your code, ensuring your environment configuration is always tracked. 📚

🛠️ Getting Started: Prerequisites

Before we jump into the fun stuff, make sure you have Docker installed on your system.

  • Docker Desktop: For macOS and Windows users, Docker Desktop is the easiest way to get Docker Engine and Docker Compose (as a plugin, docker compose) installed.
  • Docker Engine & Docker Compose CLI: For Linux users, you’ll typically install Docker Engine separately, and then docker-compose (the standalone Python tool) or the newer docker compose (as a Docker CLI plugin, which is now the recommended approach).

Note: In newer Docker versions, docker compose (with a space) is the preferred command, replacing the older docker-compose (with a hyphen). Both often work, but it’s good to adopt the new standard.


📝 The docker-compose.yml File: Your Environment’s Blueprint

The heart of Docker Compose is the docker-compose.yml file. Let’s break down its common sections and directives with examples.

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

services: # Defines the individual services (containers) that make up your application
  web:
    build: . # Or 'image: nginx:latest' if using a pre-built image
    ports:
      - "80:80" # Maps host port 80 to container port 80
    volumes:
      - ./app:/usr/src/app # Mounts local 'app' directory into the container
    environment:
      NODE_ENV: development # Sets environment variables inside the container
      DATABASE_URL: postgres://user:password@db:5432/mydb
    depends_on:
      - db # Ensures the 'db' service starts before 'web'
    networks:
      - myapp-network # Connects this service to a custom network

  db:
    image: postgres:13 # Uses an official PostgreSQL image
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db_data:/var/lib/postgresql/data # Persists DB data using a named volume
    networks:
      - myapp-network

volumes: # Defines named volumes for data persistence
  db_data:

networks: # Defines custom networks for service communication
  myapp-network:
    driver: bridge # The default network driver

Let’s dissect the key components:

  1. version: Always start with this. 3.8 (or the latest 3.x) is generally recommended for modern Compose features.
  2. services: This is where you define each individual container that forms your application.
    • web / db: These are arbitrary names you give to your services.
    • build vs. image:
      • build: .: Tells Docker Compose to build the image from a Dockerfile located in the current directory (.). Useful for your application code.
      • image: postgres:13: Pulls a pre-built image from Docker Hub. Ideal for databases, caches, or other off-the-shelf components.
    • ports: Maps ports from your host machine to the container. "HOST_PORT:CONTAINER_PORT".
      • Example: "80:80" means your host’s port 80 will forward traffic to the container’s port 80.
    • volumes: Mounts paths from your host machine into the container, or defines named volumes for data persistence.
      • Bind Mounts: - ./app:/usr/src/app links your local app directory to /usr/src/app inside the container. Changes made on your host are instantly reflected in the container, perfect for live code reloading during development! 🔄
      • Named Volumes: - db_data:/var/lib/postgresql/data persists data across container restarts. The db_data volume is defined in the volumes section.
    • environment: Sets environment variables inside the container. Crucial for configuration (e.g., database credentials, API keys).
    • depends_on: Specifies that certain services should start before others. This helps with service dependencies (e.g., your web app needs the database to be up). Note: This only guarantees start order, not readiness. For true readiness, consider healthcheck.
    • networks: Connects services to specific networks. By default, Compose creates a default network, but custom networks offer better isolation and organization for complex setups.
  3. volumes: Defines named volumes, which are managed by Docker and are the preferred way to persist data generated by Docker containers (like database files).
  4. networks: Defines custom bridge networks. Services on the same network can communicate with each other using their service names (e.g., web service can access db service via hostname db).

🚀 Practical Example: A Node.js, PostgreSQL, and Redis Stack

Let’s build a common local development environment: A Node.js backend, a PostgreSQL database, and a Redis cache.

1. Project Structure:

my-node-app/
├── app/
│   ├── index.js
│   └── package.json
├── Dockerfile
└── docker-compose.yml

2. app/package.json (for our Node.js app):

{
  "name": "my-node-app",
  "version": "1.0.0",
  "description": "A simple Node.js app",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "pg": "^8.7.1",
    "redis": "^3.1.2"
  }
}

3. app/index.js (a simple Node.js server):

const express = require('express');
const { Client } = require('pg');
const redis = require('redis');

const app = express();
const port = 3000;

// PostgreSQL Client
const pgClient = new Client({
  host: 'db', // Hostname is the service name in docker-compose.yml
  user: process.env.POSTGRES_USER,
  password: process.env.POSTGRES_PASSWORD,
  database: process.env.POSTGRES_DB,
});

// Redis Client
const redisClient = redis.createClient({
  host: 'redis', // Hostname is the service name in docker-compose.yml
  port: 6379,
});

async function connectServices() {
  try {
    await pgClient.connect();
    console.log('✅ Connected to PostgreSQL!');
    await redisClient.connect(); // For redis v4+
    console.log('✅ Connected to Redis!');
  } catch (err) {
    console.error('Failed to connect to services:', err.message);
    process.exit(1);
  }
}

connectServices();

app.get('/', async (req, res) => {
  try {
    // Example: Fetch data from DB
    const result = await pgClient.query('SELECT NOW() as now');
    const dbTime = result.rows[0].now;

    // Example: Store and retrieve from Redis
    const visitsKey = 'visits';
    await redisClient.incr(visitsKey);
    const visits = await redisClient.get(visitsKey);

    res.send(`

<h1>Hello from Node.js!</h1>

<p>Current DB Time: ${dbTime}</p>

<p>Total Visits: ${visits}</p>

<p>Environment: ${process.env.NODE_ENV}</p>
    `);
  } catch (err) {
    console.error('Error handling request:', err.message);
    res.status(500).send('Internal Server Error');
  }
});

app.listen(port, () => {
  console.log(`🚀 Node.js app listening at http://localhost:${port}`);
});

// Handle graceful shutdown
process.on('SIGINT', async () => {
  console.log('Shutting down...');
  await pgClient.end();
  await redisClient.quit();
  process.exit(0);
});

4. Dockerfile (for our Node.js app):

# Use a slim Node.js base image
FROM node:18-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to leverage Docker layer caching
COPY app/package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY app/ .

# Expose the port the app runs on
EXPOSE 3000

# Command to run the application
CMD [ "npm", "start" ]

5. docker-compose.yml (our complete environment):

version: '3.8'

services:
  # Our Node.js web application
  web:
    build: . # Build from Dockerfile in the current directory
    ports:
      - "3000:3000" # Map host port 3000 to container port 3000
    volumes:
      - ./app:/usr/src/app # Mount local 'app' directory for hot-reloading
      - /usr/src/app/node_modules # Anonymous volume to prevent host's node_modules from overriding container's
    environment:
      NODE_ENV: development
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    depends_on: # Ensure DB and Redis start before the web app
      - db
      - redis
    command: ["npm", "run", "start"] # Explicitly run the start script
    restart: unless-stopped # Keep the service running unless explicitly stopped

  # PostgreSQL database
  db:
    image: postgres:15 # Use a specific PostgreSQL version
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    volumes:
      - db_data:/var/lib/postgresql/data # Persist database data
    ports:
      - "5432:5432" # Optional: Expose DB port to host for direct connection (e.g., via GUI tool)
    restart: unless-stopped

  # Redis cache
  redis:
    image: redis:6-alpine # Use a lightweight Redis image
    ports:
      - "6379:6379" # Optional: Expose Redis port to host
    volumes:
      - redis_data:/data # Persist Redis data (AOF/RDB files)
    restart: unless-stopped

# Define named volumes for data persistence
volumes:
  db_data:
  redis_data:

✨ Running Your Environment

With all files in place, navigate to my-node-app/ in your terminal and run:

docker compose up -d
  • docker compose up: Builds (if necessary) and starts all services defined in docker-compose.yml.
  • -d: Runs the containers in “detached” mode (in the background).

You’ll see output indicating Docker is pulling images, building your app, and starting containers.

What just happened?

  1. Docker Compose built your web service image using your Dockerfile.
  2. It pulled the postgres:15 and redis:6-alpine images from Docker Hub.
  3. It created a network and connected all services.
  4. It started the db and redis containers first, then the web container.
  5. Your local app directory is mounted into the web container, so any code changes you make locally will be immediately reflected in the running container (if your app supports hot-reloading, like nodemon for Node.js, which you’d configure in your Dockerfile or command).

Access your app: Open your browser and go to http://localhost:3000. 🎉 You should see the “Hello from Node.js!” message, along with the current database time and visit count. Refresh the page to see the visit count increment!

⚙️ Common Docker Compose Commands

Here are essential commands you’ll use daily:

  • docker compose up: Build (if needed) and start all services.
  • docker compose up -d: Start services in the background (detached mode).
  • docker compose up --build: Forces Docker Compose to rebuild images even if they haven’t changed. Useful after modifying a Dockerfile.
  • docker compose down: Stop and remove all containers, networks, and volumes defined in the docker-compose.yml. Use with caution for volumes, as it will delete persistent data!
  • docker compose down --volumes: Same as down, but also removes named volumes. Be careful with this on databases!
  • docker compose ps: List the running services and their status.
  • docker compose logs [service_name]: View the logs for a specific service (e.g., docker compose logs web).
  • docker compose logs -f [service_name]: Follow the logs in real-time.
  • docker compose exec [service_name] [command]: Execute a command inside a running service’s container.
    • Example: docker compose exec web bash (opens a bash shell in the web container).
    • Example: docker compose exec db psql -U myuser mydb (connects to PostgreSQL via psql client).
  • docker compose stop [service_name]: Stop a running service without removing it.
  • docker compose start [service_name]: Start a stopped service.
  • docker compose restart [service_name]: Restart a service.
  • docker compose pull: Pull the latest images for all services.

🌟 Advanced Tips & Best Practices

  1. Use .env files for Sensitive Info: Instead of hardcoding sensitive environment variables in docker-compose.yml (like passwords), use an .env file at the same level as your docker-compose.yml.

    # .env file
    POSTGRES_USER=myuser
    POSTGRES_PASSWORD=mypassword_secure!

    Then, in docker-compose.yml, reference them:

    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

    Docker Compose automatically loads variables from .env. Remember to add .env to your .gitignore! 🚫

  2. Health Checks for Robustness: depends_on only ensures start order. For true readiness, use healthcheck to ensure a service is actually ready to receive connections before dependent services try to connect.

    services:
      db:
        # ...
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
          interval: 5s
          timeout: 5s
          retries: 5
      web:
        # ...
        depends_on:
          db:
            condition: service_healthy # Wait for DB to be healthy
  3. Docker Compose Profiles: Manage different sets of services for different development needs.

    # docker-compose.yml
    services:
      app: # Always runs
        # ...
      queue: # Runs only with 'dev' profile
        profiles: ["dev"]
        # ...
      debugger: # Runs only with 'debug' profile
        profiles: ["debug"]
        # ...

    Run with: docker compose --profile dev up -d or docker compose --profile debug up -d.

  4. Multi-stage Builds for Smaller Images: If you’re building your application image, use multi-stage Dockerfiles to keep your production images lean by discarding build-time dependencies.

    # Dockerfile
    # Stage 1: Build dependencies and app
    FROM node:18 AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build # If you have a build step
    
    # Stage 2: Create final, slim image
    FROM node:18-alpine
    WORKDIR /app
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/dist ./dist # Or wherever your built app is
    COPY --from=builder /app/index.js . # Copy entrypoint
    CMD ["node", "index.js"]
  5. .dockerignore File: Just like .gitignore, a .dockerignore file prevents unnecessary files (like node_modules from your host, .git directories, or temporary files) from being copied into your Docker image during the build process, leading to faster builds and smaller images.

    # .dockerignore
    node_modules
    .git
    .vscode
    npm-debug.log

🐛 Troubleshooting Common Issues

  • Port Conflicts: “Error: port is already allocated.”
    • Solution: Another process on your host machine is already using the port you’re trying to map. Change the host port mapping in docker-compose.yml (e.g., "3001:3000" instead of "3000:3000").
  • Volume Permissions: “Permission denied” errors when trying to read/write to mounted volumes.
    • Solution: This often happens on Linux or WSL2 if the user inside the container doesn’t have permissions for the mounted host directory.
      • Option 1: Change permissions on the host directory (chmod -R 777 my-app-folderuse with caution, as it’s less secure).
      • Option 2: Map the container user ID to the host user ID (more advanced, often done via user: directive in docker-compose.yml or specifying user in Dockerfile).
  • Service Not Ready (Despite depends_on): Your web app fails because the DB isn’t fully initialized even after starting.
    • Solution: Implement healthcheck for dependent services (like DBs) and use condition: service_healthy in depends_on. For simple cases, a wait-for-it.sh script can also be used as a command wrapper.
  • Image Pull Failures: “No such image” or authentication errors.
    • Solution: Check your internet connection. Ensure the image name and tag are correct. If it’s a private registry, make sure you’re logged in (docker login).
  • docker-compose.yml Syntax Errors: “YAMLException: bad indentation.”
    • Solution: YAML is very strict about indentation. Use a linter or a good IDE that highlights YAML errors (like VS Code with a YAML extension).

🎉 Conclusion: Your Local Dev Environment, Reimagined!

Docker Compose is an incredibly powerful and versatile tool that truly revolutionizes how you approach local development. By externalizing your environment setup into a simple YAML file, you gain:

  • Unprecedented consistency and reliability.
  • Blazing fast onboarding for new team members.
  • Effortless management of complex dependencies.
  • The freedom to experiment and iterate without fear of “breaking” your machine.

If you haven’t adopted Docker Compose for your local development yet, now is the time! Start small, define your core services, and gradually add complexity as needed. You’ll quickly wonder how you ever managed without it.

Happy containerizing! 🐳✨

답글 남기기

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