diff --git a/editorial/agent-rewrites/286.json b/editorial/agent-rewrites/286.json index 9f7ae36..b8bc170 100644 --- a/editorial/agent-rewrites/286.json +++ b/editorial/agent-rewrites/286.json @@ -1,7 +1,7 @@ { "index": 286, "slug": "editorial-2020-01-field-docker-local", - "title": "Чистый запуск Docker: как найти старое состояние и не потерять данные", - "excerpt": "После изменения Dockerfile или миграции локальный стек продолжает показывать старый результат. Разбираем, где живёт состояние, как проверить каждый слой и когда чистый запуск действительно безопасен.", - "contentHtml": "
Симптом знакомый: вы меняете миграцию или Dockerfile, выполняете docker-compose up, а приложение продолжает видеть старую схему или старую версию файлов. На новом ноутбуке тот же репозиторий ведёт себя иначе. Цена ошибки — не только потерянный час. Команда может удалить нужные локальные данные, исправить не тот слой и закрепить в README опасную команду вроде глобального prune.
Тезис простой: чистый запуск Docker — это контрольный эксперимент, а не уборка всего daemon. Сначала нужно доказать, где осталось старое состояние. Потом удалить или пересоздать только этот слой. Контейнер, образ, bind mount и named volume живут по разным правилам. Повторный up не делает их одинаково свежими.
Контейнер хранит собственный записываемый слой и параметры запуска. Если контейнер пересоздали, этот слой исчезает. Образ хранит слои, созданные при сборке. Изменение Dockerfile или lock-файла не меняет уже собранный образ само по себе. Bind mount показывает контейнеру выбранный путь с хоста. Он обычно даёт текущие файлы рабочей копии, но только по правильному пути. Named volume хранит данные независимо от жизненного цикла контейнера. Для PostgreSQL там остаются таблицы, роли и история миграций.
\nЭти слои дают разные симптомы. Старый код после изменения Dockerfile указывает на старый образ или контейнер. Старые таблицы после пересоздания контейнера указывают на volume. Файл есть на хосте, но процесс читает старую копию, если mount направлен в другой каталог. Поэтому команда docker-compose down -v может устранить один симптом и одновременно уничтожить полезные данные, не доказав причину.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Старая схема базы после нового контейнера | Named volume пережил контейнер | Сверить volume в docker-compose config и mounts через docker inspect | Сохранить данные или удалить только volume локального проекта |
| Новый Dockerfile не меняет версию приложения | Используется старый образ или контейнер | Проверить время сборки, image ID и command в docker-compose ps и inspect | Пересобрать и пересоздать только нужный сервис |
| Контейнер не видит изменённый файл | Неверный путь bind mount или рабочий каталог | Сопоставить путь на хосте, destination mount, working_dir и команду | Исправить путь; volume базы здесь не поможет |
| После очистки ошибка осталась | Причина в переменной, адресе сервиса или коде | Повторить тот же запрос и сравнить безопасный лог конфигурации | Вернуть сохранённые данные и перейти к следующей гипотезе |
До разрушительной команды нужен снимок проекта. Назовите Compose-проект, сервис, который даёт симптом, и данные, которые нельзя потерять. Затем соберите итоговую конфигурацию. В ней видны подставленные переменные, сервисы, volumes, пути и команды. Это важнее исходного YAML: Compose мог получить значение из окружения или файла .env.
docker-compose config --services\ndocker-compose config\ndocker-compose ps\ndocker-compose logs --tail=50 web db\nСохраните вывод до изменения. Если проблема связана с базой, дополнительно проверьте имя volume и его привязку к контейнеру. Если проблема связана с кодом, проверьте image ID, дату сборки и mount. Не печатайте пароль базы в общий лог. Учебные значения в примере ниже нужны только для локального стенда и не задают правило хранения секретов.
\nВ этом учебном фрагменте исходники приходят через bind mount, а PostgreSQL пишет данные в named volume. Сервисы разделены: очистка базы не должна автоматически означать удаление исходников или образа.
\nversion: '3.7'\n\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 не копирует исходники в образ. Она связывает текущий каталог хоста с путём внутри контейнера. Если процесс работает из /app, а mount направлен в /srv/app, приложение может читать другой набор файлов. Строка postgres_data:/var/lib/postgresql/data делает состояние базы постоянным между пересозданиями контейнера. Поэтому обычный up возвращает ту же схему, пока volume существует.
Если изменился Dockerfile, lock-файл или команда запуска, начните с пересборки образа и пересоздания сервиса. Удалять volume базы для этого не нужно. Учебная проверка может выглядеть так:
\ndocker-compose build --no-cache web\ndocker-compose up -d --force-recreate web\ndocker-compose exec web php -v\ndocker-compose logs --tail=50 web\nЕсли изменились исходники под bind mount, сборка может вообще не быть причиной. Сверьте файл внутри контейнера с файлом на хосте. Если они различаются, проверьте путь, рабочий каталог и способ запуска. Не заменяйте bind mount удалением named volume: это два разных источника.
\nЕсли проверка доказывает, что старые таблицы живут в postgres_data, перед удалением сохраните нужные данные. Для тестовой базы допустим отдельный разрушительный сценарий с известным Compose-проектом. Команда с -v должна быть ограничена этим проектом, а не заменяться docker volume prune. В рабочем проекте остановитесь, если не можете назвать volume и подтвердить, что его содержимое восстановимо.
Представим, что volume удалён, база создана заново, а приложение всё ещё получает ошибку подключения. Из этого следует только одно: старый volume больше не объясняет симптом. Не нужно повторять очистку и расширять радиус удаления. Проверьте значение DATABASE_URL, имя сервиса db, готовность PostgreSQL и код миграции. Внутри контейнера localhost означает сам контейнер, а не соседний сервис. Для Compose-сети адресом базы служит имя сервиса.
Если после пересборки и пересоздания сервис всё ещё запускает старую версию, проверьте, не перекрывает ли образ bind mount, не используется ли другой Compose-файл и не запущен ли контейнер вне текущего проекта. Успешный статус Up не доказывает готовность приложения. Нужен ответ контрольного маршрута и лог, который подтверждает именно проверяемое условие.
Чистый локальный запуск не проверяет production-обновление базы, резервное копирование или совместимость версий Docker. Он не доказывает, что миграция безопасна для существующих данных. Синтаксис docker-compose сохранён в командах как исторический вариант для исходного контекста; в современных установках используется docker compose. Перед копированием примера проверьте версию CLI и формат проекта.
Сценарий готов к использованию, если выполнены четыре условия: команда называет удаляемый слой; вывод до операции сохранён; данные либо не нужны, либо восстановимы; контрольный маршрут даёт заранее определённый результат после запуска. Если хотя бы одно условие не выполнено, это не чистый эксперимент, а рискованное удаление состояния.
\nСимптом появляется ещё до первого изменения в коде: проект требует старую версию 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.