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

8 lines
19 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": 288,
"slug": "editorial-2020-01-practice-docker-local",
"title": "Локальная разработка в Docker: как разделить код, данные и сеть",
"excerpt": "Контейнеры не делают окружение одинаковым сами по себе. Разбираем bind mount, named volume, сервисные имена и порядок проверки локального Compose-сценария.",
"contentHtml": "<p>Проблема проявляется так: приложение запускается у одного разработчика, но на другом не видит базу или читает старые файлы. Такая ошибка съедает часы диагностики и маскирует реальные изменения в коде. Docker помогает только тогда, когда команда фиксирует границы среды: образ, контейнер, mount, volume, сеть, порт и переменные. Если смешать эти слои, контейнер перенесёт прежнюю неопределённость в новый YAML.</p>\n<p>Локальная среда должна описывать не только команду запуска, но и владельца каждого состояния. Исходники обычно остаются на хосте и приходят в контейнер через bind mount. Зависимости и данные базы живут в named volume. Сервисы обращаются друг к другу по именам Compose, а браузер — к опубликованному порту хоста. Такой контракт можно проверить короткими командами и одним заранее выбранным запросом.</p>\n<h2>Механизм: что именно изолирует Docker</h2>\n<p>Образ содержит файловую систему и установленные пакеты на момент сборки. Контейнер запускает процесс из образа, добавляя переменные, сеть и mounts. Bind mount связывает каталог хоста с путём внутри контейнера. Он удобен для редактирования кода, но перекрывает содержимое целевого пути. Поэтому зависимости, записанные в образе в <code>/srv/app/node_modules</code>, могут исчезнуть из видимости после mount <code>.:/srv/app</code>.</p>\n<p>Named volume принадлежит Docker и не зависит от каталога рабочей копии. Он подходит для данных PostgreSQL и для зависимостей, которые должны собираться в Linux-контейнере, а не в <code>node_modules</code> хоста. Volume не решает миграции и не обновляется от смены lock-файла сам по себе. После изменения зависимостей нужен явный шаг установки.</p>\n<p>Compose создаёт сеть проекта. Внутри неё сервис доступен по имени, например <code>db</code>. Адрес <code>localhost</code> внутри контейнера означает этот же контейнер, а не ноутбук и не соседний сервис. Запись <code>ports: 3000:3000</code> связывает порт хоста с портом контейнера для входа с хоста. Она не превращает <code>localhost</code> в адрес базы для приложения.</p>\n<figure><img src=\"/assets/editorial/2020/docker-local-environment-2020.svg\" alt=\"Схема локального Docker-окружения: исходники монтируются в web, volumes хранят зависимости и данные базы, web обращается к db по имени сервиса\" /><figcaption>Один локальный контракт разделяет код, производные файлы, данные и сетевой путь.</figcaption></figure>\n<h2>Конкретный пример Compose</h2>\n<p>Ниже учебная конфигурация для приложения <code>web</code> и PostgreSQL. Предполагается, что Dockerfile собирает Node.js-приложение с рабочим каталогом <code>/srv/app</code> и скриптом <code>npm run dev</code>. Конфигурация показывает границы локального запуска, а не production: пароль приведён только для изолированного учебного примера. Тег образа выбран для читаемости; в CI, где нужна битовая воспроизводимость, его дополнительно фиксируют digest.</p>\n<pre><code>services:\n web:\n build: .\n working_dir: /srv/app\n command: npm run dev -- --host 0.0.0.0\n environment:\n DATABASE_URL: postgres://app:app@db:5432/app\n ports:\n - \"3000:3000\"\n volumes:\n - .:/srv/app\n - app_node_modules:/srv/app/node_modules\n depends_on:\n db:\n condition: service_healthy\n\n db:\n image: postgres:16.15-alpine\n environment:\n POSTGRES_DB: app\n POSTGRES_USER: app\n POSTGRES_PASSWORD: app\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}\"]\n interval: 5s\n timeout: 5s\n retries: 10\n volumes:\n - postgres_data:/var/lib/postgresql/data\n\nvolumes:\n app_node_modules:\n postgres_data:</code></pre>\n<p>В этой схеме браузер открывает <code>http://localhost:3000</code>. Процесс <code>web</code> ищет PostgreSQL по имени <code>db</code> и внутреннему порту 5432. База не публикует порт наружу, потому что для этого маршрута она нужна только приложению. Если администратору нужен доступ с хоста, публикацию добавляют отдельным решением и проверяют её риск.</p>\n<p>Здесь длинная форма <code>depends_on</code> не просто задаёт порядок создания: Compose ждёт успешного <code>healthcheck</code> для <code>db</code>. Это не отменяет проверку миграций и готовности самого приложения. Тег <code>postgres:16.15-alpine</code> всё ещё не является cryptographic pin; для строгого повторения сборки нужен digest, а его следует обновлять отдельной проверяемой процедурой.</p>\n<h2>Симптомы и точечная проверка</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Код изменился на хосте, но web отдаёт старый ответ</td><td>Рабочая копия не смонтирована в фактический каталог процесса или mount перекрыт</td><td>Сверить <code>working_dir</code>, путь запуска и <code>volumes</code> в выводе <code>docker compose config</code></td><td>Смонтировать код в каталог, из которого читает процесс; перезапустить только web</td></tr><tr><td>web не подключается к базе по localhost</td><td>Внутри web localhost указывает на web</td><td>Проверить строку подключения и выполнить DNS-проверку имени <code>db</code> из контейнера</td><td>Использовать сервисное имя <code>db</code> и внутренний порт 5432</td></tr><tr><td>После смены ветки модули падают с ошибкой платформы</td><td>В контейнер подставлен host <code>node_modules</code></td><td>Посмотреть mount для <code>/srv/app/node_modules</code> и платформу бинарного модуля</td><td>Хранить зависимости в named volume и переустановить их внутри контейнера</td></tr><tr><td>База показывает старую схему</td><td>Данные остались в named volume</td><td>Сопоставить имя volume, путь PostgreSQL и применённые миграции</td><td>Применить миграцию; очищать volume только после проверки данных и цели</td></tr><tr><td>Порт занят или браузер не получает ответ</td><td>Порт хоста занят либо процесс слушает только loopback внутри контейнера</td><td>Проверить <code>docker compose ps</code>, логи web и привязку dev-сервера к <code>0.0.0.0</code></td><td>Освободить или изменить порт хоста; исправить bind-адрес процесса</td></tr></tbody></table></div>\n<p>Таблица задаёт порядок расследования. Сначала подтверждаем слой, который способен объяснить симптом. Пересборка образа не исправляет неправильный сервисный адрес. Удаление volume не исправляет bind mount. Полная переустановка может скрыть причину и уничтожить полезное локальное состояние.</p>\n<h2>Воспроизводимая последовательность</h2>\n<p>Проверяйте один и тот же маршрут после каждого изменения. Пример ниже отделяет разбор Compose, запуск, готовность базы, DNS и вход с хоста. Команда с <code>node</code> предполагает, что в образе <code>web</code> есть Node.js; если проект использует другой runtime, замените только диагностический вызов, сохранив проверяемый адрес <code>db</code>.</p>\n<pre><code>docker compose config --quiet\ndocker compose up -d\ndocker compose ps\ndocker compose exec db pg_isready -U app -d app\ndocker compose exec web node -e 'require(\"node:dns\").promises.lookup(\"db\").then(({address}) =&gt; console.log(address)).catch((error) =&gt; { console.error(error); process.exit(1); })'\ncurl --fail http://localhost:3000/</code></pre>\n<p><code>docker compose config</code> показывает итоговую модель после подстановки переменных и слияния файлов. Его вывод может содержать пароли и другие секреты, поэтому его не отправляют в публичный лог. Критерий успеха здесь конкретный: команда возвращает код 0, <code>pg_isready</code> сообщает готовность, DNS возвращает адрес контейнера, а <code>curl</code> получает ожидаемый ответ приложения.</p>\n<h2>Порядок действий при сбое</h2>\n<ol><li>Зафиксировать версии Docker и Compose, имя проекта и критерий готовности. Для примера критерий такой: web отвечает на один URL, запрос из web к <code>db:5432</code> проходит после применения миграций.</li><li>Выполнить <code>docker compose config --quiet</code>, а затем просмотреть итоговую конфигурацию в защищённом локальном терминале. Проверить build-контекст, рабочие каталоги, mounts, имена сервисов и опубликованные порты.</li><li>Запустить <code>docker compose up -d</code>. Затем выполнить <code>docker compose ps</code> и снять короткий фрагмент <code>docker compose logs --tail=50 web db</code>. Статус <code>Up</code> означает жизнь процесса, но не готовность приложения.</li><li>Проверить маршрут с хоста: открыть <code>http://localhost:3000</code> или выполнить <code>curl --fail http://localhost:3000/</code>. Если он не работает, исследовать публикацию порта и bind-адрес web.</li><li>Проверить маршрут из контейнера: выполнить DNS-команду из примера и <code>docker compose exec db pg_isready -U app -d app</code>. Так проверяется внутренняя сеть и готовность базы, а не только браузерный путь.</li><li>Проверить миграции отдельной командой проекта. Успешный healthcheck подтверждает доступность PostgreSQL, но не то, что схема базы соответствует коду.</li><li>При ошибке изменить один слой за раз. После каждого изменения повторить тот же запрос и сохранить наблюдение. Иначе нельзя связать исправление с причиной.</li><li>Отдельно проверить чистый запуск на учебных данных. Если нужен <code>docker compose down -v</code>, сначала вывести список проекта и убедиться, что volume не содержит нужных данных.</li></ol>\n<h2>Отрицательный путь и ограничения</h2>\n<p>Если проект использует короткую форму <code>depends_on: [db]</code>, Compose задаёт порядок запуска, но не обязан ждать, пока база начнёт принимать соединения. Нужны <code>healthcheck</code> с условием <code>service_healthy</code>, повтор соединения или миграционная команда проекта. Нельзя объявлять сервис готовым только потому, что контейнер имеет статус <code>Up</code>.</p>\n<p>Bind mount зависит от файловой системы хоста и настроек Docker daemon. Скорость, права, уведомления об изменениях и поведение симлинков могут различаться на Linux, macOS и Windows. Named volume изолирует зависимости, но не устраняет различия архитектуры CPU и версий runtime. Образ, lock-файл, digest при необходимости и версия Compose должны оставаться частью проверяемого контракта.</p>\n<p>Удаление volume — разрушительное действие для локальных данных. Учебный PostgreSQL можно создать заново, рабочую базу нужно сначала экспортировать или сохранить другим согласованным способом. Не используйте очистку как универсальный рецепт. Она допустима только когда подтверждено, что старое состояние и есть проверяемая причина.</p>\n<h2>Критерий готовности</h2>\n<p>Сценарий готов, если на выбранной машине выполняются все условия: <code>docker compose config --quiet</code> завершается успешно; <code>docker compose ps</code> показывает нужные сервисы; healthcheck подтверждает PostgreSQL; DNS из web разрешает имя <code>db</code>; web отвечает по одному URL; миграции применены; код, изменённый на хосте, виден процессу; состояние базы переживает обычный перезапуск; разрушительный clean-start не входит в обычный путь. Этот критерий не утверждает переносимость на любую систему и не заменяет production-проверки. Он доказывает только тот локальный контракт, который описан в статье.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.docker.com/engine/storage/bind-mounts/\" target=\"_blank\" rel=\"noopener\">Docker Docs: Bind mounts</a> — назначение bind mount для исходников, перекрытие содержимого целевого пути и зависимость от хоста.</li><li><a href=\"https://docs.docker.com/engine/storage/volumes/\" target=\"_blank\" rel=\"noopener\">Docker Docs: Volumes</a> — назначение, жизненный цикл и поведение named volumes.</li><li><a href=\"https://docs.docker.com/compose/how-tos/networking/\" target=\"_blank\" rel=\"noopener\">Docker Docs: Networking in Compose</a> — имена сервисов, сеть проекта и различие host port и container port.</li><li><a href=\"https://docs.docker.com/compose/how-tos/startup-order/\" target=\"_blank\" rel=\"noopener\">Docker Docs: Control startup and shutdown order in Compose</a> — порядок запуска, <code>healthcheck</code> и условие <code>service_healthy</code>.</li><li><a href=\"https://docs.docker.com/reference/cli/docker/compose/config/\" target=\"_blank\" rel=\"noopener\">Docker Docs: docker compose config</a> — разбор, разрешение переменных и вывод канонической модели Compose.</li></ul>"
}