Docker Compose — установка и базовое использование

🕒 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 можно двумя способами:

  1. Устаревший вариант, когда устанавливается отдельное приложение — docker-compose:
$ sudo apt install docker-compose
  • При этом появляется команда docker-compose — это отдельное приложение.
  1. Современный вариант, когда мы устанавливаем плагин dockercompose:
$ 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 — создаёт и запускает контейнеры;
  • 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 — мы сможем редактировать приложение на хосте, без пере-сборки контейнера.

Запустим всё в фоне:

$ 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.

Мы используем cookie-файлы для наилучшего представления нашего сайта. Продолжая использовать этот сайт, вы соглашаетесь с использованием cookie-файлов.
Принять
Отказаться
Политика конфиденциальности