Compose для пайплайна
Теперь соберём настоящий пайплайн: Postgres, загрузчик CSV и dbt. Для этого нужны пять приёмов Compose: build, command, healthcheck, .env и profiles.
orders.csv → loader (Python) → Postgres → dbt → analytics.revenue_by_channel ↑ Adminerloader— наш образ. Он читает CSV и пишет в базу. Работает один раз и завершается.db— Postgres. Работает постоянно.dbt— запускаем вручную, когда нужно.
Работайте в ~/docker-practice. Понадобятся файлы:
docker-practice/├── app/ # из учебного набора├── data/orders.csv # из учебного набора├── Dockerfile # образ загрузчика├── compose.yaml├── .env # пароль (не в Git)├── .env.example # пример без секрета (в Git)├── .gitignore└── dbt/ ├── Dockerfile ├── dbt_project.yml ├── profiles.yml └── models/revenue_by_channel.sqlDockerfile
Заголовок раздела «Dockerfile»Это Dockerfile из урока «Хороший образ». Он собирает образ с psycopg и скриптами из app/:
FROM python:3.12-slim
# Пользователь без прав root. Создаём его рано: этот слой почти не меняетсяRUN useradd --create-home appuser
WORKDIR /app
# Сначала зависимости: слой берётся из кэша, пока requirements.txt не изменилсяCOPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
# Потом код: он меняется чащеCOPY app/ .
USER appuser
CMD ["python", "report.py"]По умолчанию образ запускает report.py. Для загрузки мы заменим команду в Compose.
Пароль в .env
Заголовок раздела «Пароль в .env»Compose сам читает файл .env рядом с compose.yaml. Создайте его:
echo "DB_PASSWORD=secret" > .envecho "DB_PASSWORD=change-me" > .env.exampleprintf '.env\ndbt/target/\ndbt/logs/\ndbt/.user.yml\n' > .gitignoreВ compose.yaml пароль берётся так: ${DB_PASSWORD}.
Проверьте, что подстановка работает:
docker compose config | grep POSTGRES_PASSWORD POSTGRES_PASSWORD: secretdocker compose config печатает итоговый файл после подстановки. Если переменной нет, Compose предупредит: The "DB_PASSWORD" variable is not set. Defaulting to a blank string. Это значит, что вы не создали .env или запустили команду не из той папки.
compose.yaml
Заголовок раздела «compose.yaml»services: db: image: postgres:17 restart: unless-stopped environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: shop ports: - "127.0.0.1:5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres -d shop"] interval: 5s timeout: 3s retries: 10
loader: build: . command: ["python", "load_orders.py"] environment: DB_HOST: db DB_PASSWORD: ${DB_PASSWORD} volumes: - ./data:/data:ro depends_on: db: condition: service_healthy
adminer: image: adminer:5 ports: - "8080:8080" depends_on: - db
dbt: build: ./dbt profiles: ["tools"] environment: DB_PASSWORD: ${DB_PASSWORD} DBT_PROFILES_DIR: /project volumes: - ./dbt:/project depends_on: db: condition: service_healthy
volumes: pgdata:build и command
Заголовок раздела «build и command»build: . говорит: не скачивай образ, а собери его из Dockerfile в этой папке. ./dbt — собрать из папки dbt.
command: заменяет CMD из образа. Один образ — две задачи: report.py по умолчанию и load_orders.py для загрузчика.
./data:/data:ro — папка с данными подключена только для чтения (ro).
healthcheck и depends_on
Заголовок раздела «healthcheck и depends_on»Контейнер «запущен» ещё не значит «готов». Postgres стартует несколько секунд. Если загрузчик подключится раньше, он упадёт с Connection refused.
healthcheck— команда, которая проверяет готовность.pg_isreadyотвечает «ок», когда база принимает подключения.condition: service_healthy— запускайloader, только когдаdbв статусеhealthy.
Простой depends_on: [db] ждёт только запуска контейнера, не готовности. Для Adminer этого хватает: он сам повторяет попытки.
profiles
Заголовок раздела «profiles»Служба с profiles: ["tools"] не запускается командой docker compose up. Её запускают явно. Так dbt не стартует вместе с базой.
restart
Заголовок раздела «restart»restart: unless-stopped у базы: Docker поднимет её после сбоя или перезагрузки компьютера. Остановленную вручную не тронет.
| Политика | Что делает |
|---|---|
no (по умолчанию) | Не перезапускает |
on-failure | Перезапускает, если код выхода не 0 |
unless-stopped | Всегда, кроме ручной остановки |
always | Всегда. Даже контейнер, остановленный вручную, запустится после перезапуска Docker |
Загрузчику политика не нужна: он должен выполниться один раз.
Файлы dbt
Заголовок раздела «Файлы dbt»dbt/Dockerfile:
FROM python:3.12-slimRUN pip install --no-cache-dir dbt-core==1.12.6 dbt-postgres==1.11.0WORKDIR /projectENTRYPOINT ["dbt"]Версии закреплены специально. Если поставить только dbt-postgres, pip может взять тестовую версию dbt-core. Закрепляйте обе версии.
ENTRYPOINT ["dbt"] значит: всё, что вы напишете после имени службы, станет аргументами dbt. Команда run превратится в dbt run.
dbt/dbt_project.yml:
name: shopversion: "1.0"profile: shopmodel-paths: ["models"]dbt/profiles.yml:
shop: target: dev outputs: dev: type: postgres host: db port: 5432 user: postgres password: "{{ env_var('DB_PASSWORD') }}" dbname: shop schema: analytics threads: 2host: db — имя службы. Пароль dbt берёт из переменной окружения, а не из файла.
dbt/models/revenue_by_channel.sql:
select channel, count(*) as orders, sum(amount) as revenuefrom public.orderswhere status = 'completed'group by channeldocker compose up -d --build--build пересобирает образы, если файлы изменились. Compose запустит db и adminer. Потом дождётся, когда db станет healthy, и только тогда запустит loader.
Подождите 2–3 секунды. Загрузчику нужно время, чтобы закончить работу.
Статус:
docker compose psNAME IMAGE SERVICE STATUS PORTSdocker-practice-adminer-1 adminer:5 adminer Up 8 seconds 0.0.0.0:8080->8080/tcpdocker-practice-db-1 postgres:17 db Up 8 seconds (healthy) 127.0.0.1:5432->5432/tcpЗагрузчика в списке нет: он уже завершился. Покажите все службы:
docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'SERVICE STATUSadminer Up 8 secondsdb Up 8 seconds (healthy)loader Exited (0) 2 seconds agoЛог загрузчика:
docker compose logs loaderloader-1 | Загружено строк: 500Запустите dbt. Первая сборка образа dbt занимает около минуты:
docker compose run --rm dbt runВ конце вывода:
Completed successfullyDone. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 REUSED=0 TOTAL=1dbt пишет служебные файлы в папку dbt/ на вашем компьютере: target/, logs/, .user.yml. Мы уже добавили их в .gitignore.
Посмотрите результат:
docker compose exec db psql -U postgres -d shop -c \ "select channel, orders, revenue from analytics.revenue_by_channel order by 3 desc" channel | orders | revenue---------+--------+---------- organic | 164 | 23715.82 ads | 158 | 23082.74 email | 96 | 15459.67(3 rows)docker compose run --rm запускает одноразовый контейнер службы и удаляет его после выхода. Так запускают задачи: загрузку, dbt, миграции.
Если порт занят
Заголовок раздела «Если порт занят»Если на компьютере уже что-то слушает порт 8080, вы увидите:
Error response from daemon: ... Bind for 0.0.0.0:8080 failed: port is already allocateddb при этом работает и здоров. Контейнер adminer остаётся в состоянии Created и не запущен. Исправление: поменять левое число в ports:, например "8081:8080", и выполнить docker compose up -d ещё раз.
Практика
Заголовок раздела «Практика»Задание 1
Заголовок раздела «Задание 1»Поднимите стек. Сколько строк загрузил loader?
Проверка: Загружено строк: 500.
Решение
docker compose up -d --builddocker compose logs loaderЗадание 2
Заголовок раздела «Задание 2»Уберите condition: service_healthy у loader, оставив depends_on: [db]. Выполните docker compose down -v, потом up -d. Что случилось с загрузчиком?
Проверка: loader в статусе Exited (1). В логе: connection failed: ... port 5432 failed: Connection refused. На очень быстром компьютере база может успеть стартовать. Но на это нельзя рассчитывать.
Решение
В compose.yaml:
depends_on: - dbdocker compose down -vdocker compose up -ddocker compose ps -a --format 'table {{.Service}}\t{{.Status}}'docker compose logs loaderВерните condition: service_healthy. Только healthcheck гарантирует, что база готова.
Задание 3
Заголовок раздела «Задание 3»Запустите dbt и найдите в базе таблицу (view) analytics.revenue_by_channel.
Проверка: три строки, у organic 164 заказа и выручка 23715.82.
Решение
docker compose run --rm dbt rundocker compose exec db psql -U postgres -d shop -c "select * from analytics.revenue_by_channel order by 3 desc"Задание 4
Заголовок раздела «Задание 4»Смените пароль: поменяйте DB_PASSWORD в .env на newpass. Выполните docker compose down -v, затем up -d. Проверьте, что загрузчик работает с новым паролем.
Проверка: Загружено строк: 500.
Решение
echo "DB_PASSWORD=newpass" > .envdocker compose down -vdocker compose up -ddocker compose logs loaderВ конце верните DB_PASSWORD=secret в .env и снова выполните docker compose down -v и docker compose up -d.
Нужен -v: Postgres задаёт пароль только при первом создании базы. Старый том хранит старый пароль. Без -v загрузчик упадёт с ошибкой password authentication failed for user "postgres".
Задание 5
Заголовок раздела «Задание 5»Добавьте новую строку в конец data/orders.csv и запустите загрузчик снова, не пересоздавая стек. Сколько строк в таблице orders?
Проверка: 501.
Решение
echo "1501,2024-04-01,5,email,99.00,completed" >> data/orders.csvdocker compose run --rm loaderdocker compose exec db psql -U postgres -d shop -tc "select count(*) from orders"Скрипт делает TRUNCATE и грузит файл заново, поэтому повторный запуск безопасен.
Верните файл в исходный вид. Эта команда удаляет последнюю строку и работает на macOS и Linux:
sed -i.bak '$d' data/orders.csv && rm data/orders.csv.bakОстановите стек, когда закончите: docker compose down.
Дальше: Open source за минуты.