Introduction: Why Docker Compose

Welcome back! So far, you have learned how to run single-container applications, build your own Docker images, and persist data using volumes. In the real world, however, most applications are made up of more than one service. For example, a web application might need a database, a cache, or a message queue to work properly. Running each service in its own container is a best practice, but managing all these containers manually can quickly become confusing and error-prone.

Imagine you want to run your Flask web app alongside a Redis server for caching. You would need to start each container, make sure they are on the same network, set environment variables, and keep track of which ports are mapped. Doing this by hand every time is not only tedious but also easy to get wrong.

This is where Docker Compose comes in. Docker Compose lets you define all your services, networks, and volumes in a single file, and then start everything with one command. It makes your development workflow much simpler and more reliable. You can also share your setup with teammates, and everyone will have the same environment with just a single command.

On CodeSignal, Docker and Docker Compose are already installed for you, so you can focus on learning how to use them. However, it is important to understand these steps so you can set up your own environment outside of CodeSignal in the future.

What Compose Manages for You

Docker Compose is a tool that helps you manage multi-container applications. Instead of running each container separately, you describe your entire application in a single file called docker-compose.yml. This file becomes the source of truth for your project.

With Compose, you define services (each service is a container, like your web app or Redis), networks (so containers can talk to each other easily), and volumes (for persisting data). Compose automatically creates a private network for your services, so you do not have to worry about setting up networking or DNS. Each service can be reached by its name, making service discovery simple.

By using one configuration file, you make your setup repeatable and easy to share. Anyone with Docker and Compose can run your entire stack with a single command, and everything will work the same way every time.

Project Layout We’ll Use

Let’s quickly review the files you will use in this project. This will help you see how everything fits together, especially if you remember the previous lessons.

  • Dockerfile: This file describes how to build the image for your Flask web app. It installs Python, your app code, and any dependencies.
  • app.py: This is your Flask application. It now uses Redis to count visits and writes logs to a file.
  • requirements.txt: This lists the Python packages your app needs, such as Flask and redis.
  • docker-compose.yml: This is the new file you will focus on. It defines both the web app and the Redis service, how they connect, and how data is persisted.

Here is what your project directory looks like:

docker-app/
  ├── Dockerfile
  ├── app.py
  ├── requirements.txt
  ├── docker-compose.yml
  └── logs/

Each file has a specific job. The Dockerfile builds your app image, app.py contains your code, requirements.txt lists dependencies, and docker-compose.yml ties everything together. The logs directory is used to persist log files from your app.

Writing docker-compose.yml (Step by Step)

Now, let’s build the docker-compose.yml file step by step. This file tells Docker Compose how to run your multi-container application.

First, you specify the version of the Compose file format. Here, you use version 3.8, which is widely supported.

Next, you define your services. In this example, you have two services: web and redis.

The web service is your Flask app. Compose will build the image using your Dockerfile. You map port 3000 on your host to port 3000 in the container, so you can access the app in your browser.

You also mount the logs directory from your host into the container, so logs are saved even if the container is removed. The environment section sets the REDIS_HOST variable, telling your app how to find the Redis service.

The redis service uses the official redis:alpine image from Docker Hub. You do not need to build anything here; Compose will pull the image for you.

Here is the complete docker-compose.yml:

version: '3.8'

services:
  web:
    build: .
    image: my-web-app:compose
    ports:
      - "3000:3000"
    volumes:
      - ./logs:/app/logs
    environment:
      - REDIS_HOST=redis
  
  redis:
    image: "redis:alpine"

This file is all you need to run both your web app and Redis together, with proper networking and data persistence.

Networking and Environment Variables in Compose

One of the best features of Docker Compose is automatic networking. When you start your services with Compose, it creates a private network and connects all your services to it. Each service can reach the others by using the service name as the hostname.

In your Flask app, you connect to Redis using the hostname redis. This works because Compose sets up DNS for you. In your app.py, you read the REDIS_HOST environment variable, which is set to redis in your compose file. This tells your app to connect to the Redis service running in the other container.

Here is the relevant part of your app.py:

from redis import Redis
import os

redis = Redis(host=os.environ.get("REDIS_HOST", "localhost"), port=6379)

When the app runs inside the container, os.environ.get("REDIS_HOST", "localhost") returns redis, so your app connects to the correct service. If you were running the app outside of Compose, it would default to localhost.

This automatic service discovery makes it much easier to connect your services without hardcoding IP addresses or worrying about network setup. To manage the startup order, you might use depends_on. However, be aware that depends_on only ensures a container has started; it does not wait until the service inside is ready to accept connections. For true readiness checks, you must combine it with a healthcheck.

Running and Managing the Stack

With your docker-compose.yml in place, you can now start your entire application stack with a single command. Make sure you are in the docker-app directory.

To build the images and start all services in the background, run:

docker compose up --build -d

This command builds your web app image (if needed), pulls the Redis image, creates the network, and starts both containers.

To see which services are running, use:

docker compose ps

You will see output like:

    Name                  Command               State           Ports         
----------------------------------------------------------------------------
docker-app_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp
docker-app_web_1     python app.py                  Up      0.0.0.0:3000->3000/tcp

To view the logs from your web service, use:

docker compose logs -f web

Now, open your browser and go to http://localhost:3000. Each time you refresh the page, the counter will increase. This is because your Flask app is using Redis to keep track of visits.

You can also check the logs on your host machine. The log file is saved in the logs directory:

cat logs/access.log

You will see output like:

Accessed at 2024-06-10 16:00:01.123456, visit #1
Accessed at 2024-06-10 16:00:05.789012, visit #2

This shows that your app is working with both Redis and persistent logs, all managed by Docker Compose.

Common Tweaks You’ll Make

As you work with Docker Compose, you will often need to make changes and restart your services. For example, if you update your app code or change dependencies, you should rebuild your images and restart the stack:

docker compose up --build

If you want to change environment variables, update them in your docker-compose.yml and restart the affected service.

Compose also makes it easy to scale services. For example, to run three instances of your web app, you can use:

docker compose up --scale web=3

This creates three instances of your web service, but the log file will be shared if using a bind mount. This is useful for testing how your app behaves with multiple containers.

When you are done, you can stop and remove all containers, networks, and volumes with:

docker compose down

This cleans up everything created by Compose, so you can start fresh next time.

Troubleshooting and Cleanup

Sometimes, you might run into issues like port conflicts, missing environment variables, or build cache problems. If you see an error that a port is already in use, make sure no other process is using it, or change the port mapping in your compose file.

If your app cannot find an environment variable, double-check your docker-compose.yml for typos. If you suspect a build cache issue, you can force a rebuild with:

docker compose build --no-cache

To see logs for a specific service, use:

docker compose logs web

If you need to restart just one service, you can use:

docker compose restart web

To remove everything, including named volumes, use:

docker compose down -v

This is helpful if you want to reset your environment completely.

Summary and What’s Next

In this lesson, you learned how to define and run a multi-container application using Docker Compose. You combined your Flask web app with a Redis service, set up persistent logging, and managed everything with a single configuration file. You also saw how Compose handles networking, environment variables, and data persistence for you.

You are now ready to practice modifying Compose settings, observe how services communicate, and even scale your web app. Remember, on CodeSignal, Docker and Compose are pre-installed, so you can focus on learning the concepts and commands. These skills will help you set up and manage complex applications on your own machine or in a team environment.

In the next exercises, you will get hands-on experience with Docker Compose, making changes and seeing the results for yourself.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal