{ "index": 288, "slug": "editorial-2020-01-practice-docker-local", "title": "Локальная разработка в Docker: как разделить код, данные и сеть", "excerpt": "Контейнеры не делают окружение одинаковым сами по себе. Разбираем bind mount, named volume, сервисные имена и порядок проверки локального Compose-сценария.", "contentHtml": "
Проблема проявляется так: приложение запускается у одного разработчика, но на другом не видит базу или читает старые файлы. Такая ошибка съедает часы диагностики и маскирует реальные изменения в коде. Docker помогает только тогда, когда команда фиксирует границы среды: образ, контейнер, mount, volume, сеть, порт и переменные. Если смешать эти слои, контейнер перенесёт прежнюю неопределённость в новый YAML.
\nЛокальная среда должна описывать не только команду запуска, но и владельца каждого состояния. Исходники обычно остаются на хосте и приходят в контейнер через bind mount. Зависимости и данные базы живут в named volume. Сервисы обращаются друг к другу по именам Compose, а браузер — к опубликованному порту хоста. Такой контракт можно проверить короткими командами и одним заранее выбранным запросом.
\nОбраз содержит файловую систему и установленные пакеты на момент сборки. Контейнер запускает процесс из образа, добавляя переменные, сеть и mounts. Bind mount связывает каталог хоста с путём внутри контейнера. Он удобен для редактирования кода, но перекрывает содержимое целевого пути. Поэтому зависимости, записанные в образе в /srv/app/node_modules, могут исчезнуть из видимости после mount .:/srv/app.
Named volume принадлежит Docker и не зависит от каталога рабочей копии. Он подходит для данных PostgreSQL и для зависимостей, которые должны собираться в Linux-контейнере, а не в node_modules хоста. Volume не решает миграции и не обновляется от смены lock-файла сам по себе. После изменения зависимостей нужен явный шаг установки.
Compose создаёт сеть проекта. Внутри неё сервис доступен по имени, например db. Адрес localhost внутри контейнера означает этот же контейнер, а не ноутбук и не соседний сервис. Запись ports: 3000:3000 связывает порт хоста с портом контейнера для входа с хоста. Она не превращает localhost в адрес базы для приложения.
Ниже учебная конфигурация для приложения web и PostgreSQL. Предполагается, что Dockerfile собирает Node.js-приложение с рабочим каталогом /srv/app и скриптом npm run dev. Конфигурация показывает границы локального запуска, а не production: пароль приведён только для изолированного учебного примера. Тег образа выбран для читаемости; в CI, где нужна битовая воспроизводимость, его дополнительно фиксируют digest.
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:\nВ этой схеме браузер открывает http://localhost:3000. Процесс web ищет PostgreSQL по имени db и внутреннему порту 5432. База не публикует порт наружу, потому что для этого маршрута она нужна только приложению. Если администратору нужен доступ с хоста, публикацию добавляют отдельным решением и проверяют её риск.
Здесь длинная форма depends_on не просто задаёт порядок создания: Compose ждёт успешного healthcheck для db. Это не отменяет проверку миграций и готовности самого приложения. Тег postgres:16.15-alpine всё ещё не является cryptographic pin; для строгого повторения сборки нужен digest, а его следует обновлять отдельной проверяемой процедурой.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Код изменился на хосте, но web отдаёт старый ответ | Рабочая копия не смонтирована в фактический каталог процесса или mount перекрыт | Сверить working_dir, путь запуска и volumes в выводе docker compose config | Смонтировать код в каталог, из которого читает процесс; перезапустить только web |
| web не подключается к базе по localhost | Внутри web localhost указывает на web | Проверить строку подключения и выполнить DNS-проверку имени db из контейнера | Использовать сервисное имя db и внутренний порт 5432 |
| После смены ветки модули падают с ошибкой платформы | В контейнер подставлен host node_modules | Посмотреть mount для /srv/app/node_modules и платформу бинарного модуля | Хранить зависимости в named volume и переустановить их внутри контейнера |
| База показывает старую схему | Данные остались в named volume | Сопоставить имя volume, путь PostgreSQL и применённые миграции | Применить миграцию; очищать volume только после проверки данных и цели |
| Порт занят или браузер не получает ответ | Порт хоста занят либо процесс слушает только loopback внутри контейнера | Проверить docker compose ps, логи web и привязку dev-сервера к 0.0.0.0 | Освободить или изменить порт хоста; исправить bind-адрес процесса |
Таблица задаёт порядок расследования. Сначала подтверждаем слой, который способен объяснить симптом. Пересборка образа не исправляет неправильный сервисный адрес. Удаление volume не исправляет bind mount. Полная переустановка может скрыть причину и уничтожить полезное локальное состояние.
\nПроверяйте один и тот же маршрут после каждого изменения. Пример ниже отделяет разбор Compose, запуск, готовность базы, DNS и вход с хоста. Команда с node предполагает, что в образе web есть Node.js; если проект использует другой runtime, замените только диагностический вызов, сохранив проверяемый адрес db.
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}) => console.log(address)).catch((error) => { console.error(error); process.exit(1); })'\ncurl --fail http://localhost:3000/\ndocker compose config показывает итоговую модель после подстановки переменных и слияния файлов. Его вывод может содержать пароли и другие секреты, поэтому его не отправляют в публичный лог. Критерий успеха здесь конкретный: команда возвращает код 0, pg_isready сообщает готовность, DNS возвращает адрес контейнера, а curl получает ожидаемый ответ приложения.
db:5432 проходит после применения миграций.docker compose config --quiet, а затем просмотреть итоговую конфигурацию в защищённом локальном терминале. Проверить build-контекст, рабочие каталоги, mounts, имена сервисов и опубликованные порты.docker compose up -d. Затем выполнить docker compose ps и снять короткий фрагмент docker compose logs --tail=50 web db. Статус Up означает жизнь процесса, но не готовность приложения.http://localhost:3000 или выполнить curl --fail http://localhost:3000/. Если он не работает, исследовать публикацию порта и bind-адрес web.docker compose exec db pg_isready -U app -d app. Так проверяется внутренняя сеть и готовность базы, а не только браузерный путь.docker compose down -v, сначала вывести список проекта и убедиться, что volume не содержит нужных данных.Если проект использует короткую форму depends_on: [db], Compose задаёт порядок запуска, но не обязан ждать, пока база начнёт принимать соединения. Нужны healthcheck с условием service_healthy, повтор соединения или миграционная команда проекта. Нельзя объявлять сервис готовым только потому, что контейнер имеет статус Up.
Bind mount зависит от файловой системы хоста и настроек Docker daemon. Скорость, права, уведомления об изменениях и поведение симлинков могут различаться на Linux, macOS и Windows. Named volume изолирует зависимости, но не устраняет различия архитектуры CPU и версий runtime. Образ, lock-файл, digest при необходимости и версия Compose должны оставаться частью проверяемого контракта.
\nУдаление volume — разрушительное действие для локальных данных. Учебный PostgreSQL можно создать заново, рабочую базу нужно сначала экспортировать или сохранить другим согласованным способом. Не используйте очистку как универсальный рецепт. Она допустима только когда подтверждено, что старое состояние и есть проверяемая причина.
\nСценарий готов, если на выбранной машине выполняются все условия: docker compose config --quiet завершается успешно; docker compose ps показывает нужные сервисы; healthcheck подтверждает PostgreSQL; DNS из web разрешает имя db; web отвечает по одному URL; миграции применены; код, изменённый на хосте, виден процессу; состояние базы переживает обычный перезапуск; разрушительный clean-start не входит в обычный путь. Этот критерий не утверждает переносимость на любую систему и не заменяет production-проверки. Он доказывает только тот локальный контракт, который описан в статье.
healthcheck и условие service_healthy.