Найти в Дзене
Linux | Network | DevOps

Docker Compose — это не магия. Это просто YAML + одна команда

В этой статье познакомимся с Docker Compose — инструментом для создания многоконтейнерных систем с помощью одного конфигурационного файла.
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:

  • Кстати, заранее тома создавать не обязательно.

Основные ключевые слова используемые в файле:

  • — все контейнеры (сервисы), которые будут запущены, в примере выше только 1 сервис (web), но их может быть несколько;services
  • — образ из Docker Hub или собранный локально;image
  • );build— путь кDockerfile(можно использовать вместоimage
  • ;ports— проброс портов:"host:container"
  • — переменные окружения;environment
  • — подключение томов;volumes
  • — настройка сетей (автоматически создаётся пользовательская именованная сеть).networks

Установка Docker Compose

Установить docker-compose можно двумя способами:

  1. Устаревший вариант, когда устанавливается отдельное приложение —docker-compose

$ sudo apt install docker-compose

  • — это отдельное приложение.При этом появляется командаdocker-compose
  1. Современный вариант, когда мы устанавливаем плагин 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

Что произойдёт:

  1. .Docker Compose прочитает файлdocker-compose.yml
  2. ).Соберёт образweb(на основеDockerfile
  3. Создаст изолированную сеть docker_compose_default. Сеть получит название по имени рабочего каталога, а он у нас называется docker_compose.
  4. Запустит контейнер.
  5. Пробросит порт 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 и условия мы управляем очерёдностью старта контейнеров.