В этой статье познакомимся с Docker Compose — инструментом для создания многоконтейнерных систем с помощью одного конфигурационного файла.
Docker Compose — это инструмент для подготовки и запуска многоконтейнерных систем. Он позволяет с помощью одного файла описать всю инфраструктуру приложения и затем запустить её одной командой. Таким образом, вместо ручного запуска десятков контейнеров, вы получаете централизованное и управляемое описание всего стека.
С помощью Docker Compose можно:
- Во-первых, описать все сервисы (контейнеры) в одном файле docker-compose.yml
- Во-вторых, запустить весь стек одной командой:docker compose up
- Кроме того, Docker Compose автоматически создаёт сеть, тома и настраивает проброс портов.
- В результате, становится легко масштабировать сервисы, пересобирать контейнеры и работать с логами.
Реальные приложения почти никогда не живут в одном контейнере. В реальной жизни приложения состоят из компонентов:
- Веб сервера (Nginx, Apache2)Принимают и обрабатывают HTTP/HTTPS-запросы.
- Могут распределять нагрузку между веб-приложениями (Flask, Django, Node.js).
- Делают SSL-терминацию, кеширование, gzip и т.д.
- Приложения (Flask, Django, Node.js)Обрабатывают описанную в них логику.
- Базы данных (PostgreSQL, MySQL, MongoDB)Хранят данные приложения.
- Кеш (Redis)Для ускорения работы приложения: хранит сессии, результаты запросов, очереди задач.
- Фоновые задачи (RabbitMQ, Celery)Обрабатывают фоновые процессы: рассылки, генерацию отчетов, тяжелые операции.
Каждый компонент — это отдельный контейнер, что позволяет:
- во-первых, обновлять компоненты по отдельности;
- во-вторых, масштабировать проект;
- в-третьих, изолировать ресурсы и зависимости.
Docker Compose позволяет описать всю эту инфраструктуру в одном текстовом файле и управлять ею как единым целым. Файл docker-compose.yml имеет YAML формат:
services: # список сервисов
web: # имя сервиса
image: nginx # образ
ports:
- "8080:80" # проброс портов
environment:
- NGINX_HOST=localhost # переменные окружения в контейнере
volumes:
- redis_data:/data # тома в контейнере
volumes: # ниже всех сервисов прописываем все тома
redis_data:
- Кстати, заранее тома создавать не обязательно.
Основные ключевые слова используемые в файле:
- — все контейнеры (сервисы), которые будут запущены, в примере выше только 1 сервис (web), но их может быть несколько;services
- — образ из Docker Hub или собранный локально;image
- );build— путь кDockerfile(можно использовать вместоimage
- ;ports— проброс портов:"host:container"
- — переменные окружения;environment
- — подключение томов;volumes
- — настройка сетей (автоматически создаётся пользовательская именованная сеть).networks
Установка Docker Compose
Установить docker-compose можно двумя способами:
- Устаревший вариант, когда устанавливается отдельное приложение —docker-compose
$ sudo apt install docker-compose
- — это отдельное приложение.При этом появляется командаdocker-compose
- Современный вариант, когда мы устанавливаем плагин docker —compose
$ sudo apt install docker-compose-plugin
- .При этом появляется под-командаdocker compose
Я использую современный вариант установки.
После установки можем проверить версию:
$ docker compose version
Docker Compose version v2.39.4
Запуск одиночного контейнера с помощью Docker Compose
Создадим рабочий каталог docker_compose, все файлы будем создавать в нём:
Во-первых, создадим веб-приложение на Flask app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "<h1>Hello from Docker Compose!</h1><p>Service: web</p>"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Создадим файл с зависимости requirements.txt:
Flask==2.3.3
Создадим
Dockerfile
:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
И наконец, создадим файл docker-compose.yml:
services:
web:
build: .
ports:
- "5000:5000"
- web — имя сервиса;
- build: .— собираем образ из текущей папки (в ней находится Dockerfile);
- ports — проброс порта.
Запустим приложение:
$ docker compose up -d
[+] up 3/3
✔ Image docker_compose-web Built 19.0s
✔ Network docker_compose_default Created 0.0s
✔ Container docker_compose-web-1 Created 0.1s
- — для запуска в фоне.-d
Что произойдёт:
- .Docker Compose прочитает файлdocker-compose.yml
- ).Соберёт образweb(на основеDockerfile
- Создаст изолированную сеть docker_compose_default. Сеть получит название по имени рабочего каталога, а он у нас называется docker_compose.
- Запустит контейнер.
- Пробросит порт 5000.
Посмотрим на запущенный контейнер с помощью Docker Compose:
$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
docker_compose-web-1 docker_compose-web "python app.py" web Up 0.0.0.0:5000->5000/tcp
- NAME — имя контейнера;
- SERVICE — логическое имя сервиса в рамках Docker Compose.
Остановим:
$ docker compose down
- При этом контейнер и сеть будут удалены.
- Но тома не удалятся.
Все контейнеры в одном docker-compose.yml автоматически могут общаться по имени, так как создается пользовательская сеть.
Основные команды
При работе с docker compose вы можете использовать следующие команды:
- — создаёт и запускает контейнеры;docker compose up
- — создаёт и запускает контейнеры в фоновом режиме (detached);docker compose up -d
- — останавливает и удаляет контейнеры и сети, но оставляет volumes;docker compose down
- — останавливает и удаляет контейнеры, сети, а также volumes;docker compose down -v
- — простая остановка всех контейнеров, без их удаления;docker compose stop
- — запуск остановленных контейнеров, без их создания;docker compose start
- — показывает статус контейнеров;docker compose ps
- — показывает логи всех контейнеров;docker compose logs
- ;docker compose logs -f web— показывает логи в реальном времени (-f) только сервисаweb
- docker compose exec -it web sh— выполняет команду внутри контейнера, ещё варианты:— подключиться к Redis;
- docker compose exec -it redis redis-cli— подключиться к PostgreSQL;
- docker compose exec -it db psql -U myuser -d myapp
- — пересобрать образы;docker compose build
- — пересобрать образы и запустить контейнеры;docker compose up --build
- — проверить корректность docker-compose.yml.docker compose config
Кстати,
docker compose
использует имя папки как префикс для контейнеров и сетей (но это можно изменить через -p).
Docker Compose и несколько сервисов
Давайте теперь на практике попробуем создать много-контейнерную систему, добавив к приложению на Python компоненты: PostgreSQL и Redis. Отредактируем приложение app.py:
# Подключаем необходимые модули
from flask import Flask
import redis
import psycopg2
import os
app = Flask(__name__)
# Подключение к Redis
# Используем переменные окружения, которые будем передавать с помощью файла .env
r = redis.Redis(
host=os.getenv('REDIS_HOST', 'localhost'),
port=6379,
db=0,
decode_responses=True
)
# Подключение к PostgreSQL
# Используем переменные окружения, которые будем передавать с помощью файла .env
def get_db_connection():
return psycopg2.connect(
host=os.getenv('POSTGRES_HOST', 'localhost'),
database=os.getenv('POSTGRES_DB', 'myapp'),
user=os.getenv('POSTGRES_USER', 'user'),
password=os.getenv('POSTGRES_PASSWORD', 'pass')
)
@app.route('/')
def hello():
# Счётчик в Redis
visits = r.incr('visits')
# Запись в БД
conn = get_db_connection()
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS hits (id serial PRIMARY KEY, count integer);")
cur.execute("INSERT INTO hits (count) VALUES (%s)", (visits,))
conn.commit()
cur.close()
conn.close()
return f"<h1>Hello from Docker!</h1><p>Visits: {visits}</p>"
if __name__ == '__main__':
port = int(os.getenv('FLASK_PORT', '5000'))
app.run(host='0.0.0.0', port=port)
Отредактируем
requirements.txt
, добавив библиотеки для подключения к Redis и Postgres:
Flask==2.3.3
redis==4.6.0
psycopg2-binary==2.9.7
Создадим файл с переменными
.env
:
# Flask
FLASK_PORT=5000
# Redis
REDIS_HOST=redis
# PostgreSQL
POSTGRES_HOST=db
POSTGRES_DB=myapp
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypass
POSTGRES_PORT=5432
- .Docker Compose автоматически загружает переменные из файла.env, если он находится в той же директории, где запускается docker compose. Эти переменные можно использовать внутриdocker-compose.yml— через синтаксис${VAR_NAME}
Отредактируем docker-compose.yml:
services:
web:
build: .
ports:
- "${FLASK_PORT}:5000" # используем переменную из .env
environment:
- REDIS_HOST=redis
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB} # переменная из .env
- POSTGRES_USER=${POSTGRES_USER} # переменная из .env
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # переменная из .env
depends_on:
- redis
- db
# bind mount
volumes:
- ./app.py:/app/app.py
redis:
image: redis:alpine
# Named volume для данных Redis
volumes:
- redis_data:/data
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Named volume для PostgreSQL
volumes:
- db_data:/var/lib/postgresql/data
# Объявление named volumes
volumes:
redis_data:
db_data:
Файл стал намного больше, и описывает 3 сервиса (контейнера):
- web — это контейнер с python приложением;
- redis — контейнер redis;
- db — контейнер с PostgreSQL.
Также в нём описаны тома: redis_data и db_data — используются контейнерами redis и db. Файл использует переменные окружения из файла .env. Контейнер web зависит от redis и от db, для этого используется depends_on.
Отредактируем файл
Dockerfile
:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
- Зачем копировать app.py если мы использовали bind mount вdocker-compose.yml?.
- Чтобы контейнер оставался самодостаточным, bind mount в продакшене уберём и контейнер получит последнюю версию приложения с помощьюCOPYА пока это dev — мы сможем редактировать приложение на хосте, без пере-сборки контейнера.
Запустим всё в фоне:
$ docker compose up --build -d
[+] up 4/4
✔ Image docker_compose-web Built 8.5s
✔ Container docker_compose-db-1 Running 0.0s
✔ Container docker_compose-redis-1 Running 0.0s
✔ Container docker_compose-web-1 Recreated 0.1s
- .Без опции--build— контейнер docker_compose-web-1 не пере-соберётся. То есть если правим файлdocker-compose.yml, то нужно использовать--build
Посмотрим на запущенные контейнеры:
$ docker compose ps
NAME SERVICE CREATED STATUS PORTS
docker_compose-db-1 db 24 seconds ago Up 24 seconds 5432/tcp
docker_compose-redis-1 redis 24 seconds ago Up 24 seconds 6379/tcp
docker_compose-web-1 web 24 seconds ago Up 23 seconds 0.0.0.0:5000->5000/tcp
Проверим логи нашего приложения:
$ docker compose logs -f web
web-1 | * Serving Flask app 'app'
web-1 | * Debug mode: off
web-1 | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
web-1 | * Running on all addresses (0.0.0.0)
web-1 | * Running on http://127.0.0.1:5000
web-1 | * Running on http://172.20.0.4:5000
web-1 | Press CTRL+C to quit
- — имя сервиса;web
- ).опция-fиспользуется для слежения за логами в реальном времени (аналогичноtail -f
Выполним запросы к приложению:
$ curl http://127.0.0.1:5000
<h1>Hello from Docker!</h1><p>Visits: 1</p>
$ curl http://127.0.0.1:5000
<h1>Hello from Docker!</h1><p>Visits: 2</p>
$ curl http://127.0.0.1:5000
<h1>Hello from Docker!</h1><p>Visits: 3</p>
Пересоберём проект и сделаем запрос ещё раз:
$ docker compose down
$ docker compose up -d
$ curl http://127.0.0.1:5000
<h1>Hello from Docker!</h1><p>Visits: 4</p>
- счетчик продолжает расти (Значит: Redis и PostgreSQL сохранили данные в томах).
Через bind mounts можем редактировать приложение
app.py
прямо на хосте. Например можем заменить:
# заменим
return f"<h1>Hello from Docker!</h1><p>Visits: {visits}</p>"
# на
return f"<h1>Hello from Web!</h1><p>Visits: {visits}</p>"
Перезапустим сервис web:
$ docker compose restart web
Сделаем запрос ещё раз:
$ curl http://127.0.0.1:5000
<h1>Hello from Web!</h1><p>Visits: 5</p>
В итоге приложение
app.py
хранит счетчик в Redis. За это отвечает вот эта часть кода:
r = redis.Redis(
host=os.getenv('REDIS_HOST', 'localhost'),
port=6379,
db=0,
decode_responses=True
)
visits = r.incr('visits')
А в PostgreSQL сохраняется каждое значение счетчика как запись в таблице. Вот эта часть кода:
def get_db_connection():
return psycopg2.connect(
host=os.getenv('POSTGRES_HOST', 'localhost'),
database=os.getenv('POSTGRES_DB', 'myapp'),
user=os.getenv('POSTGRES_USER', 'user'),
password=os.getenv('POSTGRES_PASSWORD', 'pass')
)
conn = get_db_connection()
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS hits (id serial PRIMARY KEY, count integ>
cur.execute("INSERT INTO hits (count) VALUES (%s)", (visits,))
conn.commit()
cur.close()
conn.close()
Можем посмотреть на Redis:
$ docker compose exec -it redis redis-cli
127.0.0.1:6379> GET visits
"5"
127.0.0.1:6379> exit
Можем посмотреть на PostgreSQL:
$ docker compose exec -it db psql -U myuser -d myapp
myapp=# SELECT * FROM hits;
id | count
----+-------
1 | 1
2 | 2
3 | 3
4 | 4
5 | 5
(5 rows)
myapp=# \q
Посмотрим на переменные в контейнере web:
$ docker compose exec web printenv
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=13c4306c476f
TERM=xterm
POSTGRES_PASSWORD=mypass
REDIS_HOST=redis
POSTGRES_HOST=db
POSTGRES_DB=myapp
POSTGRES_USER=myuser
LANG=C.UTF-8
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
PYTHON_VERSION=3.11.13
PYTHON_SHA256=8fb5f9fbc7609fa822cb31549884575db7fd9657cbffb89510b5d7975963a83a
HOME=/root
- Здесь виден пароль PostgreSQL (что не очень безопасно). Но пока не будем углубляться в безопасную передачу паролей.
Основное преимущество переменных в
.env
— это гибкость. Можно менять порты, имена, пароли без правки кода приложения.
Docker Compose — healthcheck и depends_on
Когда мы запускаем приложение, которое зависит от БД, важно чтобы
контейнер с БД запустился первым и был готов принимать подключения. Но Docker не ждёт, пока сервис внутри контейнера станет доступным — он считает контейнер работающим, как только стартовал процесс (например
PostgreSQL). Это может привести к ошибкам: Connection refused.
Простая зависимость depends_on запускает контейнеры в
нужном порядке, но не проверяет, готов ли сервис к работе. В качестве
реального ожидания можно использовать healthceck + условие.
Поправим docker-compose.yml:
### в db добавим секцию healthcheck
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Named volume для PostgreSQL
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
### в redis тоже добавим секцию healthcheck
redis:
image: redis:alpine
# Named volume для данных Redis
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
### в web поправим условие (depends_on)
web:
build: .
ports:
- "${FLASK_PORT}:5000" # используем переменную из .env
environment:
- REDIS_HOST=redis
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB} # переменная из .env
- POSTGRES_USER=${POSTGRES_USER} # переменная из .env
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # переменная из .env
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
# bind mount
volumes:
- ./app.py:/app/app.py
- .Обратите внимание что в секцииdepends_onмы пишем redis и db уже без знака -
Разберём секцию — healthcheck:
- test: ["CMD-SHELL", "redis-cli ping"] — пишем проверочную команду;
- interval: 5s — эта команда выполняется каждые 5 секунд;
- timeout: 5s — команда должна успеть выполниться за 5 секунд;
- retries: 10 — количество попыток 10;
- start_period: 10s — первый запуск проверочной команды будет через 10 секунд после запуска контейнера.
Пере-соберём проект с удалением томов:
$ docker compose down -v
$ docker compose up --build -d
Проверим состояния контейнеров:
$ docker compose ps
SERVICE CREATED STATUS PORTS
db 39 seconds ago Up 38 seconds (healthy) 5432/tcp
redis 39 seconds ago Up 38 seconds (healthy) 6379/tcp
web 39 seconds ago Up 33 seconds 0.0.0.0:5000->5000/tcp
- Здесь в столбце STATUS теперь видно состояние контейнеров (healthy) — что означает, что проверочная команда выполняется без ошибки.
- Вначале стартуют контейнеры redis и db.
- Затем Docker дожидается, чтобы они перешли в состояние healthy.
- И только после этого запускается контейнер web.
Конечно приложение упадёт, если мы вдруг выключим Redis или PostgreSQL. Чтобы этого не происходило, нужно править само приложение, что является задачей программиста. А с помощью healthcheck и условия мы управляем очерёдностью старта контейнеров.