{ "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Для локального 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 |
Ниже учебный фрагмент для проекта, которому нужен PHP 7.4 и PostgreSQL. Версии, имена каталогов и команду запуска нужно заменить на значения конкретного репозитория. Пароль в примере демонстрационный: секреты не следует хранить в Compose-файле или отправлять в общий лог.
\nservices:\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, если его не удалили вручную.
Dockerfile должен описывать именно требования приложения, а не случайные пакеты с машины разработчика:
\nFROM 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Исходный YAML не всегда равен тому, что получает Compose. Значения могут прийти из shell, файла .env, другого Compose-файла или флага --env-file. Поэтому диагностику начинаем с итоговой модели, а не с догадок о том, какой файл был прочитан.
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 (подстановки переменных). Такой вывод может содержать секреты, поэтому его не копируют в тикет без редактирования.
После этого проверяем версию PHP внутри контейнера. Команда на хосте и команда в контейнере отвечают на разные вопросы:
\nphp -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| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| PHP внутри контейнера старше или новее ожидания | Используется старый image или изменился Dockerfile без rebuild | Сверить build context, image ID и php -v | docker compose build web, затем пересоздать только web |
| Изменение PHP-файла не видно | Неверный bind mount или рабочий каталог | Сравнить файл на хосте и путь внутри контейнера | Исправить mount или working_dir, volume базы не трогать |
| Миграция видит старые таблицы | Named volume пережил контейнер | Сверить volume и mount базы через config и inspect | Сделать дамп; удалить volume только известного тестового проекта |
| После пересборки нет соединения с БД | Неверны URL, имя сервиса или готовность PostgreSQL | Проверить итоговую переменную, логи и имя db | Исправить конфигурацию; не расширять очистку |
Изменение Dockerfile или composer.lock относится к образу. Начинаем с пересборки и пересоздания сервиса:
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, готовность базы или корректный ответ приложения.
Изменение исходника под bind mount обычно не требует пересборки: сначала проверяем, что контейнер видит тот же файл. Исключение — проект, который копирует код в образ и не подключает его через mount. Тогда проверяем Dockerfile и действительно выполняем rebuild. Не переносим привычку одного режима в другой.
\nУдаление данных — отдельный сценарий. Перед ним сохраняем дамп и записываем точное имя Compose-проекта и volume:
\ndocker 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Представим, что контейнер пересоздан, а приложение всё ещё получает ошибку подключения. Это исключает только временное состояние старого контейнера. Следом проверяем DATABASE_URL, имя сервиса db, готовность PostgreSQL и код миграции.
В Compose-сети localhost внутри контейнера означает этот же контейнер. Для обращения к соседнему сервису используется его имя из Compose, в нашем примере — db. Если приложение запускается на хосте, а база в контейнере, адрес будет другим; нельзя переносить URL из одного режима запуска в другой без проверки.
Если приложение видит старый PHP после пересборки, сравниваем image ID, время сборки и фактическую команду. Если оно видит старый файл, сравниваем mount и рабочий каталог. Если ошибка появилась после удаления volume, восстанавливаем дамп и возвращаемся к конфигурации миграций. Повторное расширение очистки без новой проверки не добавляет доказательств.
\nDocker решает расхождение локального окружения, но не делает миграцию безопасной и не заменяет резервное копирование. Пример не проверяет production-обновление базы, права доступа, сетевую задержку или совместимость приложения с новой версией Docker. Эти условия нужно проверять отдельно.
\nДля старого проекта команда docker-compose с дефисом могла быть исходной CLI. В актуальной документации используется docker compose; перед копированием команд проверьте установленную версию и доступный синтаксис. Важно сохранить смысл операции: сначала увидеть итоговую конфигурацию, затем изменить выбранный слой и повторить контрольный маршрут.
Локальная схема готова к командному использованию, если новый разработчик получает одинаковую версию PHP из Dockerfile, видит описанные в Compose mount и сервисы, может проверить конфигурацию одной последовательностью команд, а очистка данных требует явного подтверждения. Если команда запуска зависит от ручной настройки хоста или неизвестного volume, окружение ещё не воспроизводимо.
\ndown и флага --volumes.