You will learn:

  • How to define a multi-container application using docker compose
  • How to connect services via an internal network
  • How to use environment variables and a .env file
  • How to use volumes for data persistence
  • How to configure an application without modifying source code

Theoretical Introduction

In the previous exercise, we created a custom Docker image and ran a single container using the docker run command. This approach is suitable for simple applications consisting of a single service.

In practice, however, applications usually consist of multiple parts:

  • web application
  • database
  • cache (e.g. Redis)
  • reverse proxy server
  • and other services

Running each service separately with docker run would be messy and hard to maintain.

Docker Compose was created for this purpose.

Docker Compose allows you to:

  • define multiple services in a single file (docker-compose.yml)
  • automatically create an internal network between services
  • manage volumes
  • use environment variables
  • start the entire application with a single command

In this exercise, we will create a simple application consisting of:

  • a Flask web application
  • a PostgreSQL database

The web application will store the visit count in the database.

Application Architecture


User (browser)
↓
web (Flask)
↓
db (PostgreSQL)

The services will communicate over an internal Docker network.
The service name in Compose automatically becomes its DNS name.

1. Creating the Application

First, we will prepare a simple Flask application that:

  • connects to the database
  • creates a table (if it does not exist)
  • increments a counter on each visit
  • displays the visit count

Create the file app.py:

import os
import psycopg2
from flask import Flask

app = Flask(__name__)

DB_HOST = os.getenv("DB_HOST", "db")
DB_NAME = os.getenv("POSTGRES_DB", "appdb")
DB_USER = os.getenv("POSTGRES_USER", "appuser")
DB_PASS = os.getenv("POSTGRES_PASSWORD", "secret")
APP_MESSAGE = os.getenv("APP_MESSAGE", "Hello from Docker Compose!")

def get_connection():
    return psycopg2.connect(
        host=DB_HOST,
        database=DB_NAME,
        user=DB_USER,
        password=DB_PASS
    )

@app.route("/")
def index():
    conn = get_connection()
    cur = conn.cursor()

    cur.execute("""
        CREATE TABLE IF NOT EXISTS visits (
            id SERIAL PRIMARY KEY
        );
    """)

    cur.execute("INSERT INTO visits DEFAULT VALUES;")
    conn.commit()

    cur.execute("SELECT COUNT(*) FROM visits;")
    count = cur.fetchone()[0]

    cur.close()
    conn.close()

    return f"<h1>{APP_MESSAGE}</h1><p>Visit count: {count}</p>"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

2. Defining Dependencies

Create the file requirements.txt:

Flask==3.0.2
psycopg2-binary==2.9.9

3. Preparing the Docker Image for the Web Service

Now we will create a Dockerfile that builds the image for our application.

This image:

  • uses the official Python image
  • installs dependencies
  • copies the application
  • starts the Flask server
FROM python:3.12-alpine

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

4. Configuration Using Environment Variables

Create the file .env:

POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=supersecret
APP_MESSAGE=Hello from Docker Compose!

Docker Compose will automatically load this file.

The advantages are:

  • configuration can be changed without modifying code
  • the same Compose file can be used in different environments

5. Defining Multiple Services with Docker Compose

Now we will create the docker-compose.yml file.

The file can read values from .env. ${VARIABLE} means the value is loaded from .env.

The docker-compose.yml file contains the following sections:

  • services defines the individual containers
  • db uses a ready-made image from Docker Hub
  • web is built from the local Dockerfile
  • depends_on ensures the db container starts before web; however, it does not guarantee that PostgreSQL is ready to accept connections — that is why web has restart: always set
  • volumes ensures data persistence
services:
  db:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data

  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - db
    environment:
      DB_HOST: db
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      APP_MESSAGE: ${APP_MESSAGE}
    restart: always

volumes:
  db_data:

6. Starting the Entire Application

Try a few commands to start the application and verify it is working.

Start the application and watch what happens. Open your browser at:

http://localhost:5000

Refresh the page several times. The visit count will increase.

Use these commands:

Build and start:

docker compose up --build

Start in the background:

docker compose up -d --build

Check status:

docker compose ps

View logs:

docker compose logs -f

7. Verifying Data Persistence

Stop the application:

docker compose down

Start it again:

docker compose up -d

The visit count will be preserved.

This is because we use a named volume:

volumes:
  - db_data:/var/lib/postgresql/data

If you were to use:

docker compose down -v

the volume will be deleted and the database will be re-initialized.

8. Bonus Tasks

  1. Try changing the configuration:

Change the value in .env:

APP_MESSAGE=New message from the environment!

Then restart only the web service:

docker compose up -d web

The application will display the new message without changing the source code and without needing to rebuild the image.

  1. Try running a command inside a running container:
docker compose exec db psql -U appuser -d appdb
  1. Try scaling the service:
docker compose up --scale web=3

10. Shutdown

Stop the application:

docker compose down

Summary

In this exercise, you learned how to:

  • create a multi-container application
  • use Docker Compose
  • connect services via an internal network
  • use a .env file
  • work with volumes
  • configure an application using environment variables

Docker Compose greatly simplifies the development and testing of complex applications and enables a reproducible environment with a single command.

Previous Post Next Post

Multi-container Application with Docker Compose