8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"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. Она показывает границы локального запуска. Она не описывает production: пароль приведён только для изолированного учебного примера, а версия образа служит фиксированной точкой эксперимента. В рабочем проекте значения берут из согласованного файла окружения и секретного хранилища.</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\n db:\n image: postgres:16-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 app_node_modules:\n postgres_data:</code></pre>\n<p>В этой схеме браузер открывает <code>http://localhost:3000</code>. Процесс <code>web</code> ищет PostgreSQL по имени <code>db</code>. База не публикует порт наружу, потому что для этого маршрута она нужна только приложению. Если администратору нужен доступ с хоста, публикацию добавляют отдельным решением и проверяют её риск. Наличие <code>depends_on</code> задаёт порядок запуска контейнеров, но не доказывает готовность базы принимать соединения.</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<ol><li>Зафиксировать версии Docker и Compose, имя проекта и критерий готовности. Для учебного примера критерий такой: web отвечает на один URL, а запрос из web к <code>db:5432</code> проходит после применения миграций.</li><li>Выполнить <code>docker compose config</code> и проверить итоговые переменные, build-контекст, рабочие каталоги, mounts, имена сервисов и опубликованные порты. Смотрим раскрытую конфигурацию, а не только исходный YAML.</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</code>. Если он не работает, исследовать публикацию порта и bind адрес web.</li><li>Проверить маршрут из контейнера: выполнить диагностическую команду через <code>docker compose exec web</code> и обратиться к <code>db</code>, а не к localhost. Так проверяется внутренняя сеть, а не браузерный путь.</li><li>При ошибке изменить один слой за раз. После каждого изменения повторить тот же запрос и сохранить наблюдение. Иначе нельзя связать исправление с причиной.</li><li>Отдельно проверить чистый запуск на учебных данных. Если нужен <code>docker compose down -v</code>, сначала вывести список проекта и убедиться, что volume не содержит нужных данных.</li></ol>\n<h2>Отрицательный путь и ограничения</h2>\n<p>Если база не готова к моменту запуска web, <code>depends_on</code> не обязан ждать успешного подключения. Нужна проверка готовности, повтор соединения или миграционная команда проекта. Нельзя объявлять сервис готовым только потому, что контейнер имеет статус <code>Up</code>.</p>\n<p>Bind mount зависит от файловой системы хоста и настроек Docker daemon. Скорость, права, уведомления об изменениях и поведение симлинков могут различаться на Linux, macOS и Windows. Named volume изолирует зависимости, но не устраняет различия архитектуры CPU и версий runtime. Образ, lock-файл и версия Compose должны оставаться частью проверяемого контракта.</p>\n<p>Удаление volume — разрушительное действие для локальных данных. Учебный PostgreSQL можно создать заново, рабочую базу нужно сначала экспортировать или сохранить другим согласованным способом. Не используйте очистку как универсальный рецепт. Она допустима только когда подтверждено, что старое состояние и есть проверяемая причина.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Сценарий готов, если на выбранной машине выполняются все условия: <code>docker compose config</code> показывает ожидаемые значения; <code>docker compose ps</code> показывает нужные сервисы; web отвечает по одному URL; из web разрешается имя <code>db</code> и проходит тестовое соединение; код, изменённый на хосте, виден процессу; состояние базы переживает обычный перезапуск; разрушительный clean-start не входит в обычный путь. Этот критерий не утверждает переносимость на любую систему и не заменяет production-проверки. Он доказывает только тот локальный контракт, который описан в статье.</p>\n<h2>Проверяемые источники</h2>\n<ul><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/engine/storage/volumes/\" target=\"_blank\" rel=\"noopener\">Docker Docs: Volumes</a> — назначение и жизненный цикл named volumes.</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> — порядок запуска и проверки готовности зависимостей.</li></ul>"
|
||
}
|