DEV Community

Chandrashekhar Kachawa
Chandrashekhar Kachawa

Posted on

Docker Compose for Multi-Container Applications: A Practical Guide

In modern application development, it’s rare to find a single-service system. Most real-world applications rely on multiple services working together — think of a web server, database, cache, and message broker forming one cohesive stack. Managing all these containers manually can be messy. That’s where Docker Compose becomes your secret weapon.

Docker Compose is a simple yet powerful tool for defining and running multi-container Docker applications. You describe your entire stack in a single docker-compose.yml file, and with one command, you can build, start, and stop everything — from your API to your database — like clockwork.


🚀 Why Docker Compose?

Here’s why Compose is a game changer for developers and DevOps engineers alike:

  • Simplified Configuration: Define your entire application stack in one YAML file.
  • Quick Environment Setup: Start or tear down complete environments with a single command.
  • Service Orchestration: Manage all your containers together rather than individually.
  • Portability: Share and reuse the same configuration across teams and environments.

Whether you’re setting up a local dev environment, a testing pipeline, or even lightweight staging, Compose keeps things consistent and reproducible.


🧩 Anatomy of a docker-compose.yml

The docker-compose.yml file is the backbone of your setup. It defines services, networks, and volumes that together form your stack.

Here’s a simple structure:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    depends_on:
      - db
  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
networks:
  default:
    # You can define custom networks here if needed
    # driver: bridge
Enter fullscreen mode Exit fullscreen mode

Let's break down the key sections:

version

Specifies the Compose file format version. It's important for compatibility and features. 3.8 is a commonly used recent version.

services

This is where you define the individual components (containers) of your application. Each service typically corresponds to a single container.

  • build: Specifies the path to the directory containing the Dockerfile for this service. If omitted, image must be specified.
  • image: Specifies the Docker image to use for the service (e.g., postgres:13, nginx:latest).
  • ports: Maps host ports to container ports. Format: "HOST_PORT:CONTAINER_PORT".
  • volumes: Mounts host paths or named volumes into the container for data persistence or code synchronization. Format: "HOST_PATH:CONTAINER_PATH" or "VOLUME_NAME:CONTAINER_PATH".
  • environment: Sets environment variables inside the container.
  • depends_on: Expresses dependency between services. Services listed here will be started before the current service. Note that depends_on only ensures the order of startup, not that the dependent service is ready.

volumes

Defines named volumes that can be used by services for persistent data storage. This is crucial for databases where you don't want data to be lost when containers are removed.

networks

Defines custom networks. By default, Compose creates a default network for your application, allowing all services to communicate with each other using their service names as hostnames.

Essential Docker Compose Commands

docker compose up

Builds, (re)creates, starts, and attaches to containers for all services defined in docker-compose.yml.

Usage: docker compose up [OPTIONS] [SERVICE...]

Common Options:

  • -d: Run containers in detached mode (in the background).
  • --build: Build images before starting containers.

Example: Start all services in detached mode.

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

docker compose down

Stops and removes containers, networks, and volumes created by up.

Usage: docker compose down [OPTIONS]

Common Options:

  • --volumes (or -v): Remove named volumes declared in the volumes section of the Compose file.

Example: Stop and remove all services and their associated networks.

docker compose down
Enter fullscreen mode Exit fullscreen mode

Example: Stop and remove services, networks, and volumes.

docker compose down -v
Enter fullscreen mode Exit fullscreen mode

docker compose build

Builds or rebuilds services.

Usage: docker compose build [OPTIONS] [SERVICE...]

Example: Build the web service image.

docker compose build web
Enter fullscreen mode Exit fullscreen mode

Other Useful Commands

  • docker compose ps: Lists containers for the current project.
  • docker compose logs [SERVICE]: Displays log output from services.
  • docker compose exec [SERVICE] COMMAND: Executes a command in a running container.

Practical Example: An Express.js Web App with PostgreSQL

Let's create a simple Python Flask application that connects to a PostgreSQL database, all managed by Docker Compose.

1. Project Structure

my-flask-app/
├── app.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

2. app.js (Express.js Application)

const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = 8000;

const pool = new Pool({
  host: process.env.DB_HOST || 'db',
  user: process.env.DB_USER || 'user',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'mydatabase',
  port: 5432,
});

app.get('/', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.send('Hello from Express.js! Connected to PostgreSQL successfully!');
  } catch (err) {
    console.error(err);
    res.status(500).send(`Hello from Express.js! Could not connect to PostgreSQL: ${err.message}`);
  }
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

3. package.json

{
  "name": "my-express-app",
  "version": "1.0.0",
  "description": "A simple Express.js app with PostgreSQL",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.11.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Dockerfile (for the Express.js app)

FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

5. docker-compose.yml

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    environment:
      DB_HOST: db
      DB_NAME: mydatabase
      DB_USER: user
      DB_PASSWORD: password
    depends_on:
      - db
  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
Enter fullscreen mode Exit fullscreen mode

How to Run This Example

  1. Save the files above in a directory named my-express-app.
  2. Navigate to the my-express-app directory in your terminal.
  3. Run docker compose up -d.
  4. Open your web browser and go to http://localhost:8000. You should see the message "Hello from Express.js! Connected to PostgreSQL successfully!".
  5. To stop and remove the services, run docker compose down -v.

Conclusion

Docker Compose is an incredibly powerful tool for managing multi-container applications. It simplifies the development workflow, making it easier to define, run, and scale complex applications. By mastering docker-compose.yml and its associated commands, you can significantly boost your productivity and streamline your containerization efforts. Happy composing!

Top comments (0)