🕒 9 мин.
В этой статье познакомимся с Docker Compose — инструментом для создания многоконтейнерных систем с помощью одного конфигурационного файла.
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:
- Кстати, заранее тома создавать не обязательно.
Основные ключевые слова используемые в файле:
services— все контейнеры (сервисы), которые будут запущены, в примере выше только 1 сервис (web), но их может быть несколько;image— образ из Docker Hub или собранный локально;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— создаёт и запускает контейнеры;docker compose up -d— создаёт и запускает контейнеры в фоновом режиме (detached);docker compose down— останавливает и удаляет контейнеры и сети, но оставляет volumes;docker compose down -v— останавливает и удаляет контейнеры, сети, а также volumes;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— выполняет команду внутри контейнера, ещё варианты:docker compose exec -it redis redis-cli— подключиться к Redis;docker compose exec -it db psql -U myuser -d myapp— подключиться к PostgreSQL;
docker compose build— пересобрать образы;docker compose up --build— пересобрать образы и запустить контейнеры;docker compose config— проверить корректность docker-compose.yml.
Кстати, 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 — мы сможем редактировать приложение на хосте, без пере-сборки контейнера.
- Чтобы контейнер оставался самодостаточным, bind mount в продакшене уберём и контейнер получит последнюю версию приложения с помощью
Запустим всё в фоне:
$ 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 и условия мы управляем очерёдностью старта контейнеров.
Если понравилась статья, подпишись на мой канал в VK или Telegram.