Docker Compose is a tool that allows you to define and run multi-container Docker applications. Instead of running each part of your application (like a database, a web server, and a caching service) with separate, complex docker run commands, you define everything in a single configuration file.
💡 The Core Problem it Solves
Imagine running a complex application that requires:
A Frontend (e.g., a React application)
A Backend API (e.g., a Python/Flask service)
A Database (e.g., PostgreSQL or MySQL)
A Caching Service (e.g., Redis)
Without Compose, you would have to:
Run four separate
docker runcommands.Manually create a network so they can talk to each other.
Ensure the backend starts after the database.
Docker Compose fixes this by:
Reading a single configuration file (the
docker-compose.yml).Creating a single, isolated network for all services.
Starting all services in the correct order with one command.
How it Works
Docker Compose uses a declarative YAML file—by convention named docker-compose.yml—to define your entire application stack.
In following example, we leverage Docker Compose to run a sample web application using Python Flask for the frontend and PostgreSQL as the backend database.
Python Flask is a lightweight web framework used to build websites and web applications using Python.
We will create 4 files.
1️⃣ app.py
-
Your Python Flask application
-
Handles HTTP requests (
/) -
Connects to PostgreSQL via
psycopg2 -
Returns the PostgreSQL version to the browser
2️⃣ requirements.txt
-
Lists Python dependencies your app needs
-
For our project:
-
Docker will install these packages inside the container
3️⃣ Dockerfile
-
Blueprint to build your Docker image
-
Steps:
-
Start from
python:3.11-slimimage -
Set working directory
/app -
Copy
app.pyandrequirements.txt -
Install dependencies
-
Expose port 5000
-
Run the app with
python app.py
-
4️⃣ docker-compose.yml
-
Defines all containers for your app
-
Services:
-
web→ Flask app -
db→ PostgreSQL
-
-
Can define volumes, networks, environment variables like
DATABASE_URL -
Allows you to start everything with one command:
docker compose up -d
[root@devopsvm01 ~]# cd DockerCompose/
[root@devopsvm01 DockerCompose]#
[root@devopsvm01 DockerCompose]# ls -lrt
total 4
drwxr-xr-x. 2 root root 62 Nov 3 19:03 app
-rw-r--r--. 1 root root 555 Nov 3 19:04 docker-compose.yml
[root@devopsvm01 DockerCompose]# cat docker-compose.yml
services:
db:
image: postgres:15
container_name: postgres-db
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydatabase
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
web:
build: ./app
container_name: flask-app
restart: always
environment:
DATABASE_URL: postgres://myuser:mypassword@db:5432/mydatabase
ports:
- "5000:5000"
depends_on:
- db
volumes:
postgres_data:
[root@devopsvm01 DockerCompose]# cd app/
[root@devopsvm01 app]# ls -lrt
total 12
-rw-r--r--. 1 root root 467 Nov 3 18:43 app.py
-rw-r--r--. 1 root root 36 Nov 3 18:44 requirements.txt
-rw-r--r--. 1 root root 291 Nov 3 19:03 Dockerfile
[root@devopsvm01 app]# cat app.py
from flask import Flask
import psycopg2
import os
app = Flask(__name__)
DATABASE_URL = os.getenv("DATABASE_URL")
@app.route("/")
def index():
# Connect to Postgres and get version
conn = psycopg2.connect(DATABASE_URL)
cur = conn.cursor()
cur.execute("SELECT version();")
version = cur.fetchone()
cur.close()
conn.close()
return f"PostgreSQL version: {version[0]}"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
[root@devopsvm01 app]#
[root@devopsvm01 app]# cat requirements.txt
flask==2.3.3
psycopg2-binary==2.9.7
[root@devopsvm01 app]#
[root@devopsvm01 app]# cat Dockerfile
# Use Python slim image
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Copy app files
COPY app.py /app/
COPY requirements.txt /app/
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Expose port
EXPOSE 5000
# Run the app
CMD ["python", "app.py"]
[root@devopsvm01 app]#
This is the core section, defining the two containers that make up your application.
A. The db Service (PostgreSQL Database)
db:
image: postgres:15
container_name: postgres-db
restart: always
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydatabase
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"Instruction Purpose Detail db:Service Name Defines this set of configurations as a service named db. This name is used as the internal network hostname for other services (like your web app) to connect to the database. image: postgres:15Container Image Instructs Docker to pull and use the official PostgreSQL version 15 image from Docker Hub. container_name: postgres-dbExplicit Name Assigns the specific name postgres-db to the resulting container. This is useful for easy management via docker ps or docker logs. restart: alwaysRestart Policy Ensures that if the container stops (either due to an error or a host reboot), Docker will automatically attempt to restart it. environment:Configuration Sets environment variables that the PostgreSQL image uses for initial setup: POSTGRES_USER / PASSWORD / DBThese variables are read by the PostgreSQL entrypoint script to create the database, user, and password on the first run. volumes:Data Persistence Creates or uses the named volume postgres_data and mounts it to the PostgreSQL default data directory inside the container. - postgres_data:/var/lib/postgresql/dataThis is crucial because it ensures all your database files are saved on the host and will not be lost when the container is stopped or removed. ports:Port Mapping Maps the PostgreSQL's internal listening port (5432) to the host machine's port (5432). - "5432:5432"This allows non-Docker processes (like a desktop SQL client or your host's terminal) to connect to the database.
B. The web Service (Flask Application)
web:
build: ./app
container_name: flask-app
restart: always
environment:
DATABASE_URL: postgres://myuser:mypassword@db:5432/mydatabase
ports:
- "5000:5000"
depends_on:
- dbComponent Step Purpose buildBuild the Image: Docker Compose navigates to the ./app directory and executes the Dockerfile found there to build a custom image for the web application. Creates the executable image for your application code. container_nameName the Container: The resulting container instance will be named flask-app. Easy reference via the Docker CLI. restartSet Restart Policy: Ensures the web service restarts if it crashes. Maintains application uptime. environmentSet Database Connection String: Provides the Flask app with the credentials and hostname needed to connect to the database. Note: The hostname is db because Compose uses the service name as the internal DNS name. Allows the application to connect to the database service. portsExpose the Port: Maps the application's internal port 5000 (standard for Flask) to host port 5000. Allows users to access the application via a web browser on the host machine. depends_onSet Startup Order: Ensures that the db container is started and running before Docker Compose attempts to start the web container. Prevents the web app from failing immediately due to a missing database connection.
C. Volumes Definition
postgres_data:Will create a persistent volume named postgres_data so my PostgreSQL data will not be lost if the container is removed
2) requirements.txt
[root@devopsvm01 app]# cat requirements.txt
flask==2.3.3
psycopg2-binary==2.9.7
[root@devopsvm01 app]#
requirements.txt file is a standard file in the Python ecosystem used to define the project's dependencies (the external libraries and packages your application needs to run).Dockerfile what to install when the web-app image is built.Meaning of each package:
1️⃣ Flask (2.3.3)
-
A lightweight Python web framework
-
Used to build your web application
-
Version 2.3.3 is being installed
2️⃣ psycopg2-binary (2.9.7)
-
A PostgreSQL database adapter for Python
-
Allows your Flask app to connect to a PostgreSQL database
-
Version 2.9.7 is used
| Instruction | Step | Purpose |
# Use Python slim image | A comment explaining the next line. | |
FROM python:3.11-slim | Base Image: Starts the image build process using python:3.11-slim. | This provides a minimal Debian operating system environment with Python 3.11 pre-installed. Using -slim results in a smaller, more secure final image. |
# Set working directory | A comment explaining the next line. | |
WORKDIR /app | Set Context: Defines the directory /app inside the container as the current working directory for subsequent instructions.WORKDIR automatically creates the directory if it doesn’t already exist. | All following commands (like COPY and CMD) will execute relative to /app. |
# Copy app files | A comment explaining the next lines. | |
COPY app.py /app/ | Copy Code: Copies your main Python application file (app.py) from the host machine into the /app directory in the image. | Puts your executable code into the image. |
COPY requirements.txt /app/ | Copy Dependencies: Copies the requirements.txt file (which lists Flask and psycopg2-binary) into the /app directory. | Provides the list of necessary libraries. |
# Install dependencies | A comment explaining the next line. | |
RUN pip install --no-cache-dir -r requirements.txt | Install Packages: Executes the pip command to read requirements.txt and download/install all dependencies (Flask and the Postgres adapter) into the image. | Makes sure the application can run and connect to the database. |
# Expose port | A comment explaining the next line. | |
EXPOSE 5000 | Documentation: This is a documentation instruction, informing anyone using the image that the application inside listens on port 5000. | It does not actually publish the port; the ports: section in your docker-compose.yml handles the actual publishing. |
# Run the app | A comment explaining the next line. | |
CMD ["python", "app.py"] | Execution Command: Specifies the default command that runs when a container is started from this image. | This is the instruction that starts your Flask application server. |
web and db).| Line(s) | Code / Concept | Purpose in the Stack |
import... | Imports | Imports the necessary libraries: Flask for the web server, psycopg2 for the database connection, and os to read environment variables. |
app = Flask(__name__) | App Initialization | Creates the Flask application instance. |
DATABASE_URL = os.getenv("DATABASE_URL") | Read Environment Variable | Retrieves the database connection string from the environment. This variable is set in your docker-compose.yml file (DATABASE_URL: postgres://...). |
@app.route("/") | Route Definition | Defines the main route (/) for the web application. When a user visits the homepage, the index function executes. |
conn = psycopg2.connect(DATABASE_URL) | Database Connection | Uses the DATABASE_URL to establish a live connection to the db service (PostgreSQL). |
cur.execute("SELECT version();") | SQL Query | Executes a simple SQL query to retrieve the PostgreSQL database version. This proves the connection is working. |
cur.close(); conn.close() | Cleanup | Closes the cursor and the database connection to release resources. |
return f"PostgreSQL version: {version[0]}" | Web Response | Sends the retrieved database version back to the user's web browser. |
app.run(host="0.0.0.0", port=5000) | Run Server | Starts the Flask development server, making it listen on: Port 5000 (as exposed in the Dockerfile and mapped in docker-compose.yml) and Host 0.0.0.0 (which makes the server accessible from outside the container). |
[root@devopsvm01 DockerCompose]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@devopsvm01 DockerCompose]#
[root@devopsvm01 DockerCompose]# docker compose up
[+] Running 15/15
✔ db Pulled 30.3s
✔ d7ecded7702a Already exists 0.0s
✔ 829b70172da7 Pull complete 1.2s
✔ 73c8530669d9 Pull complete 3.0s
✔ c535265a0c2c Pull complete 3.4s
✔ d4b384052df7 Pull complete 5.2s
✔ eada273bc1ff Pull complete 5.7s
✔ 8438d895aa42 Pull complete 5.9s
✔ 60acc70d7b69 Pull complete 5.9s
✔ 5d3e627a217e Pull complete 26.2s
✔ 78c29b10a6ea Pull complete 26.3s
✔ f8780632cce8 Pull complete 26.3s
✔ 4dc908580fc7 Pull complete 26.3s
✔ 83e930c50688 Pull complete 26.4s
✔ 891b5a61c527 Pull complete 26.4s
[+] Building 21.2s (13/13) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading from stdin 499B 0.0s
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 390B 0.0s
=> [internal] load metadata for docker.io/library/python:3.11-slim 2.7s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/python:3.11-slim@sha256:e4676722fba839e2e5cdb844a52262b43e90e56dbd55b7ad953ee3615ad7534f 4.7s
=> => resolve docker.io/library/python:3.11-slim@sha256:e4676722fba839e2e5cdb844a52262b43e90e56dbd55b7ad953ee3615ad7534f 0.0s
=> => sha256:1ee9c106547f05aa380c4cdec2837c546439943d73d965a3fc49f228dc8be993 1.29MB / 1.29MB 0.5s
=> => sha256:f002d17b63fe84a7f8a66f20cfa63aec4f6cd2a44069f05b6296b0abfcf2a8e1 14.36MB / 14.36MB 2.6s
=> => sha256:65868b001a40155a1d3f5aa7f5a10ba02a7d55697301839dc047c9d549b670bc 248B / 248B 1.3s
=> => sha256:e4676722fba839e2e5cdb844a52262b43e90e56dbd55b7ad953ee3615ad7534f 10.37kB / 10.37kB 0.0s
=> => sha256:b596083aa14d47c78a652138aa9b98607585499d7c7ec343ae378f6c5770822d 1.75kB / 1.75kB 0.0s
=> => sha256:870925f757415a696459255b0a20b082e914cac42efaff4424b59e21bff80d5b 5.48kB / 5.48kB 0.0s
=> => extracting sha256:1ee9c106547f05aa380c4cdec2837c546439943d73d965a3fc49f228dc8be993 0.5s
=> => extracting sha256:f002d17b63fe84a7f8a66f20cfa63aec4f6cd2a44069f05b6296b0abfcf2a8e1 1.8s
=> => extracting sha256:65868b001a40155a1d3f5aa7f5a10ba02a7d55697301839dc047c9d549b670bc 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 703B 0.0s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY app.py /app/ 0.1s
=> [4/5] COPY requirements.txt /app/ 0.0s
=> [5/5] RUN pip install --no-cache-dir -r requirements.txt 9.7s
=> exporting to image 3.2s
=> => exporting layers 3.2s
=> => writing image sha256:23e7f667a7aba26fcf59440d709ada2bb7a6d6170f850108e87d9217f692c59a 0.0s
=> => naming to docker.io/library/dockercompose-web 0.0s
=> resolving provenance for metadata file 0.0s
[+] Running 5/5
✔ dockercompose-web Built 0.0s
✔ Network dockercompose_default Created 0.9s
✔ Volume dockercompose_postgres_data Created 0.0s
✔ Container postgres-db Created 0.3s
✔ Container flask-app Created 0.1s
Attaching to flask-app, postgres-db
postgres-db | The files belonging to this database system will be owned by user "postgres".
postgres-db | This user must also own the server process.
postgres-db |
postgres-db | The database cluster will be initialized with locale "en_US.utf8".
postgres-db | The default database encoding has accordingly been set to "UTF8".
postgres-db | The default text search configuration will be set to "english".
postgres-db |
postgres-db | Data page checksums are disabled.
postgres-db |
postgres-db | fixing permissions on existing directory /var/lib/postgresql/data ... ok
postgres-db | creating subdirectories ... ok
postgres-db | selecting dynamic shared memory implementation ... posix
postgres-db | selecting default max_connections ... 100
postgres-db | selecting default shared_buffers ... 128MB
postgres-db | selecting default time zone ... Etc/UTC
postgres-db | creating configuration files ... ok
postgres-db | running bootstrap script ... ok
flask-app | * Serving Flask app 'app'
flask-app | * Debug mode: off
flask-app | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
flask-app | * Running on all addresses (0.0.0.0)
flask-app | * Running on http://127.0.0.1:5000
flask-app | * Running on http://172.18.0.3:5000
flask-app | Press CTRL+C to quit
postgres-db | performing post-bootstrap initialization ... ok
postgres-db | syncing data to disk ... ok
postgres-db |
postgres-db | initdb: warning: enabling "trust" authentication for local connections
postgres-db | initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
postgres-db |
postgres-db | Success. You can now start the database server using:
postgres-db |
postgres-db | pg_ctl -D /var/lib/postgresql/data -l logfile start
postgres-db |
postgres-db | waiting for server to start....2025-11-03 13:43:36.508 UTC [47] LOG: starting PostgreSQL 15.15 (Debian 15.15-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
postgres-db | 2025-11-03 13:43:36.512 UTC [47] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-db | 2025-11-03 13:43:36.520 UTC [50] LOG: database system was shut down at 2025-11-03 13:43:35 UTC
postgres-db | 2025-11-03 13:43:36.532 UTC [47] LOG: database system is ready to accept connections
postgres-db | done
postgres-db | server started
postgres-db | CREATE DATABASE
postgres-db |
postgres-db |
postgres-db | /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
postgres-db |
postgres-db | 2025-11-03 13:43:36.965 UTC [47] LOG: received fast shutdown request
postgres-db | waiting for server to shut down...2025-11-03 13:43:36.970 UTC [47] LOG: aborting any active transactions
postgres-db | 2025-11-03 13:43:37.001 UTC [47] LOG: background worker "logical replication launcher" (PID 53) exited with exit code 1
postgres-db | .2025-11-03 13:43:37.006 UTC [48] LOG: shutting down
postgres-db | 2025-11-03 13:43:37.009 UTC [48] LOG: checkpoint starting: shutdown immediate
postgres-db | 2025-11-03 13:43:37.243 UTC [48] LOG: checkpoint complete: wrote 922 buffers (5.6%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.104 s, sync=0.125 s, total=0.237 s; sync files=301, longest=0.012 s, average=0.001 s; distance=4239 kB, estimate=4239 kB
postgres-db | 2025-11-03 13:43:37.257 UTC [47] LOG: database system is shut down
postgres-db | done
postgres-db | server stopped
postgres-db |
postgres-db | PostgreSQL init process complete; ready for start up.
postgres-db |
postgres-db | 2025-11-03 13:43:37.412 UTC [1] LOG: starting PostgreSQL 15.15 (Debian 15.15-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
postgres-db | 2025-11-03 13:43:37.415 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
postgres-db | 2025-11-03 13:43:37.416 UTC [1] LOG: listening on IPv6 address "::", port 5432
postgres-db | 2025-11-03 13:43:37.425 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres-db | 2025-11-03 13:43:37.436 UTC [63] LOG: database system was shut down at 2025-11-03 13:43:37 UTC
postgres-db | 2025-11-03 13:43:37.457 UTC [1] LOG: database system is ready to accept connections
[root@devopsvm01 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
61800444516f dockercompose-web "python app.py" 4 minutes ago Up 4 minutes 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp flask-app
d0e35d5e0907 postgres:15 "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp postgres-db
[root@devopsvm01 ~]#
Verify the volume.
[root@devopsvm01 app]# docker volume ls
DRIVER VOLUME NAME
local Jenkins-Vol
local dockercompose_postgres_data
[root@devopsvm01 app]#
Verify the setup
[root@devopsvm01 app]# curl http://localhost:5000
PostgreSQL version: PostgreSQL 15.15 (Debian 15.15-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit[root@devopsvm01 app]#
[root@devopsvm01 app]#
This confirms Flask can connect to PostgreSQL inside Docker Compose network.
To see Flask app logs in real-time:
[root@devopsvm01 app]# docker logs -f flask-app
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.18.0.3:5000
Press CTRL+C to quit
172.18.0.1 - - [03/Nov/2025 13:46:02] "GET / HTTP/1.1" 200 -
172.18.0.1 - - [03/Nov/2025 13:46:02] "GET /favicon.ico HTTP/1.1" 404 -
172.18.0.1 - - [03/Nov/2025 13:55:59] "GET / HTTP/1.1" 200 -
172.18.0.1 - - [03/Nov/2025 13:56:27] "GET / HTTP/1.1" 200 -
^C[root@devopsvm01 app]#
For PostgreSQL logs:
[root@devopsvm01 app]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
flask-app dockercompose-web "python app.py" web 17 minutes ago Up 17 minutes 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp
postgres-db postgres:15 "docker-entrypoint.s…" db 17 minutes ago Up 17 minutes 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
[root@devopsvm01 app]#
[root@devopsvm01 app]# docker network ls
NETWORK ID NAME DRIVER SCOPE
7f85aa39d125 bridge bridge local
869a14362909 dockercompose_default bridge local
ec851c7b1512 host host local
3f368f337667 none null local
[root@devopsvm01 app]#
No comments:
Post a Comment