From 31590e6526eb292cdc9a585965fb188b9337efd7 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 22:54:53 +0300 Subject: [PATCH] editorial: refine Docker local development article 288 --- editorial/agent-rewrites/288.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editorial/agent-rewrites/288.json b/editorial/agent-rewrites/288.json index 1c12b6b..deff5b0 100644 --- a/editorial/agent-rewrites/288.json +++ b/editorial/agent-rewrites/288.json @@ -3,5 +3,5 @@ "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" + "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. Предполагается, что Dockerfile собирает Node.js-приложение с рабочим каталогом /srv/app и скриптом npm run dev. Конфигурация показывает границы локального запуска, а не production: пароль приведён только для изолированного учебного примера. Тег образа выбран для читаемости; в CI, где нужна битовая воспроизводимость, его дополнительно фиксируют digest.

\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        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. База не публикует порт наружу, потому что для этого маршрута она нужна только приложению. Если администратору нужен доступ с хоста, публикацию добавляют отдельным решением и проверяют её риск.

\n

Здесь длинная форма depends_on не просто задаёт порядок создания: Compose ждёт успешного healthcheck для db. Это не отменяет проверку миграций и готовности самого приложения. Тег postgres:16.15-alpine всё ещё не является cryptographic pin; для строгого повторения сборки нужен digest, а его следует обновлять отдельной проверяемой процедурой.

\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

Проверяйте один и тот же маршрут после каждого изменения. Пример ниже отделяет разбор Compose, запуск, готовность базы, DNS и вход с хоста. Команда с node предполагает, что в образе web есть Node.js; если проект использует другой runtime, замените только диагностический вызов, сохранив проверяемый адрес db.

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

docker compose config показывает итоговую модель после подстановки переменных и слияния файлов. Его вывод может содержать пароли и другие секреты, поэтому его не отправляют в публичный лог. Критерий успеха здесь конкретный: команда возвращает код 0, pg_isready сообщает готовность, DNS возвращает адрес контейнера, а curl получает ожидаемый ответ приложения.

\n

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

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

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

\n

Если проект использует короткую форму depends_on: [db], Compose задаёт порядок запуска, но не обязан ждать, пока база начнёт принимать соединения. Нужны healthcheck с условием service_healthy, повтор соединения или миграционная команда проекта. Нельзя объявлять сервис готовым только потому, что контейнер имеет статус Up.

\n

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

\n

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

\n

Критерий готовности

\n

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

\n

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

\n" }