Skip to main content

Command Palette

Search for a command to run...

🐳 Everything About Docker Compose – Simplify Your Multi-Container Development

Updated
β€’10 min read
🐳 Everything About Docker Compose – Simplify Your Multi-Container Development

In modern app development, it's rare to run just a single container. A typical full-stack project might need a backend, a frontend, a database, a cache mechanism, etc β€” all running together. Managing these containers manually with separate docker run commands quickly becomes complex and error-prone.

That’s where Docker Compose shines.

With Docker Compose, you can define and run multi-container applications with a single YAML file. It lets you spin up all services in one go, making development, testing, and deployment smooth and reproducible.

In this blog, you'll learn:

βœ… What Docker Compose is and why it's useful

βœ… How to write and understand a docker-compose.yml file

βœ… Real-world use cases and best practices

βœ… A hands-on full-stack example using Node.js, React, MongoDB, and Redis

βœ… Key differences between Dockerfile and docker-compose.yml

Whether you're just getting started with containers or looking to streamline your workflow, this guide will help you master Docker Compose from the ground up.


πŸš€ What is Docker Compose?

Docker Compose is a tool that allows you to define and run multi-container Docker applications. With Compose, you use a YAML file (docker-compose.yml) to configure your application's services (e.g., backend, frontend, database), and then start all of them with a single command:

docker-compose up

βš™οΈ In short:

  • It orchestrates multiple containers as services.

  • It’s part of the Docker CLI and installed by default with Docker Desktop.

  • Great for local development, testing, and CI pipelines.


🧠 Why and Where Do We Use Docker Compose?

You should use Docker Compose when:

  • You have multiple containers (like Node.js backend, React frontend, MongoDB) that need to work together.

  • You want a repeatable, version-controlled environment setup.

  • You’re developing microservices or need isolated dev environments.

  • You’re working on CI/CD pipelines or want to automate container startup.


πŸ›  How to Write docker-compose.yml File – With Syntax & Explanation

Docker Compose uses a YAML file to define, configure, and run multi-container Docker applications. The file is commonly named docker-compose.yml.

Let’s break down the file syntax and all important keywords with explanation.

βœ… Basic Syntax

version: "3.8" # version is optional in Compose v3.9+ as Docker automatically uses the latest schema.

services:
  service_name:
    image: image_name
    build: path_or_options
    ports:
      - "host_port:container_port"
    environment:
      - KEY=value
    volumes:
      - host_path:container_path
    depends_on:
      - another_service

πŸ”‘ Top-Level Keywords

KeywordTypeDescription
versionstringCompose file format version (3.8 is recommended for latest compatibility)
servicesmapDefines all the containers (services) you want to run
volumesmapNamed volumes for data persistence
networksmapCustom networks (optional; Compose uses default if not specified)

🧩 services – All Your Containers

Each service under services: defines a separate container.

Common Service Options:

OptionTypeDescription
buildstring/mapPath to Dockerfile or build options
imagestringUse a prebuilt image from Docker Hub or local
container_namestringOptional custom container name
portslistMap ports from host to container (host:container)
volumeslistMount files/dirs from host or named volume
environmentlist/mapEnvironment variables for the container
env_filelist/stringLoad environment variables from file
depends_onlistWait for listed services to start first
commandstring/listOverride default command (CMD) of the image
restartstringRestart policy (no, on-failure, always, unless-stopped)

πŸ“ Detailed Example

version: "3.8"

services:
  app:
    build: ./app
    container_name: my-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - API_KEY=abc123
    volumes:
      - .:/app
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: mongo:6
    container_name: my-db
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

  redis:
    image: redis
    container_name: my-redis
    ports:
      - "6379:6379"

volumes:
  mongo-data:

πŸ’¬ Explanation of All Commands

version: "3.8" (Deprecated)

  • Specifies the version of the Docker Compose file format. Use "3.8" for modern compatibility.

services:

  • Group of all containers your app needs.

build: ./app

  • Tells Docker to build an image using the Dockerfile in the ./app directory.

image: mongo:6

  • Pulls the mongo image with tag 6 from Docker Hub.

container_name: my-app

  • Optional. Gives your container a fixed name instead of a random one.

ports:

ports:
  - "3000:3000"
  • Maps port 3000 on the host to port 3000 in the container.

  • Syntax: "HOST_PORT:CONTAINER_PORT"

environment: or env_file:

environment:
  - NODE_ENV=production
  - API_KEY=xyz123
  • Sets environment variables inside the container.

Or use:

env_file: .env

To load from a file.

volumes:

volumes:
  - ./data:/app/data
  • Mounts ./data from the host into /app/data in the container.

  • Useful for persistent storage or live-reloading code in dev mode.

Also at the bottom:

volumes:
  mongo-data:
  • Defines a named volume for MongoDB data.

depends_on:

depends_on:
  - db
  - redis
  • Ensures that db and redis start before the app.

⚠️ Note: This doesn't wait for services to be ready β€” only that they start.

restart: unless-stopped

  • Automatically restarts containers unless explicitly stopped.

  • Other options:

    • no – never restart (default)

    • always – always restart

    • on-failure – restart only on non-zero exit codes

command:

command: ["npm", "run", "dev"]
  • Overrides the default CMD in Dockerfile.

πŸ§ͺ Useful Docker Compose Commands

  • Start Services: Run the services defined in the docker-compose.yml file.
docker-compose up -d

This command starts all the services defined in the docker-compose.yml file in detached mode (-d). If you want to run the services in the foreground, you can omit the -d flag:

docker-compose up
  • Stop Services: Stop the running services defined in the docker-compose.yml file.
docker-compose down

This command stops and removes all the containers defined in the docker-compose.yml file, along with their networks and volumes. If you want to remove the volumes as well, you can use the -v flag:

docker-compose down -v
  • Build services: Build the images for the services defined in the docker-compose.yml file.
docker-compose build

This command builds the images for the services defined in the docker-compose.yml file. If you have made changes to the Dockerfiles or the context, this command will rebuild the images accordingly.

  • List running services: View the status of the services defined in the docker-compose.yml file.
docker-compose ps

This command lists all the services defined in the docker-compose.yml file along with their current status (running, exited, etc.). It provides a quick overview of the state of your multi-container application.

  • Execute command in a container: Run a command inside a specific service container.
docker-compose exec <service_name> <command>

For example, to open a shell in the web service container:

docker-compose exec web /bin/bash

This command allows you to run a command inside a specific service container defined in the docker-compose.yml file. The exec command is useful for debugging or performing administrative tasks directly within the container. You can replace <service_name> with the name of the service you want to access, and <command> with the command you want to run inside that service's container. If you want to run an interactive shell, you can use -it flags:

docker-compose exec -it <service_name> /bin/bash

🧯 Clean Up

To stop and remove all containers, networks, and volumes created by Compose:

docker-compose down

To remove volumes too:

docker-compose down -v

πŸ—οΈ Hands-On Example: Full-Stack App with Docker Compose

Let’s build a complete end-to-end Dockerized application using:

  • Node.js backend (Express)

  • React frontend (Vite)

  • MongoDB database

  • Redis cache

We'll use Dockerfile for backend/frontend builds and Docker Compose to orchestrate all services. This example will explain every command and configuration step-by-step.

πŸ—οΈ Project Structure

my-fullstack-app/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ Dockerfile
β”‚   β”œβ”€β”€ index.js
β”‚   └── package.json
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ Dockerfile
β”‚   β”œβ”€β”€ vite.config.js
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   └── App.jsx
β”‚   └── package.json
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ .env

πŸ”§ Step-by-Step Setup

1️⃣ Backend – Node.js + Express + Redis + MongoDB

backend/index.js

const express = require('express');
const mongoose = require('mongoose');
const redis = require('redis');

const app = express();
app.use(express.json());

const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/mydb';
const redisHost = process.env.REDIS_HOST || 'localhost';

mongoose.connect(mongoURL)
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => console.error(err));

const client = redis.createClient({ url: `redis://${redisHost}:6379` });
client.connect().then(() => console.log('Connected to Redis'));

app.get('/', async (req, res) => {
  await client.set('message', 'Hello from Redis!');
  const msg = await client.get('message');
  res.json({ msg });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Backend running on port ${PORT}`));

backend/package.json

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.0.0",
    "redis": "^4.6.7"
  }
}

backend/Dockerfile

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

2️⃣ Frontend – React + Vite

frontend/src/App.jsx

import { useEffect, useState } from 'react';

function App() {
  const [message, setMessage] = useState('');

  useEffect(() => {
    fetch('http://localhost:5000/')
      .then(res => res.json())
      .then(data => setMessage(data.msg));
  }, []);

  return (
    <div>
      <h1>Frontend + Backend + Redis</h1>
      <p>Message from backend: {message}</p>
    </div>
  );
}

export default App;

frontend/package.json

{
  "name": "frontend",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "vite": "^4.5.0",
    "@vitejs/plugin-react": "^4.0.0"
  }
}

frontend/Dockerfile

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

3️⃣ Docker Compose – Tying It All Together

docker-compose.yml

version: "3.8"

services:
  backend:
    build: ./backend
    ports:
      - "5000:5000"
    environment:
      - MONGO_URL=mongodb://mongo:27017/mydb
      - REDIS_HOST=redis
    depends_on:
      - mongo
      - redis

  frontend:
    build: ./frontend
    ports:
      - "3000:80"
    depends_on:
      - backend

  mongo:
    image: mongo
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

  redis:
    image: redis
    ports:
      - "6379:6379"

volumes:
  mongo-data:

4️⃣ .env (Optional)

If you want to externalize environment variables:

.env

MONGO_URL=mongodb://mongo:27017/mydb
REDIS_HOST=redis

Then in docker-compose.yml, use:

env_file:
  - .env

5️⃣ How to Run

πŸ‘‰ One-time setup:

docker-compose up --build

🧹 To stop and clean up:

docker-compose down -v

🌍 Access the App


πŸ†š Difference Between Dockerfile and docker-compose.yml

  • Dockerfile is used to build a Docker image. It contains a set of instructions (like FROM, RUN, COPY, CMD, etc.) to define how an image should be created β€” e.g., installing dependencies, copying code, and setting up the environment.

  • docker-compose.yml is used to run and manage multiple containers together as services. It defines how to run containers, configure networks, set environment variables, link services like databases, and expose ports β€” all in a single YAML file.

πŸ“Œ In Short:

FeatureDockerfiledocker-compose.yml
PurposeDefines how to build an imageDefines how to run containers
ScopeSingle container imageMulti-container application setup
Used Bydocker build, docker rundocker-compose up, docker-compose down
File FormatDockerfile instructionsYAML configuration
Example UseBuild a Node.js imageRun Node.js app with MongoDB and Redis

πŸ§ͺ Bonus Tips

  • Use depends_on to set container startup order.

  • Use .env file to manage secrets and ports.

  • Use volumes for persistent data (like databases).

  • Add restart: always for production-like resiliency.


🏁 Conclusion

Docker Compose is a game-changer for local development and multi-container apps. Instead of running multiple docker run commands, Compose gives you a single declarative file to define and run everything β€” clean, consistent, and reproducible.

Whether you're building full-stack apps, microservices, or dev environments, mastering Docker Compose will save you time and boost your productivity.


πŸ”— What’s Next?

You might want to explore:

πŸ“ Additional Resources

✍️ Author’s Note

This blog is a comprehensive guide to Docker Compose, but the best way to learn is by doing! Try building your own multi-container app using the examples provided. If you have any questions or suggestions, feel free to reach out.


πŸ’¬ Have Questions or Suggestions?

Drop a comment below or connect with me on LinkedIn or GitHub. Let’s make apps faster together! πŸš€

More from this blog

B

Build Better with Kuntal

19 posts

Practical guides, tips, and tutorials on full-stack development, performance, and mobile appsβ€”helping you build better, faster, and smarter with clean, real-world code. πŸš€