Docker Compose for Multi-Container Applications: A Practical Guide | Chandrashekhar Kachawa | Tech Blog

Docker Compose for Multi-Container Applications: A Practical Guide

Docker

In the world of modern application development, it’s rare to find an application that consists of a single, monolithic service. Most applications are composed of multiple interconnected services—a web server, a database, a caching layer, a message queue, and so on. Managing these services individually with Docker can quickly become cumbersome. This is where Docker Compose comes to the rescue.

Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.

Why Docker Compose?

  • Simplified Configuration: Define your entire application stack in a single docker-compose.yml file.
  • Easy Environment Setup: Spin up complex development, testing, or staging environments with one command.
  • Service Orchestration: Manage the lifecycle of all services in your application together.
  • Portability: Your docker-compose.yml file can be shared and used across different environments and teams.

Understanding docker-compose.yml

The docker-compose.yml file is the heart of your Docker Compose setup. It’s a YAML file that defines your services, networks, and volumes.

Here’s a basic 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

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

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

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

docker compose down -v

docker compose build

Builds or rebuilds services.

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

Example: Build the web service image.

docker compose build web

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

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}`);
});

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"
  }
}

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"]

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:

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!

Latest Posts

Enjoyed this article? Follow me on X for more content and updates!

Follow @Ctrixdev