Перейти к содержимому
Поиск
Разработка

Docker Compose: PHP, Node.js и базы данных — мастерская и готовые стенды

Собираем окружение приложения: PHP с PostgreSQL и Node.js с Redis. Настраиваем порты, сеть, переменные и тома в мини-мастерской, разбираем команды и скачиваем два готовых стенда.

Александр Некрасов
Docker Compose: PHP, Node.js и базы данных в общей среде разработки

Docker Compose позволяет описать окружение приложения в одном файле и запускать его согласованным набором сервисов. PHP, PostgreSQL, Node.js, Redis, сеть между ними и хранилища данных становятся частью проекта. Коллеге не нужно по переписке выяснять версию базы, порт сервера и список расширений PHP: исходники и конфигурация лежат рядом, а запуск повторяется понятной командой.

Разберём, где это полезно, как устроены контейнеры и почему запущенная база ещё не обязательно готова к запросам. В статье есть мини-мастерская конфигураций и два законченных учебных приложения: PHP с формой заявок и PostgreSQL, Node.js со счётчиком и кешированием отчёта в Redis. Вы сможете изменить настройки, скачать файлы, запустить стенд у себя и проверить результат через браузер и HTTP.

Что работает на странице. Мастерская собирает настоящие compose.yaml и .env.example, показывает связи и команды. Сам Docker в браузере не запускается. Приложения, база и Redis работают после запуска скачанного проекта на компьютере с Docker Engine. Это разделение видно и в интерфейсе мастерской.

Что умеет Docker Compose и что получает разработчик

Представьте небольшой сервис приёма заявок. Страница вызывает PHP, PHP сохраняет заявку в PostgreSQL. Рядом работает Node.js, которому нужен Redis. Без общего описания окружения у каждого участника постепенно появляются свои версии, пароли, ручные команды и забытые настройки. Compose превращает эту информацию в проверяемый файл: можно увидеть изменение в Git, обсудить его и повторить запуск.

Возможность Что это даёт на практике Пример
Запуск нескольких сервисов Один согласованный жизненный цикл окружения Приложение и его база запускаются вместе
Внутренняя сеть Обращение к соседям по имени сервиса PHP подключается к db, Node.js — к redis
Тома данных Данные переживают пересоздание контейнера Заявки остаются после обновления PHP
Сборка образа Системные зависимости описаны рядом с кодом PDO PostgreSQL устанавливается в Dockerfile
Логи и команды Диагностика без поиска случайного процесса Посмотреть ошибку app или зайти в db

Compose особенно полезен там, где у приложения есть зависимости. Для одной статической страницы контейнеры могут ничего не упростить. Для проекта с базой, кешем, обработчиком задач и разными версиями языка выигрыш становится заметнее. При этом инструмент не исправляет ошибки программы: он помогает воспроизвести среду, в которой их можно найти.

Официальная документация: Возможности Docker Compose.

Где применять: команда, локальная разработка, тесты и сервер

Первый сценарий — подключение нового разработчика. Вместо установки базы в систему он получает репозиторий, пример настроек и инструкцию. Второй — работа над несколькими заказами: одному проекту нужен PostgreSQL, другому Redis, и их окружения не должны делить случайные таблицы и порты. Третий — воспроизведение ошибки: команда запускает одну версию кода с одинаковой схемой сервисов.

Четвёртый сценарий — интеграционный тест. Проверка должна пройти через настоящий HTTP-обработчик и настоящее хранилище, а не только через подставную функцию в unit-тесте. Compose удобно поднимает такое окружение перед проверками. Пятый — демонстрация заказчику или тестовый сервер, где нужен небольшой, понятный набор компонентов на одном хосте.

Для простого рабочего сервиса Compose тоже может быть частью развёртывания. Однако подготовка рабочего сервера включает домен, TLS, права, резервные копии, наблюдение и обновления. Два учебных стенда из этой статьи рассчитаны на локальную практику: они показывают связи и поведение данных, а не заменяют проектирование промышленной инфраструктуры.

Мини-мастерская: соберите свой стенд и скачайте конфигурацию

Выберите PHP с PostgreSQL или Node.js с Redis. Измените имя проекта и внешний порт, посмотрите, как меняются схема, YAML и команды. Название тома поможет понять, какая часть отвечает за постоянные данные. Мастерская проверяет допустимые значения и сохраняет результат в файлы, которые нужно положить в соответствующий каталог скачанного проекта.

Семь практических разборов объясняют порт, сеть, PHP, Node.js, том, готовность сервисов и жизненный цикл. У каждого разбора есть задача: например, найти адрес базы из контейнера или выбрать действие после изменения Dockerfile. Начните с настроек по умолчанию; затем меняйте по одному параметру и сравнивайте результат. Так проще увидеть причинно-следственную связь.

Открыть мастерскую на весь экран ↗Скачать два стенда и исходники ZIP ↓

Сгенерированный YAML использует файлы приложения из ZIP. Скачивание одного compose.yaml не заменяет Dockerfile, исходники и SQL. Если меняете имя проекта или тома, существующие данные не переносятся автоматически: новый набор настроек может подключить другое хранилище.

Если браузер сохранил env.example без начальной точки, переименуйте файл в .env.example перед дальнейшими действиями. Вкладка быстрого старта позволяет выбрать команды для PowerShell или Linux/macOS. При повторной настройке не перезаписывайте свой рабочий .env вслепую: сравните значения и сохраните нужные изменения.

Быстрый старт: от архива до страницы приложения

Скачайте и распакуйте архив в отдельную рабочую папку. Внутри находятся два независимых проекта: php-postgres и node-redis. Выберите один, скопируйте .env.example в .env, проверьте конфигурацию и запустите сборку. Все команды ниже выполняются из указанного каталога, а не из произвольной папки терминала.

Первый запуск PHP и PostgreSQL

cd php-postgres
# PowerShell:
Copy-Item .env.example .env
# Linux/macOS вместо Copy-Item: cp .env.example .env
docker compose config -q
docker compose up -d --build --wait
docker compose ps

Откройте http://127.0.0.1:8088, введите учебное имя клиента и текст заявки, отправьте форму. После обновления страницы запись должна остаться. Для второго проекта перейдите в node-redis и повторите подготовку; его страница открывается на http://127.0.0.1:3088. Если порт изменён в мастерской, используйте именно новое значение.

Первый запуск обычно дольше следующих: Docker скачивает базовые образы, устанавливает зависимости и создаёт том базы. Пока команда выполняется, не нужно многократно запускать её заново. Если ожидание завершилось ошибкой, посмотрите состояние и логи; раздел диагностики ниже помогает понять, где прервалась цепочка.

Официальная документация: docker compose up.

Что установить и как проверить, что Docker действительно работает

Для этих примеров нужны Docker Engine с Linux-контейнерами, Docker CLI и современный Compose. Docker Desktop объединяет эти компоненты на поддерживаемых системах; на Linux можно использовать Engine и Compose plugin. Конкретные требования к Windows, WSL и виртуализации проверяйте по инструкции установки для своей системы.

Проверка клиента, Compose и соединения с Engine

docker --version
docker compose version
docker info

Первые две команды подтверждают наличие программ командной строки. Только ответ сервера в docker info показывает, что клиент смог подключиться к Docker Engine. Ошибка подключения к daemon не означает, что сломан YAML: сначала запустите установленный движок и проверьте выбранный context. Не переключайте случайно удалённый рабочий сервер ради учебного запуска.

В статье используются Linux-образы. PHP, Node.js и PostgreSQL внутри контейнеров не требуют отдельной установки на компьютере разработчика. Python 3 нужен только для дополнительного HTTP smoke-скрипта из архива; основные страницы можно проверить браузером. На 7 ноября 2026 года официальный latest-релиз Compose — v5.7.0; это справочная дата, а не требование обновлять исправно работающую среду без проверки совместимости.

Официальная документация: Установка Compose, Compose v5.7.0.

docker compose, docker-compose и строка version

В старых руководствах встречается команда docker-compose и начало файла version: "3.8". В этой статье используется современная команда docker compose и Compose Specification. Номер версии CLI и исторический номер формата YAML — разные вещи. Добавление version не устанавливает нужную программу и не включает автоматически новые возможности.

В актуальном формате верхнее поле version устарело; конфигурация начинается с имени проекта или services. Если старый клиент не понимает параметр, нужно сверить его версию и документацию. Бесконечно менять число 3.8 на 3.9 бесполезно. Для переноса старого проекта сначала полезно получить итоговый результат через docker compose config и сравнить его с ожидаемой схемой.

Официальная документация: Версии Compose и формата файла, version и имя проекта.

Образ, контейнер, сервис, сборка, сеть и том — простыми словами

Термин Что означает В нашем проекте
Image — образ Подготовленная файловая система и настройки запуска PHP с Apache и установленным pdo_pgsql
Container — контейнер Конкретный экземпляр запуска образа Работающий процесс PHP или PostgreSQL
Service — сервис Описание роли в compose.yaml app, db или redis
Build — сборка Получение образа по Dockerfile и контексту Копирование PHP-кода и установка расширения
Network — сеть Связь между контейнерами проекта app видит db по имени
Volume — том Хранилище с отдельным от контейнера жизненным циклом pgdata для записей PostgreSQL

Сервис и контейнер часто совпадают в небольшом проекте, но понятия различаются: сервис — инструкция, контейнер — запущенный экземпляр. Образ можно пересобрать, контейнер заменить, а том подключить снова. Именно это позволяет обновлять код без намеренного удаления базы. Файлы, записанные только внутрь удаляемого контейнера, такой гарантии не имеют.

Dockerfile отвечает на вопрос «из чего собрать приложение». Compose отвечает «какие компоненты запустить вместе и как их соединить». SQL, JavaScript и PHP по-прежнему описывают работу самого приложения. Это три уровня проекта: код задачи, образ исполнения и схема окружения. Если держать их отдельно в голове, конфигурация перестаёт выглядеть набором загадочных ключей.

Стенд № 1: PHP, Apache и PostgreSQL

Этот пример подходит для заявки с сайта, внутренней формы, маленького кабинета или административного списка. Сервис app принимает HTTP-запросы, проверяет поля и выполняет SQL через PDO. Сервис db хранит записи. Браузер обращается только к приложению, а база не получает опубликованного порта на компьютере.

Полный compose.yaml из архива: PHP + PostgreSQL

name: aen-compose-php
services:
  app:
    build: .
    ports:
      - "127.0.0.1:${APP_PORT:-8088}:80"
    environment:
      DB_HOST: db
      DB_PORT: "5432"
      DB_NAME: ${POSTGRES_DB:-compose_demo}
      DB_USER: ${POSTGRES_USER:-compose_demo}
      DB_PASSWORD: ${POSTGRES_PASSWORD:?Copy .env.example to .env first}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1/api.php?action=health') === false ? 1 : 0);"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    init: true
    restart: unless-stopped
  db:
    image: postgres:18.6-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-compose_demo}
      POSTGRES_USER: ${POSTGRES_USER:-compose_demo}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Copy .env.example to .env first}
    volumes:
      - pgdata:/var/lib/postgresql
      - ./db/init.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
volumes:
  pgdata:

Читайте конфигурацию сверху вниз. build указывает место сборки приложения. ports открывает только веб-страницу. environment передаёт настройки подключения. depends_on связывает запуск приложения с готовностью базы. Раздел volumes объявляет постоянное хранилище. Это описание конкретной учебной системы, которое можно сравнить со схемой в мастерской.

В PostgreSQL 18 официальный образ изменил организацию каталога данных. Поэтому в этом проекте том подключён к /var/lib/postgresql, а не скопирован механически из примера для старой версии. При смене major-версии базы нужно отдельно проверять правила миграции данных; замена тега образа сама по себе их не выполняет.

Официальная документация: Официальный образ PostgreSQL.

Dockerfile для PHP: почему расширение нужно устанавливать явно

Dockerfile PHP-приложения из исходников

FROM php:8.5.12-apache-bookworm
RUN apt-get update \
    && apt-get install -y --no-install-recommends libpq-dev \
    && docker-php-ext-install pdo_pgsql \
    && rm -rf /var/lib/apt/lists/*
COPY src/ /var/www/src/
COPY public/ /var/www/html/
RUN printf 'expose_php=Off\ndisplay_errors=Off\nlog_errors=On\nerror_log=/proc/self/fd/2\n' > /usr/local/etc/php/conf.d/demo.ini
EXPOSE 80

Наличие PHP не означает наличие всех драйверов баз. Для PostgreSQL приложению нужен pdo_pgsql; установка описана в Dockerfile, поэтому её не приходится повторять вручную после каждого пересоздания контейнера. Apache обслуживает HTTP, а исходники попадают в подготовленный каталог приложения.

В учебном примере PHP-код копируется в образ. Когда вы меняете файл формы или обработчика на компьютере, уже работающий контейнер не получает изменение автоматически. Выполните docker compose up -d --build --wait. Такой способ удобно показывает разницу между исходниками, сборкой и запущенным экземпляром. Для активной разработки можно добавить bind mount, но это отдельная осознанная настройка.

Зависимости приложения и системные библиотеки — тоже разные уровни. Если ваш проект использует пакеты PHP, добавьте установку из composer.lock в сборку. Команды и принцип фиксации зависимостей разобраны в руководстве по Composer. Не копируйте случайный локальный vendor с другой платформы, если хотите повторяемый результат.

Как PHP читает настройки и сохраняет заявку через PDO

Важная деталь подключения — имя сервера db. Оно разрешается во внутренней сети Compose. Внутри PHP-контейнера localhost указывает на сам PHP-контейнер, где PostgreSQL не запущен. Поэтому адрес, удобный для браузера на компьютере, нельзя бездумно использовать в строке подключения приложения.

Принцип подключения и параметризованной записи; полный обработчик находится в ZIP

$pdo = new PDO(
    sprintf('pgsql:host=%s;port=5432;dbname=%s', getenv('DB_HOST'), getenv('DB_NAME')),
    getenv('DB_USER'),
    getenv('DB_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$statement = $pdo->prepare(
    'INSERT INTO requests (customer, note) VALUES (:customer, :note)'
);
$statement->execute(['customer' => $customer, 'note' => $note]);

Здесь $customer и $note — уже проверенные сервером значения запроса. Подготовленный SQL передаёт их как параметры. Это не заменяет проверку длины и обязательности полей: SQL-защита и бизнес-валидация решают разные задачи. Полный обработчик в архиве возвращает JSON, различает некорректный ввод и недоступность хранилища, а интерфейс показывает результат человеку.

Для проверки используйте три действия: отправить корректную заявку, попробовать пустое обязательное поле и обновить список. В первом случае должна появиться запись, во втором — понятная ошибка без новой строки в базе, в третьем — сохранённые данные. Это полезнее проверки одной надписи «сервер запущен»: вы проходите реальный путь от формы до постоянного хранилища.

Тот же путь можно проверить напрямую через HTTP, без формы

# PowerShell: отправка JSON и чтение списка
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:8088/api.php?action=create' -ContentType 'application/json; charset=utf-8' -Body '{"customer":"Demo team","note":"Check Compose"}'
Invoke-RestMethod -Uri 'http://127.0.0.1:8088/api.php?action=list'

Официальная документация: Подготовленные запросы PDO.

Стенд № 2: Node.js, Redis, счётчик и кеш отчёта

Второй пример показывает задачи, для которых Redis часто добавляют к веб-сервису: быстрое состояние, счётчики и временный кеш. Node.js принимает HTTP-запросы, увеличивает значение и возвращает статистику. Отдельный endpoint отчёта демонстрирует TTL: результат некоторое время используется повторно, затем рассчитывается заново.

Полный compose.yaml из архива: Node.js + Redis

name: aen-compose-node
services:
  app:
    build: .
    ports:
      - "127.0.0.1:${APP_PORT:-3088}:3000"
    environment:
      PORT: "3000"
      REDIS_URL: redis://redis:6379
      CACHE_TTL: ${CACHE_TTL:-20}
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    init: true
    restart: unless-stopped
  redis:
    image: redis:8.10.3-alpine
    command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"]
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
volumes:
  redisdata:

Веб-приложение слушает порт 3000 внутри контейнера, с компьютера доступен 3088. Redis слушает свой внутренний порт и не публикуется наружу. Адрес подключения приложения — redis://redis:6379. Имя redis берётся из названия сервиса, поэтому не нужно запоминать динамический IP контейнера.

Нажмите увеличение счётчика, обновите страницу и запросите отчёт несколько раз. Сравните признаки попадания в кеш и оставшееся время жизни, затем повторите после истечения TTL. Так видно, какие данные должны переживать перезапуск, а какие специально устаревают. Redis может использоваться и для очередей задач, но полноценная очередь требует дополнительных правил обработки, повторов и подтверждения выполнения.

Официальная документация: Клиент node-redis.

Node.js в контейнере: зависимости, HTTP и остановка

Dockerfile Node.js из исходников

FROM node:24.22.2-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
COPY --chown=node:node src/ ./src/
COPY --chown=node:node public/ ./public/
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]

Файл package-lock.json фиксирует дерево зависимостей, а npm ci устанавливает его согласованно с package.json. Это помогает избежать ситуации, когда два разработчика собирают разные пакеты под одним названием проекта. Обновление зависимости становится отдельным изменением: обновили lock-файл, прогнали проверки, пересобрали образ.

Внутри контейнера HTTP-сервер должен слушать доступный сетевой интерфейс, обычно 0.0.0.0, а ограничение доступа с компьютера задаётся публикацией порта. Если слушать только внутренний 127.0.0.1, соседние контейнеры и перенаправленный порт могут не достучаться до приложения. Эти два адреса относятся к разным уровням сети.

Обработчик должен корректно отвечать при потере Redis и закрывать соединения при остановке. Compose управляет процессами, однако смысл ошибки определяет приложение: невозможно выдать успешное сохранение, если запись не попала в хранилище. Для более крупного HTTP API можно взять структуру из мастерской Fastify и подключить её к аналогичному окружению.

Основные операции клиента Redis; полный HTTP-сервер с обработкой ошибок находится в архиве

import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
redis.on('error', error => console.error('Redis connection:', error.message));
await redis.connect();

// Атомарное увеличение общего значения.
const counter = await redis.incr('compose:counter');

// Отдельный учебный ключ с ограниченным сроком жизни.
await redis.set('example:report', JSON.stringify({ counter }), { EX: 20 });
const report = await redis.get('example:report');
console.log(report === null ? 'Кеш отсутствует или истёк' : JSON.parse(report));
await redis.close();

MISS при создании снимка и HIT при повторном запросе до истечения TTL

# PowerShell: увеличить счётчик и дважды запросить отчёт
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:3088/api/increment' -ContentType 'application/json' -Body '{}'
Invoke-RestMethod -Uri 'http://127.0.0.1:3088/api/report'
Invoke-RestMethod -Uri 'http://127.0.0.1:3088/api/report'

Официальная документация: Установка из package-lock.json, HTTP в Node.js 24.

Сеть: почему db и redis работают, а localhost путает

Откуда идёт запрос Куда обращаться Почему
Браузер на компьютере 127.0.0.1:8088 или 127.0.0.1:3088 Это опубликованный порт хоста
PHP-контейнер db:5432 Имя PostgreSQL-сервиса во внутренней сети
Node.js-контейнер redis:6379 Имя Redis-сервиса во внутренней сети
Внутри самого app 127.0.0.1 и внутренний HTTP-порт Так healthcheck проверяет свой процесс

Не записывайте IP контейнера в .env: после пересоздания адрес может измениться. Используйте стабильное имя сервиса. Соседям в общей сети не требуется публикация порта через ports, чтобы обращаться друг к другу. А отсутствие ports не является системой прав внутри этой сети: подключённые к ней компоненты всё ещё должны иметь разумный доступ к данным.

В простом стенде достаточно сети по умолчанию. В большом приложении можно отделить внешний слой от базы дополнительными сетями. Но схема должна оставаться объяснимой: нарисуйте, кто к кому обращается, и оставляйте только необходимые связи. Для начала полезнее правильно понять одну сеть, чем добавлять несколько ради количества строк в YAML.

Официальная документация: Сеть и имена сервисов.

Порты: как избежать конфликта и случайного открытия базы

Запись 127.0.0.1:8088:80 читается так: адрес интерфейса хоста, порт хоста, порт контейнера. Веб-сервер внутри продолжает слушать 80, даже если снаружи вы выбрали 8089. Для второго учебного проекта аналогично используется внутренний 3000. Меняется точка входа с компьютера, а не протокол работы сервера.

Привязка к 127.0.0.1 рассчитана на доступ с этого компьютера. Если убрать адрес и оставить только пару портов, область доступа может стать шире. Поэтому мастерская сохраняет локальную привязку и не предлагает открыть базу в интернет одним переключателем. Для удалённой демонстрации сначала проектируют доступ, авторизацию и сетевые правила.

Если порт занят, выберите другой внешний порт в мастерской или .env. Не нужно останавливать неизвестный процесс. После изменения пересоздайте сервис через up -d и используйте новый URL. Два проекта могут иметь сервис с одинаковым именем app, но не могут одновременно занять один и тот же адрес и порт хоста.

Если .env уже существует, обновите APP_PORT и в нём. Число после :- в сгенерированном YAML является запасным: непустое значение из .env или оболочки имеет приоритет. Поэтому замена только YAML при сохранённой старой настройке может оставить прежний внешний порт.

Официальная документация: Параметры services.

Переменные: подстановка в YAML и окружение контейнера

Файл .env рядом с Compose прежде всего даёт значения для подстановки выражений ${...}. Это ещё не означает, что каждая строка автоматически попадёт в PHP или Node.js. Внутреннее окружение сервиса задаётся отдельно через environment или env_file. Путаница между этими этапами — частая причина пустого getenv().

Два этапа: Compose подставляет значение, сервис получает переменную

# .env
APP_PORT=8088
DB_NAME=workshop

# Фрагмент compose.yaml
services:
  app:
    ports:
      - "127.0.0.1:${APP_PORT:-8088}:80"
    environment:
      DB_NAME: ${DB_NAME:?Укажите DB_NAME}

Сначала Compose читает значение порта и имени базы, затем создаёт контейнер с указанной переменной DB_NAME. В PHP её читает getenv("DB_NAME"), в Node.js — process.env.DB_NAME. Переменные оболочки могут переопределять одноимённые значения файла, поэтому при неожиданном результате проверьте и терминал, из которого запускаете команду.

Синтаксис :- задаёт запасное значение, а :? позволяет остановиться с понятной ошибкой, если обязательный параметр пуст. Для общей работы с конфигурацией PHP есть отдельная статья о phpdotenv и getenv(). Библиотека загрузки .env внутри приложения и подстановка Compose выполняются на разных этапах.

Официальная документация: Подстановка переменных.

Секреты: чего .env не делает и когда нужны secrets

Имя файла .env не шифрует его содержимое. Рабочие пароли не следует коммитить в Git, включать в ZIP и выводить в диагностические ответы. В архиве находятся только учебные значения для отдельного локального стенда. В настоящем проекте храните пример имён переменных в .env.example, а реальные значения передавайте через подходящий механизм окружения или секретов.

Compose поддерживает secrets: сервису предоставляется конкретный файл, обычно в /run/secrets/.... Программа или образ должны уметь его прочитать; соглашение переменных _FILE поддерживается не каждым приложением автоматически. Локальный file-based secret всё ещё хранится на хосте: права, резервные копии и доступ к машине остаются важными.

Не вставляйте секрет в Dockerfile через COPY и не используйте его как обычный аргумент сборки, рассчитывая, что удаление следующей строкой всё исправит. Для приватных зависимостей у сборки есть отдельный механизм secret mounts. Проверяя итоговую конфигурацию, помните: полный вывод docker compose config может содержать подставленные значения. Для короткой проверки используйте config -q.

Официальная документация: Передача secrets, Проверка конфигурации.

depends_on и healthcheck: процесс запущен, но готов ли сервис

База может уже иметь работающий процесс, но ещё создавать каталог данных и таблицы. Если приложение в этот момент отправит SQL, оно получит ошибку. Простой порядок запуска решает только часть задачи. В наших конфигурациях база и Redis имеют healthcheck, а приложение ожидает состояние service_healthy.

Сокращённая схема ожидания PostgreSQL

depends_on:
  db:
    condition: service_healthy

# В определении сервиса db:
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
  interval: 5s
  timeout: 3s
  retries: 20

Двойной знак доллара нужен, чтобы значение прочитала оболочка внутри контейнера, а не преждевременно подставил Compose. Сам pg_isready проверяет готовность PostgreSQL принимать подключения. HTTP healthcheck приложения проверяет другой уровень — доступность его собственного обработчика. Даже хороший healthcheck не заменяет сценарий создания записи.

Если база станет недоступна через час, первоначальный depends_on не восстановит логику запроса. Приложению нужны обработка ошибки, переподключение и понятный ответ клиенту. Состояние unhealthy само по себе также не означает автоматический перезапуск контейнера: это сигнал о проверке, а политика восстановления задаётся отдельно.

Официальная документация: Порядок запуска и готовность.

Где живут данные: named volume и bind mount

Named volume подходит для данных, которыми управляет сервис: файлов PostgreSQL, Redis и других хранилищ. Docker создаёт и подключает том независимо от файлов приложения. Bind mount связывает конкретный путь компьютера с путём контейнера; это удобно для редактируемого кода или конфигурации. Эти варианты не взаимозаменяемы по последствиям.

Ситуация Что выбрать Что учесть
Постоянная база учебного стенда Named volume Пересоздание контейнера не равно удалению тома
Редактирование кода без сборки Bind mount Файлы контейнера под точкой монтирования будут скрыты
Конфигурация только для чтения Bind mount с read_only Контейнеру не нужен доступ на запись
Перенос данных на другой компьютер Экспорт и проверенное восстановление Имя тома не переносит его содержимое

У проекта есть имя, у тома — собственная идентичность. Если вы переименовали проект или хранилище и увидели пустую базу, не спешите удалять старое: вероятно, подключился новый том. Верните прежние настройки и проверьте список хранилищ. Мастерская позволяет увидеть эти параметры, но намеренно не переносит и не очищает реальные данные.

Начальный SQL в образе PostgreSQL выполняется при инициализации пустого каталога. Изменение этого SQL после появления данных не является миграцией. Для развивающегося приложения нужны отдельные миграции схемы: контролируемые изменения с порядком выполнения и проверкой результата.

Официальная документация: Постоянные тома, Bind mounts, Официальный образ PostgreSQL.

Ежедневные команды: запуск, остановка, пересборка и удаление

Команда Когда применять Результат
docker compose up -d --build --wait Первый запуск или изменение кода образа Сборка, создание и ожидание готовности
docker compose ps Проверить состояние Список сервисов, состояние и порты
docker compose stop Временно остановить работу Контейнеры остаются
docker compose start Вернуться после stop Запуск существующих контейнеров
docker compose restart app Перезапустить текущий процесс Новый код и настройки сами не собираются
docker compose down Убрать контейнеры и сеть стенда Именованные тома по умолчанию сохраняются

Команда restart не подхватывает изменённый Dockerfile и не создаёт контейнер с новым окружением. После изменения конфигурации используйте up -d, после изменения копируемого кода — up -d --build. Это важное различие: можно десять раз перезапустить старый образ и всё ещё видеть старую страницу.

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

Официальная документация: docker compose down, docker compose up.

Логи, exec и диагностика без угадывания

Примеры диагностики PHP-стенда; имя пользователя и базы сверяйте с .env

docker compose ps
docker compose logs --tail=100 app
docker compose logs --tail=100 db
docker compose exec app php -m
docker compose exec db psql -U compose_demo -d compose_demo -c "SELECT count(*) FROM requests;"

Начните с того, какой компонент не отвечает. Страница вообще недоступна — проверьте app и порт. Страница открылась, но заявка не сохранилась — смотрите HTTP-ответ и логи приложения, затем состояние базы. Ошибка драйвера PDO — проверьте расширения PHP. Неверный пароль после изменения .env — вспомните, что у уже инициализированной базы смена переменной не меняет автоматически существующую роль.

Для Node-стенда имя второго сервиса — redis: можно выполнить docker compose exec redis redis-cli ping и ожидать PONG. Это подтверждает ответ Redis, но не правильность всего API. Команда exec работает в существующем запущенном контейнере, поэтому сначала полезен ps. Для скриптов и перенаправлений используйте exec -T, когда интерактивный терминал не нужен.

Логи следует ограничивать нужным периодом и количеством строк. Не публикуйте их целиком без просмотра: исключения, URL и конфигурация могут содержать личные данные или секреты. Хороший отчёт об ошибке содержит команду, ожидаемый результат, статус сервиса и короткий относящийся к проблеме фрагмент.

Официальная документация: Просмотр логов, Команды внутри контейнера.

Как проверять стенд: YAML, HTTP, данные и перезапуск

У проверки есть несколько независимых уровней. config -q подтверждает, что Compose принимает структуру и подстановки. Сборка подтверждает доступность образов и зависимостей. Healthcheck показывает базовую готовность. HTTP-сценарий проверяет действие приложения. Повтор после перезапуска показывает, где действительно сохраняется состояние. Ни один ранний уровень не заменяет следующие.

HTTP smoke-проверки из архива

# Из корня распакованного архива, когда стенды уже запущены:
python scripts/smoke.py php http://127.0.0.1:8088
python scripts/smoke.py node http://127.0.0.1:3088

PHP-проверка должна пройти путь создания и чтения учебной заявки. Node-проверка — увеличение счётчика и получение отчёта. Затем запомните одну запись или значение, выполните обычный down, снова up -d --wait и убедитесь, что постоянные данные сохранились. Для проверки на своём проекте используйте отдельное окружение и синтетические записи.

Smoke-скрипт добавляет учебную заявку или увеличивает счётчик, но не очищает базу. Проверка Redis действительно ждёт истечения TTL; по умолчанию это 20 секунд. Если вы задали TTL больше 30 секунд, используйте параметр --skip-ttl и проверьте истечение вручную: ограничение ожидания скрипта не является ограничением Redis.

В архиве предусмотрены проверки кода отдельно от Docker. Подставной Redis и локальная тестовая база позволяют быстро проверить ветви обработчика, но не доказывают работу контейнерной сети или PostgreSQL в образе. Для браузерных действий можно добавить тесты Playwright: заполнение формы, ошибки и сохранение результата после обновления.

Что проверено 7 ноября 2026 года. Оба стенда собраны и запущены в настоящих Linux-контейнерах: Docker Engine 29.9.2, Compose 5.6.1. Все четыре сервиса получили состояние healthy. Проверены создание и чтение заявки через PHP и PostgreSQL, ошибки HTTP, Redis INCR, повторное чтение кеша и его истечение через 20 секунд. Перезапуск app и цикл down → up сохранили заявку и счётчик; значения дополнительно прочитаны командами SQL и Redis. Отчёты находятся в workshop/evidence/ архива. Это проверка конкретной локальной сборки, поэтому после изменения версий, настроек или платформы повторите сценарии в своей среде.

Резервные копии: том не равен бэкапу

Том помогает пережить замену контейнера, но не защищает от удаления, повреждения диска или ошибочного SQL. Для PostgreSQL учебного стенда можно сделать логический экспорт через pg_dump. Чтобы избежать особенностей двоичного перенаправления разных оболочек, сначала создайте файл внутри контейнера, затем скопируйте его на компьютер.

Учебный экспорт; замените compose_demo, если изменили пользователя или базу

docker compose exec -T db pg_dump -U compose_demo -d compose_demo -Fc -f /tmp/workshop.dump
docker compose cp db:/tmp/workshop.dump ./workshop.dump

Факт появления файла — только начало. Восстановите его в отдельную тестовую базу, проверьте записи и приложение. Храните копию вне диска, потерю которого она должна пережить. Для рабочего сервиса отдельно выбирают периодичность, срок хранения, шифрование, доступ и допустимую потерю последних изменений. Логический экспорт одной базы также не является копией всех ролей и всей инфраструктуры.

В Redis включён AOF с синхронизацией everysec. Это полезно для учебного счётчика, но не обещает абсолютной сохранности при аварии: последние изменения могут быть потеряны. Кешируемый отчёт специально живёт ограниченное время и может быть создан заново. Сначала определите, какие данные являются источником истины, а какие восстановимым ускорителем.

Официальная документация: Экспорт PostgreSQL через pg_dump, Сохранение данных Redis.

Версии, lock-файлы и воспроизводимость сборки

В исходниках указаны конкретные теги базовых образов, а не latest. Это делает выбор версии заметным для команды. Однако тег может быть перевыпущен: для строгой фиксации используют digest образа. Такая фиксация не отменяет обновления безопасности — она превращает обновление в контролируемое изменение с новой сборкой и проверками.

Повторяемость зависит не только от Docker. npm использует package-lock.json, Composer — composer.lock. Системные пакеты, архитектура процессора и доступность внешних репозиториев тоже влияют на сборку. Поэтому точнее говорить об управляемой и проверяемой среде, чем обещать побайтово одинаковый результат от любого компьютера через несколько лет.

В .dockerignore исключают лишнее: Git-историю, локальные секреты, кеши и ненужные каталоги зависимостей. Контекст сборки должен содержать то, что необходимо образу. После обновления базы проверяйте совместимость формата данных и процедуру миграции; после обновления языка — тесты приложения и расширения. Обновление тега не является доказательством успешного обновления системы.

Официальная документация: Сборка и фиксация образов.

Разработка, CI и рабочий сервер: что меняется

В разработке удобны подробные ошибки и быстрый доступ к коду. В CI важнее предсказуемый старт, отдельные тестовые данные и понятный exit code. На рабочем сервере нужны ограниченные права, корректные таймауты, доступность после перезагрузки, защищённый вход и наблюдение. Один файл может быть основой для этих сред, но параметры не должны смешиваться случайно.

Принцип наложения конфигурации; compose.production.yaml создаётся под конкретный сервис

docker compose -f compose.yaml -f compose.production.yaml config -q
docker compose -f compose.yaml -f compose.production.yaml up -d --build --wait

Дополнительный файл выше не входит в учебный архив как готовое решение для любого сервера. Сначала нужно описать реальные отличия: reverse proxy, TLS, имена доменов, источники секретов, политику логов и ресурсов. После объединения файлов всегда проверяйте итоговую конфигурацию: некоторые списки объединяются, и ожидаемое «заменить порт» может потребовать специального правила.

Профили позволяют включать необязательные инструменты, например административный клиент базы только по запросу. Это полезно, когда вспомогательный интерфейс не должен жить постоянно. Не превращайте профиль в систему авторизации: если инструмент доступен по сети, его собственные права и защита всё равно необходимы.

Официальная документация: Compose на сервере, Профили Compose.

Границы Compose: когда нужен другой уровень инфраструктуры

Compose удобно описывает приложение на выбранном Docker-хосте. Он не превращает один сервер в отказоустойчивый кластер. Если этот сервер недоступен, локальные контейнеры и тома тоже недоступны. Два экземпляра приложения на одной машине не защищают от отказа самой машины, а увеличение числа контейнеров не создаёт автоматически общую базу или балансировку.

Когда появляются требования к нескольким узлам, планированию размещения, автоматическому переносу нагрузки и сложным обновлениям, стоит рассматривать оркестрацию и управляемые сервисы. Kubernetes решает другой масштаб задач, но требует своих знаний и эксплуатации. Его не нужно выбирать только потому, что слово кажется следующим обязательным шагом после Docker.

Для личного сервиса, небольшой внутренней системы или тестового стенда прозрачный Compose может быть удобнее сложной платформы. Выбор определяется временем восстановления, допустимым простоем, ростом нагрузки и возможностями команды. Сначала опишите эти требования, затем выбирайте инструменты. Самый длинный YAML не делает проект более надёжным.

Частые проблемы и быстрый маршрут диагностики

Симптом Вероятная причина Что проверить
Cannot connect to Docker daemon Клиент установлен, Engine недоступен docker info, запуск движка и выбранный context
Port is already allocated Порт занят другим процессом Изменить внешний APP_PORT, повторить up
PHP не видит PostgreSQL Неверный host или отсутствует драйвер DB_HOST=db, php -m, логи app
Сервис running, но запросы падают Запуск не равен готовности healthcheck, depends_on и ответ API
Новый пароль из .env не подходит База уже инициализирована Существующая роль и управляемая смена пароля
Изменил код, вижу старую страницу Код находится в старом образе up -d –build, а не только restart
После переименования пустая база Подключён другой проект или том Вернуть прежнее имя и проверить тома
YAML скачан, сборка не начинается Не хватает файлов контекста Распаковать весь ZIP, выбрать правильный каталог

Разбирайте одну проблему за раз. Сначала клиент и Engine, затем конфигурация, сборка, состояние сервисов, сеть и действие приложения. Не удаляйте тома в надежде исправить любой сбой: этим легко потерять единственный полезный след. Сохранённая заявка, короткий HTTP-ответ и несколько строк логов часто объясняют больше, чем повторная установка всего окружения.

Исходники и следующий шаг: адаптируйте стенд к своей задаче

В ZIP находятся оба приложения, Dockerfile, Compose-конфигурации, пример переменных, схема базы, lock-файл Node.js, проверки, инструкции и лицензии. Начните с неизменённого варианта. Затем добавьте одно поле заявки или новый показатель счётчика, повторите сборку и HTTP-проверку. После этого перенесите принцип на собственный сервис: замените обработчик, сохраните понятную сеть, настройки и отдельное хранилище.

Открыть мастерскую на весь экран ↗Скачать два стенда и исходники ZIP ↓

Главный результат практики — умение объяснить путь запроса целиком: браузер → опубликованный порт → приложение → имя сервиса базы → постоянные данные. Вы будете понимать, что пересобирать после изменения кода, где искать ошибку, что переживает перезапуск и какие настройки нужны коллеге для повторения окружения. Именно из таких проверяемых шагов получается удобная рабочая инфраструктура.

Основные источники — документация Docker Compose, Compose Specification, руководства PHP, Node.js, PostgreSQL и Redis, ссылки на которые приведены рядом с примерами. Проверяйте актуальные требования при обновлении образов и сохраняйте подтверждённый набор версий вместе с проектом.