{ "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

Механизм: что именно изолирует Docker

\n

Образ содержит файловую систему и установленные пакеты на момент сборки. Контейнер запускает процесс из образа, добавляя переменные, сеть и mounts. Bind mount связывает каталог хоста с путём внутри контейнера. Он удобен для редактирования кода, но перекрывает содержимое целевого пути. Поэтому зависимости, записанные в образе в /srv/app/node_modules, могут исчезнуть из видимости после mount .:/srv/app.

\n

Named volume принадлежит Docker и не зависит от каталога рабочей копии. Он подходит для данных PostgreSQL и для зависимостей, которые должны собираться в Linux-контейнере, а не в node_modules хоста. Volume не решает миграции и не обновляется от смены lock-файла сам по себе. После изменения зависимостей нужен явный шаг установки.

\n

Compose создаёт сеть проекта. Внутри неё сервис доступен по имени, например db. Адрес localhost внутри контейнера означает этот же контейнер, а не ноутбук и не соседний сервис. Запись ports: 3000:3000 связывает порт хоста с портом контейнера для входа с хоста. Она не превращает localhost в адрес базы для приложения.

\n
\"Схема
Один локальный контракт разделяет код, производные файлы, данные и сетевой путь.
\n

Конкретный пример Compose

\n

Ниже учебная конфигурация для приложения web и PostgreSQL. Она показывает границы локального запуска. Она не описывает production: пароль приведён только для изолированного учебного примера, а версия образа служит фиксированной точкой эксперимента. В рабочем проекте значения берут из согласованного файла окружения и секретного хранилища.

\n
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:
\n

В этой схеме браузер открывает http://localhost:3000. Процесс web ищет PostgreSQL по имени db. База не публикует порт наружу, потому что для этого маршрута она нужна только приложению. Если администратору нужен доступ с хоста, публикацию добавляют отдельным решением и проверяют её риск. Наличие depends_on задаёт порядок запуска контейнеров, но не доказывает готовность базы принимать соединения.

\n

Симптомы и точечная проверка

\n
СимптомПричинаПроверкаДействие
Код изменился на хосте, но 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 адрес процесса
\n

Таблица задаёт порядок расследования. Сначала подтверждаем слой, который способен объяснить симптом. Пересборка образа не исправляет неправильный сервисный адрес. Удаление volume не исправляет bind mount. Полная переустановка может скрыть причину и уничтожить полезное локальное состояние.

\n

Порядок действий

\n
  1. Зафиксировать версии Docker и Compose, имя проекта и критерий готовности. Для учебного примера критерий такой: web отвечает на один URL, а запрос из web к db:5432 проходит после применения миграций.
  2. Выполнить docker compose config и проверить итоговые переменные, build-контекст, рабочие каталоги, mounts, имена сервисов и опубликованные порты. Смотрим раскрытую конфигурацию, а не только исходный YAML.
  3. Запустить docker compose up -d. Затем выполнить docker compose ps и снять короткий фрагмент docker compose logs --tail=50 web db. Статус Up означает жизнь процесса, но не готовность приложения.
  4. Проверить маршрут с хоста: открыть http://localhost:3000 или выполнить curl. Если он не работает, исследовать публикацию порта и bind адрес web.
  5. Проверить маршрут из контейнера: выполнить диагностическую команду через docker compose exec web и обратиться к db, а не к localhost. Так проверяется внутренняя сеть, а не браузерный путь.
  6. При ошибке изменить один слой за раз. После каждого изменения повторить тот же запрос и сохранить наблюдение. Иначе нельзя связать исправление с причиной.
  7. Отдельно проверить чистый запуск на учебных данных. Если нужен docker compose down -v, сначала вывести список проекта и убедиться, что volume не содержит нужных данных.
\n

Отрицательный путь и ограничения

\n

Если база не готова к моменту запуска web, depends_on не обязан ждать успешного подключения. Нужна проверка готовности, повтор соединения или миграционная команда проекта. Нельзя объявлять сервис готовым только потому, что контейнер имеет статус Up.

\n

Bind mount зависит от файловой системы хоста и настроек Docker daemon. Скорость, права, уведомления об изменениях и поведение симлинков могут различаться на Linux, macOS и Windows. Named volume изолирует зависимости, но не устраняет различия архитектуры CPU и версий runtime. Образ, lock-файл и версия Compose должны оставаться частью проверяемого контракта.

\n

Удаление volume — разрушительное действие для локальных данных. Учебный PostgreSQL можно создать заново, рабочую базу нужно сначала экспортировать или сохранить другим согласованным способом. Не используйте очистку как универсальный рецепт. Она допустима только когда подтверждено, что старое состояние и есть проверяемая причина.

\n

Проверяемый критерий готовности

\n

Сценарий готов, если на выбранной машине выполняются все условия: docker compose config показывает ожидаемые значения; docker compose ps показывает нужные сервисы; web отвечает по одному URL; из web разрешается имя db и проходит тестовое соединение; код, изменённый на хосте, виден процессу; состояние базы переживает обычный перезапуск; разрушительный clean-start не входит в обычный путь. Этот критерий не утверждает переносимость на любую систему и не заменяет production-проверки. Он доказывает только тот локальный контракт, который описан в статье.

\n

Проверяемые источники

\n" }