Перейти к содержимому

Практика 7. Docker Compose: многоконтейнерные приложения


Цель работы

Освоить Docker Compose для управления многоконтейнерными приложениями: сервисы, сети, volumes, healthchecks, переменные окружения.


Часть 1. Первый docker-compose.yml

Задание 1.1. Установка и проверка

  1. Проверьте версию Docker Compose:
    Окно терминала
    docker compose version
    Если не установлен:
    Окно терминала
    sudo apt install docker-compose-plugin

Задание 1.2. Один сервис

  1. Создайте рабочий каталог:

    Окно терминала
    mkdir -p ~/compose-lab && cd ~/compose-lab
  2. Создайте docker-compose.yml:

    services:
    web:
    image: nginx:alpine
    ports:
    - "8080:80"
    volumes:
    - ./html:/usr/share/nginx/html:ro
  3. Создайте HTML-файл:

    Окно терминала
    mkdir html
    echo "<h1>Docker Compose работает!</h1>" > html/index.html
  4. Запустите:

    Окно терминала
    docker compose up -d
    docker compose ps
    curl http://localhost:8080
  5. Просмотрите логи и остановите:

    Окно терминала
    docker compose logs
    docker compose down

Часть 2. Многосервисное приложение

Задание 2.1. Приложение + БД + кэш

Создайте проект со следующей структурой:

~/compose-lab/
├── docker-compose.yml
├── .env
├── app/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py
└── html/

Файл app/requirements.txt:

flask==3.0.0
psycopg2-binary==2.9.9
redis==5.0.1

Файл app/app.py:

import os
import json
from datetime import datetime
from flask import Flask, jsonify
import psycopg2
import redis
app = Flask(__name__)
# Подключение к Redis
redis_client = redis.Redis(
host=os.environ.get('REDIS_HOST', 'redis'),
port=int(os.environ.get('REDIS_PORT', 6379)),
decode_responses=True
)
def get_db_connection():
return psycopg2.connect(os.environ.get('DATABASE_URL'))
@app.route('/')
def index():
# Увеличиваем счётчик посещений в Redis
visits = redis_client.incr('visits')
return jsonify({
"message": "Hello from Docker Compose!",
"visits": visits,
"timestamp": datetime.now().isoformat(),
"hostname": os.uname().nodename
})
@app.route('/db')
def db_check():
try:
conn = get_db_connection()
cur = conn.cursor()
cur.execute('SELECT version();')
version = cur.fetchone()[0]
cur.close()
conn.close()
return jsonify({"database": "connected", "version": version})
except Exception as e:
return jsonify({"database": "error", "message": str(e)}), 500
@app.route('/health')
def health():
return jsonify({"status": "healthy"})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port, debug=True)

Файл app/Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]

Файл .env:

Окно терминала
POSTGRES_USER=appuser
POSTGRES_PASSWORD=appsecret
POSTGRES_DB=appdb
APP_PORT=8080

Файл docker-compose.yml:

services:
web:
build:
context: ./app
dockerfile: Dockerfile
ports:
- "${APP_PORT:-8080}:5000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
networks:
- app-network
redis:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "64mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
networks:
- app-network
volumes:
db-data:
redis-data:
networks:
app-network:
driver: bridge

Задание 2.2. Запуск и проверка

  1. Соберите и запустите:

    Окно терминала
    cd ~/compose-lab
    docker compose up -d --build
  2. Проверьте состояние сервисов:

    Окно терминала
    docker compose ps

    Дождитесь, пока все сервисы станут healthy.

  3. Протестируйте приложение:

    Окно терминала
    # Главная страница (с счётчиком посещений)
    curl http://localhost:8080
    curl http://localhost:8080
    curl http://localhost:8080
    # Проверка БД
    curl http://localhost:8080/db
    # Health check
    curl http://localhost:8080/health
  4. Просмотрите логи:

    Окно терминала
    docker compose logs -f web # Логи только web-сервиса
    docker compose logs -f # Логи всех сервисов

Часть 3. Управление и отладка

Задание 3.1. Выполнение команд внутри контейнеров

  1. Подключитесь к PostgreSQL:

    Окно терминала
    docker compose exec db psql -U appuser -d appdb

    Внутри psql:

    \dt
    SELECT version();
    \q
  2. Проверьте Redis:

    Окно терминала
    docker compose exec redis redis-cli

    Внутри redis-cli:

    GET visits
    KEYS *
    EXIT
  3. Откройте shell в контейнере web:

    Окно терминала
    docker compose exec web sh
    env | grep -E "(DATABASE|REDIS)"
    exit

Задание 3.2. Перезапуск и пересборка

  1. Пересоберите только один сервис:

    Окно терминала
    docker compose build web
    docker compose up -d web
  2. Перезапустите без пересборки:

    Окно терминала
    docker compose restart web
  3. Пересоберите всё с нуля:

    Окно терминала
    docker compose build --no-cache
    docker compose up -d

Задание 3.3. Остановка и удаление

  1. Остановите всё:

    Окно терминала
    docker compose down

    Данные в volumes сохраняются.

  2. Остановите и удалите volumes:

    Окно терминала
    docker compose down -v

    Данные потеряны.


Часть 4. Переменные окружения

Задание 4.1. Приоритет переменных

  1. Измените порт через переменную окружения shell:

    Окно терминала
    APP_PORT=9090 docker compose up -d
    curl http://localhost:9090
    docker compose down
  2. Измените порт в .env на 8888 и запустите без shell-переменной:

    Окно терминала
    # Отредактируйте .env: APP_PORT=8888
    docker compose up -d
    curl http://localhost:8888
    docker compose down
  3. Используйте значение по умолчанию из compose-файла (удалите APP_PORT из .env):

    Окно терминала
    docker compose up -d
    curl http://localhost:8080 # Используется default из ${APP_PORT:-8080}
    docker compose down

Задание 4.2. env_file

  1. Создайте файл config/app.env:

    Окно терминала
    mkdir config
    cat > config/app.env << 'EOF'
    LOG_LEVEL=debug
    SECRET_KEY=my-secret-key-12345
    EOF
  2. Подключите его в compose-файле к сервису web:

    web:
    env_file:
    - ./config/app.env
  3. Проверьте, что переменные доступны внутри контейнера:

    Окно терминала
    docker compose up -d --build
    docker compose exec web env | grep -E "(LOG_LEVEL|SECRET_KEY)"
    docker compose down

Часть 5. Масштабирование

Задание 5.1. Запуск нескольких экземпляров

  1. Измените порт web-сервиса, чтобы Docker назначал случайные порты:

    web:
    ports:
    - "5000" # Случайный порт на хосте
  2. Запустите 3 экземпляра:

    Окно терминала
    docker compose up -d --scale web=3
    docker compose ps
  3. Узнайте назначенные порты:

    Окно терминала
    docker compose port web 5000
    docker compose ps --format "table {{.Name}}\t{{.Ports}}"
  4. Проверьте каждый экземпляр (hostname будет разным):

    Окно терминала
    # Получите порты и проверьте каждый
    for port in $(docker compose ps web --format '{{.Ports}}' | grep -oP '\d+(?=->5000)'); do
    echo "Port $port:"
    curl -s http://localhost:$port | python3 -m json.tool
    echo ""
    done
  5. Вернитесь к одному экземпляру:

    Окно терминала
    docker compose up -d --scale web=1
    docker compose down

Часть 6. Реестры образов

Задание 6.1. Тегирование и публикация

  1. Тегируйте образ для Docker Hub (замените yourusername):

    Окно терминала
    docker compose build web
    docker tag compose-lab-web:latest yourusername/compose-lab-web:1.0.0
    docker images | grep compose-lab
  2. (Необязательно) Войдите в Docker Hub и отправьте:

    Окно терминала
    docker login
    docker push yourusername/compose-lab-web:1.0.0

Мини-проект: полноценный стек с Nginx

Дополните проект reverse proxy на Nginx, чтобы получить архитектуру:

Клиент → Nginx (порт 80) → Web App (порт 5000) → PostgreSQL + Redis
  1. Создайте файл nginx/nginx.conf:

    upstream web_app {
    server web:5000;
    }
    server {
    listen 80;
    location / {
    proxy_pass http://web_app;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    }
    location /health {
    proxy_pass http://web_app/health;
    }
    }
  2. Добавьте сервис nginx в docker-compose.yml:

    nginx:
    image: nginx:alpine
    ports:
    - "80:80"
    volumes:
    - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
    - web
    restart: unless-stopped
    networks:
    - app-network
  3. Уберите проброс порта у web-сервиса (трафик идёт только через nginx).

  4. Запустите и проверьте:

    Окно терминала
    docker compose up -d --build
    curl http://localhost
    curl http://localhost/health
    docker compose logs -f nginx
    docker compose down

Контрольные вопросы

  1. Какую проблему решает Docker Compose по сравнению с docker run?
  2. Из каких секций верхнего уровня состоит docker-compose.yml?
  3. Чем image отличается от build в описании сервиса?
  4. Как сервисы обращаются друг к другу внутри Compose? Почему работают DNS-имена?
  5. Чем bind mount отличается от named volume в контексте Compose?
  6. Опишите приоритет переменных окружения в Docker Compose.
  7. Зачем нужны healthchecks? Чем service_healthy отличается от service_started?
  8. Что произойдёт при docker compose down? А при docker compose down -v?
  9. Какие ограничения у масштабирования через --scale?
  10. Чем Docker Compose отличается от Kubernetes? Когда использовать каждый?