Docker Basics #3 - Docker Compose

Last Edited: 3/2/2025

This blog post introduces Docker Compose in Docker.

DevOps

In our previous article, we introduced how to build a Docker image from a Dockerfile and run a Docker container from the image. We also discussed how to use volumes to avoid making new images for every code change during development. In this article, we will introduce Docker Compose, which allows us to perform these functionalities in a simpler manner.

Docker Compose

In a small project, we can manually type a command like docker run --name myserver_c myserver -d -p 3000:4000 -v /Users/.../server:/app -v /app/node_modules to run the image and instantiate a container once. However, when running multiple containers for frontend and backend and for horizontal scaling, manually typing this long command becomes tedious and most importantly error-prone. One possible solution is to set up a bash script, but Docker offers us a better native solution, Docker Compose, which allows us to configure our builds in docker-compose.yaml as follows.

docker-compose.yaml
version: "3.8" # Docker Compose Version
 
services:
  myserver: # service's name
    build: ./server # directory to look for Dockerfile
    container_name: myserver_c
    ports:
      - "3000:4000" # local port: container port
    volumes:
      - ./server:/app # volume (relative local path: container path)
      - /app/node_modules # anonymous volume

The above example entries of docker-compose.yaml create a <directory_name>_myserver image (where the directory name is the name of the directory where the YAML file lives) and spin up a container myserver_c with port and volume mappings. For the image configuration, it will look for the path under the build section (in this case ./server) and use the Dockerfile in that directory. To build and run the image, we can use the docker compose up command, and to stop the container, we can use docker compose down. Upon stopping the containers, we can also remove them with associated images and volumes using docker compose down -rmi all -v.

Horizontal Scaling

Using Docker Compose, we can build multiple images from Dockerfiles in multiple directories and run them to spin up multiple containers at once. The following example spins up multiple images and containers from the same Dockerfile for horizontal scaling.

docker-compose.yaml
version: "3.8"
 
services:
  myserver1:
    build: ./server
    container_name: myserver_c1
    ports:
      - "3000:4000"
  myserver2:
    build: ./server
    container_name: myserver_c2
    ports:
      - "3001:4000"
  myserver3:
    build: ./server
    container_name: myserver_c3
    ports:
      - "3002:4000"

The above uses the same Dockerfile to create three images and containers, mapping to local ports 3000, 3001, and 3002. By setting up a reverse proxy or load balancer that distributes traffic to these local ports, we can achieve horizontal scaling. However, we can also containerize the reverse proxy, which allows it to communicate with servers over container ports directly without exposing servers to the local ports. It also allows us to use the replicas field as follows:

docker-compose.yaml
version: "3.8"
 
services:
  myserver:
    build: ./server
    container_name: myserver_c
    deploy:
      replicas: 3
  reverse-proxy:
    # ...reverse proxy service config (omitted)

The above spins up three containers for the servers automatically, which are not exposed to the local ports. This limits their accesses to the reverse proxy, exposed to the local port. (We will omit the implementation of the reverse proxy in this article and cover it in the next one.) We can dynamically set the number of the service myserver from three to six with docker compose scale myserver=6 after docker compose up for flexible horizontal scaling.

Image Dependencies

When setting up multiple images, there are cases where building and running images in a particular order is important. For example, we need to build servers first before setting up a reverse proxy to distribute traffic, and we need to build the backend web server before building the frontend for it. We can set the order for building and running images using the depends_on field as follows:

docker-compose.yaml
version: "3.8"
 
services:
  backend:
    build: ./backend
    container_name: backend_c
    ports:
      - "4000:4000"
  frontend:
    build: ./frontend
    container_name: frontend_c
    ports:
      - "3000:3000"
    depends_on:
      - backend

We can list the services' names that must be built and run before the service in question in the depends_on field like above, which ensures that the backend service is built and run before setting up the frontend service.

Conclusion

In this article, we introduced Docker Compose and its basic functionalities that allow us to easily configure and build services. There are many other features of Docker Compose that were not covered in this article, so I recommend checking out the official documentation cited below for more information.

Resources