{ "index": 286, "slug": "editorial-2020-01-field-docker-local", "title": "Старый PHP в Docker: как воспроизводимо поднять локальный проект", "excerpt": "Проект требует версию PHP, которой нет на компьютере разработчика. Разбираем, как описать окружение в Docker, проверить границы между образом, контейнером и данными и не превратить чистый запуск в потерю локальной базы.", "contentHtml": "

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

\n

В такой ситуации Docker полезен не как кнопка «запустить всё», а как описание границы проекта. Образ фиксирует базовое окружение и набор установленных пакетов, контейнер запускает процесс, bind mount (прямое подключение пути с хоста) отдаёт текущий код, а named volume хранит данные отдельно от контейнера. Сначала нужно разделить эти слои. Только после этого становится понятно, что пересобирать и что нельзя удалять.

\n

Что именно должно быть одинаковым

\n

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

\n

Контейнер не делает файлы проекта частью образа автоматически. В типичной схеме исходники подключаются через bind mount, а база пишет в named volume. Поэтому пересоздание контейнера может убрать его записываемый слой, но не очистить базу. И наоборот: удаление volume не исправит старый код, который процесс получает через неверный mount.

\n
Граница локального окружения и способ её проверить
СлойЧто в нём живётНаблюдаемый симптомПроверка
ОбразPHP, расширения, системные пакеты и команда сборкиВ контейнере не та версия PHP или нет расширенияdocker compose images и docker compose exec web php -v
КонтейнерЗапущенный процесс, его параметры и временные измененияПерезапуск возвращает прежнее состояние процессаdocker compose ps, docker inspect и логи
Bind mountФайлы выбранного каталога на хостеПроцесс видит не тот файл, который открыт в редактореСопоставить host path, destination и working_dir
Named volumeДанные базы или другой persistent storageНовая миграция не применяется к «новой» базеПроверить volume в итоговом Compose config и mounts
\n

Минимальная воспроизводимая схема

\n

Ниже учебный фрагмент для проекта, которому нужен PHP 7.4 и PostgreSQL. Версии, имена каталогов и команду запуска нужно заменить на значения конкретного репозитория. Пароль в примере демонстрационный: секреты не следует хранить в Compose-файле или отправлять в общий лог.

\n
services:\n  web:\n    build: .\n    command: php -S 0.0.0.0:8080 -t public\n    ports:\n      - '8080:8080'\n    environment:\n      DATABASE_URL: 'postgres://app:app@db:5432/app'\n    volumes:\n      - .:/srv/app\n\n  db:\n    image: postgres:12.1-alpine\n    environment:\n      POSTGRES_DB: app\n      POSTGRES_USER: app\n      POSTGRES_PASSWORD: app\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n\nvolumes:\n  postgres_data:
\n

Строка .:/srv/app подключает текущий каталог к контейнеру. Если команда запускает PHP из /app, а mount установлен в /srv/app, процесс может работать не с теми файлами. Строка postgres_data:/var/lib/postgresql/data отделяет данные PostgreSQL от жизненного цикла контейнера. При следующем up Compose использует существующий volume, если его не удалили вручную.

\n

Dockerfile должен описывать именно требования приложения, а не случайные пакеты с машины разработчика:

\n
FROM php:7.4-cli\n\nWORKDIR /srv/app\nCOPY --from=composer:2 /usr/bin/composer /usr/bin/composer\nCOPY composer.json composer.lock ./\nRUN composer install --no-interaction --prefer-dist\nCOPY . .\n\nCMD [\"php\", \"-S\", \"0.0.0.0:8080\", \"-t\", \"public\"]
\n

Это не универсальный production-образ. В реальном проекте понадобятся его расширения PHP, системные библиотеки и способ запуска, зафиксированные в исходном Dockerfile. Смысл примера в другом: версия интерпретатора и порядок установки зависимостей находятся в описании окружения, а не в личной настройке ноутбука.

\n
\"Схема
Перед чистым запуском сначала фиксируем конфигурацию, затем меняем только подтверждённый слой и повторяем тот же контрольный маршрут.
\n

Сначала получить итоговую конфигурацию

\n

Исходный YAML не всегда равен тому, что получает Compose. Значения могут прийти из shell, файла .env, другого Compose-файла или флага --env-file. Поэтому диагностику начинаем с итоговой модели, а не с догадок о том, какой файл был прочитан.

\n
docker compose config --services\ndocker compose config\ndocker compose ps\ndocker compose logs --tail=50 web db
\n

В выводе проверяем имя проекта, образ или build context, список mount, рабочий каталог, команду и адрес базы. Если нужно выяснить источник подстановок, команда docker compose config --environment показывает окружение, которое Compose использует для interpolation (подстановки переменных). Такой вывод может содержать секреты, поэтому его не копируют в тикет без редактирования.

\n

После этого проверяем версию PHP внутри контейнера. Команда на хосте и команда в контейнере отвечают на разные вопросы:

\n
php -v\ndocker compose exec web php -v\ndocker compose exec web php -m | sort\ndocker compose exec web composer check-platform-reqs
\n

Первая строка показывает локальную систему и может быть полезна для сравнения. Вторая и третья показывают фактический интерпретатор и расширения процесса приложения. Если composer check-platform-reqs не проходит, сначала сверяем Dockerfile и lock-файл. Обновлять зависимости «до последних» для исправления расхождения не нужно: это меняет задачу и может скрыть исходную причину.

\n

Симптом → гипотеза → ограниченное действие

\n

Один и тот же внешний симптом не доказывает один и тот же слой. Таблица помогает выбрать действие с минимальным радиусом разрушения.

\n
Диагностическая матрица для локального проекта
СимптомГипотезаПроверкаДействие
PHP внутри контейнера старше или новее ожиданияИспользуется старый image или изменился Dockerfile без rebuildСверить build context, image ID и php -vdocker compose build web, затем пересоздать только web
Изменение PHP-файла не видноНеверный bind mount или рабочий каталогСравнить файл на хосте и путь внутри контейнераИсправить mount или working_dir, volume базы не трогать
Миграция видит старые таблицыNamed volume пережил контейнерСверить volume и mount базы через config и inspectСделать дамп; удалить volume только известного тестового проекта
После пересборки нет соединения с БДНеверны URL, имя сервиса или готовность PostgreSQLПроверить итоговую переменную, логи и имя dbИсправить конфигурацию; не расширять очистку
\n

Что пересобирать, а что сохранять

\n

Изменение Dockerfile или composer.lock относится к образу. Начинаем с пересборки и пересоздания сервиса:

\n
docker compose build web\ndocker compose up -d --force-recreate web\ndocker compose exec web php -v\ndocker compose logs --tail=50 web
\n

Если команда после rebuild всё ещё показывает старый PHP, проверяем, какой Compose-файл и проект использованы, не перекрыт ли путь bind mount и не запущен ли контейнер из другого каталога. Статус Up доказывает только, что процесс не завершился сразу. Он не доказывает нужную версию PHP, готовность базы или корректный ответ приложения.

\n

Изменение исходника под bind mount обычно не требует пересборки: сначала проверяем, что контейнер видит тот же файл. Исключение — проект, который копирует код в образ и не подключает его через mount. Тогда проверяем Dockerfile и действительно выполняем rebuild. Не переносим привычку одного режима в другой.

\n

Удаление данных — отдельный сценарий. Перед ним сохраняем дамп и записываем точное имя Compose-проекта и volume:

\n
docker compose exec -T db pg_dump -U app app > backup.sql\ndocker compose down\ndocker volume ls\n# После проверки имени тестового volume:\ndocker volume rm project_postgres_data\ndocker compose up -d db
\n

Команда docker compose down -v удаляет named volumes, объявленные в Compose-файле, и подключённые anonymous volumes. Это не «обновление PHP» и не безопасная команда по умолчанию. В рабочем проекте остановитесь, если не можете доказать, что volume принадлежит именно этому локальному стенду и что данные восстановимы. Глобальный docker volume prune для такого расследования не подходит: он расширяет область удаления за пределы одной гипотезы.

\n

Контрольный маршрут после изменения

\n

После действия повторяем не только команду запуска, но и тот же признак, по которому заметили проблему. Так эксперимент остаётся воспроизводимым.

\n
  1. Запишите исходный симптом: например, проект падает на отсутствующем расширении или миграция видит старую таблицу.
  2. Сохраните итоговый config, список сервисов, короткие логи и идентификатор проверяемого образа.
  3. Назовите один слой, который должен измениться: image, container, bind mount или named volume.
  4. Выполните одно ограниченное действие и не удаляйте соседние слои «на всякий случай».
  5. Проверьте версию PHP, расширения, состояние базы и один заранее выбранный URL или CLI-маршрут.
  6. Сравните результат с исходным симптомом и запишите, какая гипотеза подтвердилась или исключилась.
\n

Если чистый запуск не помог

\n

Представим, что контейнер пересоздан, а приложение всё ещё получает ошибку подключения. Это исключает только временное состояние старого контейнера. Следом проверяем DATABASE_URL, имя сервиса db, готовность PostgreSQL и код миграции.

\n

В Compose-сети localhost внутри контейнера означает этот же контейнер. Для обращения к соседнему сервису используется его имя из Compose, в нашем примере — db. Если приложение запускается на хосте, а база в контейнере, адрес будет другим; нельзя переносить URL из одного режима запуска в другой без проверки.

\n

Если приложение видит старый PHP после пересборки, сравниваем image ID, время сборки и фактическую команду. Если оно видит старый файл, сравниваем mount и рабочий каталог. Если ошибка появилась после удаления volume, восстанавливаем дамп и возвращаемся к конфигурации миграций. Повторное расширение очистки без новой проверки не добавляет доказательств.

\n

Границы решения

\n

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

\n

Для старого проекта команда docker-compose с дефисом могла быть исходной CLI. В актуальной документации используется docker compose; перед копированием команд проверьте установленную версию и доступный синтаксис. Важно сохранить смысл операции: сначала увидеть итоговую конфигурацию, затем изменить выбранный слой и повторить контрольный маршрут.

\n

Локальная схема готова к командному использованию, если новый разработчик получает одинаковую версию PHP из Dockerfile, видит описанные в Compose mount и сервисы, может проверить конфигурацию одной последовательностью команд, а очистка данных требует явного подтверждения. Если команда запуска зависит от ручной настройки хоста или неизвестного volume, окружение ещё не воспроизводимо.

\n

Проверяемые источники

" }