Files
progcode/editorial/agent-rewrites/286.json
T

8 lines
21 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 286,
"slug": "editorial-2020-01-field-docker-local",
"title": "Старый PHP в Docker: как воспроизводимо поднять локальный проект",
"excerpt": "Проект требует версию PHP, которой нет на компьютере разработчика. Разбираем, как описать окружение в Docker, проверить границы между образом, контейнером и данными и не превратить чистый запуск в потерю локальной базы.",
"contentHtml": "<p>Симптом появляется ещё до первого изменения в коде: проект требует старую версию PHP, а в системе разработчика установлена другая. На одном компьютере команда запускается, на другом падает при старте или видит другой набор расширений. Установка нужного PHP поверх рабочей среды кажется быстрым решением, но она меняет систему хоста и не фиксирует версии для следующего участника.</p>\n<p>В такой ситуации Docker полезен не как кнопка «запустить всё», а как описание границы проекта. Образ фиксирует базовое окружение и набор установленных пакетов, контейнер запускает процесс, bind mount (прямое подключение пути с хоста) отдаёт текущий код, а named volume хранит данные отдельно от контейнера. Сначала нужно разделить эти слои. Только после этого становится понятно, что пересобирать и что нельзя удалять.</p>\n<h2>Что именно должно быть одинаковым</h2>\n<p>Для локального PHP-проекта важна не только строка с версией интерпретатора. На результат влияют расширения PHP, зависимости Composer, веб-сервер, переменные окружения и состояние базы. Если один из этих элементов берётся с хоста, разработчик получает не тот проект, который описан в репозитории.</p>\n<p>Контейнер не делает файлы проекта частью образа автоматически. В типичной схеме исходники подключаются через bind mount, а база пишет в named volume. Поэтому пересоздание контейнера может убрать его записываемый слой, но не очистить базу. И наоборот: удаление volume не исправит старый код, который процесс получает через неверный mount.</p>\n<div class=\"table-scroll\"><table><caption>Граница локального окружения и способ её проверить</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Что в нём живёт</th><th scope=\"col\">Наблюдаемый симптом</th><th scope=\"col\">Проверка</th></tr></thead><tbody><tr><td>Образ</td><td>PHP, расширения, системные пакеты и команда сборки</td><td>В контейнере не та версия PHP или нет расширения</td><td><code>docker compose images</code> и <code>docker compose exec web php -v</code></td></tr><tr><td>Контейнер</td><td>Запущенный процесс, его параметры и временные изменения</td><td>Перезапуск возвращает прежнее состояние процесса</td><td><code>docker compose ps</code>, <code>docker inspect</code> и логи</td></tr><tr><td>Bind mount</td><td>Файлы выбранного каталога на хосте</td><td>Процесс видит не тот файл, который открыт в редакторе</td><td>Сопоставить host path, destination и <code>working_dir</code></td></tr><tr><td>Named volume</td><td>Данные базы или другой persistent storage</td><td>Новая миграция не применяется к «новой» базе</td><td>Проверить volume в итоговом Compose config и mounts</td></tr></tbody></table></div>\n<h2>Минимальная воспроизводимая схема</h2>\n<p>Ниже учебный фрагмент для проекта, которому нужен PHP 7.4 и PostgreSQL. Версии, имена каталогов и команду запуска нужно заменить на значения конкретного репозитория. Пароль в примере демонстрационный: секреты не следует хранить в Compose-файле или отправлять в общий лог.</p>\n<pre><code>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:</code></pre>\n<p>Строка <code>.:/srv/app</code> подключает текущий каталог к контейнеру. Если команда запускает PHP из <code>/app</code>, а mount установлен в <code>/srv/app</code>, процесс может работать не с теми файлами. Строка <code>postgres_data:/var/lib/postgresql/data</code> отделяет данные PostgreSQL от жизненного цикла контейнера. При следующем <code>up</code> Compose использует существующий volume, если его не удалили вручную.</p>\n<p>Dockerfile должен описывать именно требования приложения, а не случайные пакеты с машины разработчика:</p>\n<pre><code>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\"]</code></pre>\n<p>Это не универсальный production-образ. В реальном проекте понадобятся его расширения PHP, системные библиотеки и способ запуска, зафиксированные в исходном Dockerfile. Смысл примера в другом: версия интерпретатора и порядок установки зависимостей находятся в описании окружения, а не в личной настройке ноутбука.</p>\n<figure><img src=\"/assets/editorial/2020/docker-local-clean-start-2020.svg\" alt=\"Схема воспроизводимого локального запуска Docker: зафиксировать Compose config и логи, назвать слой старого состояния, выполнить ограниченное действие и повторить контрольный маршрут\" loading=\"lazy\" /><figcaption>Перед чистым запуском сначала фиксируем конфигурацию, затем меняем только подтверждённый слой и повторяем тот же контрольный маршрут.</figcaption></figure>\n<h2>Сначала получить итоговую конфигурацию</h2>\n<p>Исходный YAML не всегда равен тому, что получает Compose. Значения могут прийти из shell, файла <code>.env</code>, другого Compose-файла или флага <code>--env-file</code>. Поэтому диагностику начинаем с итоговой модели, а не с догадок о том, какой файл был прочитан.</p>\n<pre><code>docker compose config --services\ndocker compose config\ndocker compose ps\ndocker compose logs --tail=50 web db</code></pre>\n<p>В выводе проверяем имя проекта, образ или build context, список mount, рабочий каталог, команду и адрес базы. Если нужно выяснить источник подстановок, команда <code>docker compose config --environment</code> показывает окружение, которое Compose использует для interpolation (подстановки переменных). Такой вывод может содержать секреты, поэтому его не копируют в тикет без редактирования.</p>\n<p>После этого проверяем версию PHP внутри контейнера. Команда на хосте и команда в контейнере отвечают на разные вопросы:</p>\n<pre><code>php -v\ndocker compose exec web php -v\ndocker compose exec web php -m | sort\ndocker compose exec web composer check-platform-reqs</code></pre>\n<p>Первая строка показывает локальную систему и может быть полезна для сравнения. Вторая и третья показывают фактический интерпретатор и расширения процесса приложения. Если <code>composer check-platform-reqs</code> не проходит, сначала сверяем Dockerfile и lock-файл. Обновлять зависимости «до последних» для исправления расхождения не нужно: это меняет задачу и может скрыть исходную причину.</p>\n<h2>Симптом → гипотеза → ограниченное действие</h2>\n<p>Один и тот же внешний симптом не доказывает один и тот же слой. Таблица помогает выбрать действие с минимальным радиусом разрушения.</p>\n<div class=\"table-scroll\"><table><caption>Диагностическая матрица для локального проекта</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>PHP внутри контейнера старше или новее ожидания</td><td>Используется старый image или изменился Dockerfile без rebuild</td><td>Сверить build context, image ID и <code>php -v</code></td><td><code>docker compose build web</code>, затем пересоздать только <code>web</code></td></tr><tr><td>Изменение PHP-файла не видно</td><td>Неверный bind mount или рабочий каталог</td><td>Сравнить файл на хосте и путь внутри контейнера</td><td>Исправить mount или <code>working_dir</code>, volume базы не трогать</td></tr><tr><td>Миграция видит старые таблицы</td><td>Named volume пережил контейнер</td><td>Сверить volume и mount базы через config и inspect</td><td>Сделать дамп; удалить volume только известного тестового проекта</td></tr><tr><td>После пересборки нет соединения с БД</td><td>Неверны URL, имя сервиса или готовность PostgreSQL</td><td>Проверить итоговую переменную, логи и имя <code>db</code></td><td>Исправить конфигурацию; не расширять очистку</td></tr></tbody></table></div>\n<h2>Что пересобирать, а что сохранять</h2>\n<p>Изменение Dockerfile или <code>composer.lock</code> относится к образу. Начинаем с пересборки и пересоздания сервиса:</p>\n<pre><code>docker compose build web\ndocker compose up -d --force-recreate web\ndocker compose exec web php -v\ndocker compose logs --tail=50 web</code></pre>\n<p>Если команда после rebuild всё ещё показывает старый PHP, проверяем, какой Compose-файл и проект использованы, не перекрыт ли путь bind mount и не запущен ли контейнер из другого каталога. Статус <code>Up</code> доказывает только, что процесс не завершился сразу. Он не доказывает нужную версию PHP, готовность базы или корректный ответ приложения.</p>\n<p>Изменение исходника под bind mount обычно не требует пересборки: сначала проверяем, что контейнер видит тот же файл. Исключение — проект, который копирует код в образ и не подключает его через mount. Тогда проверяем Dockerfile и действительно выполняем rebuild. Не переносим привычку одного режима в другой.</p>\n<p>Удаление данных — отдельный сценарий. Перед ним сохраняем дамп и записываем точное имя Compose-проекта и volume:</p>\n<pre><code>docker compose exec -T db pg_dump -U app app &gt; backup.sql\ndocker compose down\ndocker volume ls\n# После проверки имени тестового volume:\ndocker volume rm project_postgres_data\ndocker compose up -d db</code></pre>\n<p>Команда <code>docker compose down -v</code> удаляет named volumes, объявленные в Compose-файле, и подключённые anonymous volumes. Это не «обновление PHP» и не безопасная команда по умолчанию. В рабочем проекте остановитесь, если не можете доказать, что volume принадлежит именно этому локальному стенду и что данные восстановимы. Глобальный <code>docker volume prune</code> для такого расследования не подходит: он расширяет область удаления за пределы одной гипотезы.</p>\n<h2>Контрольный маршрут после изменения</h2>\n<p>После действия повторяем не только команду запуска, но и тот же признак, по которому заметили проблему. Так эксперимент остаётся воспроизводимым.</p>\n<ol><li>Запишите исходный симптом: например, проект падает на отсутствующем расширении или миграция видит старую таблицу.</li><li>Сохраните итоговый config, список сервисов, короткие логи и идентификатор проверяемого образа.</li><li>Назовите один слой, который должен измениться: image, container, bind mount или named volume.</li><li>Выполните одно ограниченное действие и не удаляйте соседние слои «на всякий случай».</li><li>Проверьте версию PHP, расширения, состояние базы и один заранее выбранный URL или CLI-маршрут.</li><li>Сравните результат с исходным симптомом и запишите, какая гипотеза подтвердилась или исключилась.</li></ol>\n<h2>Если чистый запуск не помог</h2>\n<p>Представим, что контейнер пересоздан, а приложение всё ещё получает ошибку подключения. Это исключает только временное состояние старого контейнера. Следом проверяем <code>DATABASE_URL</code>, имя сервиса <code>db</code>, готовность PostgreSQL и код миграции.</p>\n<p>В Compose-сети <code>localhost</code> внутри контейнера означает этот же контейнер. Для обращения к соседнему сервису используется его имя из Compose, в нашем примере — <code>db</code>. Если приложение запускается на хосте, а база в контейнере, адрес будет другим; нельзя переносить URL из одного режима запуска в другой без проверки.</p>\n<p>Если приложение видит старый PHP после пересборки, сравниваем image ID, время сборки и фактическую команду. Если оно видит старый файл, сравниваем mount и рабочий каталог. Если ошибка появилась после удаления volume, восстанавливаем дамп и возвращаемся к конфигурации миграций. Повторное расширение очистки без новой проверки не добавляет доказательств.</p>\n<h2>Границы решения</h2>\n<p>Docker решает расхождение локального окружения, но не делает миграцию безопасной и не заменяет резервное копирование. Пример не проверяет production-обновление базы, права доступа, сетевую задержку или совместимость приложения с новой версией Docker. Эти условия нужно проверять отдельно.</p>\n<p>Для старого проекта команда <code>docker-compose</code> с дефисом могла быть исходной CLI. В актуальной документации используется <code>docker compose</code>; перед копированием команд проверьте установленную версию и доступный синтаксис. Важно сохранить смысл операции: сначала увидеть итоговую конфигурацию, затем изменить выбранный слой и повторить контрольный маршрут.</p>\n<p>Локальная схема готова к командному использованию, если новый разработчик получает одинаковую версию PHP из Dockerfile, видит описанные в Compose mount и сервисы, может проверить конфигурацию одной последовательностью команд, а очистка данных требует явного подтверждения. Если команда запуска зависит от ручной настройки хоста или неизвестного volume, окружение ещё не воспроизводимо.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.docker.com/engine/storage/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Engine: Storage</a> — writable layer контейнера, volumes и bind mounts.</li><li><a href=\"https://docs.docker.com/reference/compose-file/volumes/\" target=\"_blank\" rel=\"noopener noreferrer\">Compose file reference: volumes</a> — жизненный цикл named volumes и их подключение к сервисам.</li><li><a href=\"https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/\" target=\"_blank\" rel=\"noopener noreferrer\">Compose: variable interpolation</a> — источники переменных и проверка итоговой конфигурации.</li><li><a href=\"https://docs.docker.com/reference/cli/docker/compose/down/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker CLI: docker compose down</a> — область действия <code>down</code> и флага <code>--volumes</code>.</li></ul>"
}