8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"index": 287,
|
||
"slug": "editorial-2020-01-mechanism-docker-local",
|
||
"title": "Почему локальный Docker ломается: адреса, файлы и состояние",
|
||
"excerpt": "Контейнер может быть запущен и всё равно не видеть базу, файл или переменную. Разбираем границы Compose и порядок проверки, который отделяет проблему хоста от проблемы контейнера.",
|
||
"contentHtml": "<p>Проблема обычно выглядит так: контейнер <code>api</code> имеет статус <code>Up</code>, но запрос к базе завершается ошибкой; приложение ищет файл и получает <code>ENOENT</code>; переменная есть в терминале, но внутри процесса её нет. Цена ошибки — потерянное время и неверное исправление. Команда меняет Dockerfile, удаляет том или добавляет повторные попытки, хотя причина лежит в адресе, пути монтирования или способе передачи конфигурации.</p>\n<p>Главный тезис прост: Docker изолирует процесс и описывает его окружение, но не делает все пространства одинаковыми. Хост, Docker daemon, файловая система контейнера, сеть Compose и окружение процесса имеют разные правила. Нужно назвать наблюдателя для каждого факта. Тогда <code>localhost</code>, путь файла и значение переменной перестают быть двусмысленными.</p>\n<h2>Сначала восстановите маршрут запроса</h2>\n<p>Возьмём учебный стек из двух сервисов. Браузер на хосте обращается к опубликованному порту <code>api</code>. Процесс <code>api</code> ищет базу по имени <code>db</code> во внутренней сети Compose. PostgreSQL хранит данные в named volume. Запрос проходит через три адреса и два типа хранилища. Статус контейнера сообщает только о запуске процесса. Он не доказывает готовность базы, правильность переменной или наличие файла по нужному пути.</p>\n<p>У слова <code>localhost</code> нет одного смысла. На хосте оно означает хост, где работает браузер или скрипт. В контейнере <code>api</code> оно означает сам контейнер <code>api</code>. Оно не означает контейнер <code>db</code>. В стандартной сети Compose сервисы находят друг друга по именам сервисов, поэтому клиент внутри <code>api</code> подключается к <code>db:5432</code>. Опубликованный порт нужен клиенту с хоста, а не соседнему контейнеру.</p>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td><code>ECONNREFUSED localhost:5432</code> внутри <code>api</code></td><td>Клиент обращается к себе</td><td>Показать host внутри <code>api</code></td><td>Использовать <code>db</code> в общей сети</td></tr><tr><td><code>ENOTFOUND db</code></td><td>Процесс вне сети Compose</td><td><code>docker compose ps</code> и <code>docker inspect</code></td><td>Запустить сервисы одним проектом</td></tr><tr><td><code>ENOENT /srv/api/server.js</code></td><td>Путь mount не совпал с командой</td><td>Сверить <code>pwd</code>, mount и <code>working_dir</code></td><td>Согласовать пути хоста и контейнера</td></tr><tr><td>Переменная пустая</td><td>Подстановка не стала env процесса</td><td><code>docker compose config</code> и проверка внутри</td><td>Явно задать <code>environment</code> или <code>env_file</code></td></tr><tr><td>Остались старые данные</td><td>Named volume пережил контейнер</td><td><code>docker volume ls</code></td><td>Описать безопасный чистый старт</td></tr></tbody></table></div>\n<h2>Минимальная конфигурация</h2>\n<p>Ниже учебный пример. Он показывает связи, а не готовую production-среду. API получает рабочий каталог, код с хоста и адрес базы. PostgreSQL не публикует порт наружу: его клиент находится в той же сети. Пароль <code>dev-only</code> демонстрационный и не должен переходить в реальную среду.</p>\n<pre><code>services:\n api:\n build: .\n working_dir: /srv/api\n command: node server.js\n ports:\n - \"8080:8080\"\n volumes:\n - .:/srv/api\n environment:\n NODE_ENV: development\n DATABASE_HOST: db\n DATABASE_PORT: \"5432\"\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: dev-only\n volumes:\n - postgres_data:/var/lib/postgresql/data\n\nvolumes:\n postgres_data:</code></pre>\n<p>Имя <code>db</code> работает только внутри сети, к которой подключён <code>api</code>. Compose создаёт сеть проекта и регистрирует сервисы во внутреннем DNS. IP контейнера не является контрактом: после пересоздания он может измениться. Поэтому адрес базы задают именем сервиса, а не результатом разового <code>docker inspect</code>. Если API запустили отдельно через <code>docker run</code>, связь с сетью Compose могла исчезнуть.</p>\n<p><code>depends_on</code> выражает порядок запуска, но не готовность PostgreSQL принимать соединения. База может ещё выполнять инициализацию. Учебный пример должен проверять реальное подключение после старта. Статус <code>Up</code> не равен готовности.</p>\n<figure><img src=\"/assets/editorial/2020/docker-local-request-path-2020.svg\" alt=\"Схема пути запроса от браузера на хосте через опубликованный порт API к базе db во внутренней сети и named volume PostgreSQL\" loading=\"lazy\" /><figcaption>Иллюстрация разделяет хостовый порт, процесс API, сервисное имя базы и постоянное хранилище. Один localhost не заменяет эти границы.</figcaption></figure>\n<h2>Почему переменная есть, но приложение её не видит</h2>\n<p>Compose сначала собирает итоговую конфигурацию. Для подстановки он может взять значение из оболочки или файла <code>.env</code>. Только явная передача в <code>environment</code> или <code>env_file</code> делает значение окружением процесса. Поэтому проверка должна иметь два наблюдения: итоговый YAML и фактическое окружение внутри <code>api</code>.</p>\n<pre><code>docker compose config\ndocker compose up -d\ndocker compose ps\ndocker compose logs --tail=100 api\ndocker compose exec api sh -lc 'printf "DATABASE_HOST=%s\\n" "$DATABASE_HOST"'\ndocker compose exec api getent hosts db</code></pre>\n<p>Первая команда показывает, что Compose подставил в конфигурацию. Последняя проверяет DNS из пространства контейнера. Между ними остаётся вопрос: использует ли приложение эту переменную. Для этого смотрят безопасный диагностический лог или выполняют известный endpoint. Не печатайте весь <code>env</code>, если рядом есть секреты. Наличие значения и право показать значение — разные решения.</p>\n<h2>Файл на хосте не равен файлу в контейнере</h2>\n<p>Bind mount связывает конкретный путь хоста с конкретным путём внутри контейнера. Если проект смонтирован в <code>/app</code>, а команда запуска ищет <code>/srv/api/server.js</code>, процесс не обязан увидеть код. Монтирование поверх непустой директории также скрывает содержимое образа под mount.</p>\n<p>Проверьте вместе <code>working_dir</code>, путь в <code>command</code> и target mount. Затем зайдите в контейнер: выполните <code>pwd</code>, <code>ls -la</code> и проверку нужного файла. Проверка рабочей копии на хосте отвечает только на вопрос о хосте. Она не доказывает, что контейнер получил тот же файл.</p>\n<p>Named volume решает другую задачу. Он хранит данные вне жизненного цикла контейнера. Поэтому пересоздание <code>db</code> не обязано создавать пустую базу. Это удобно для локальной работы и опасно для эксперимента, которому нужно чистое состояние. Удаление volume разрушительно: сначала зафиксируйте имя и подтвердите, что данные можно потерять.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Опишите один сбой: клиент, адрес, путь, команда и точный текст ошибки. Не начинайте с «Docker не работает».</li><li>Определите наблюдателя: хостовый shell, браузер, процесс <code>api</code> или база.</li><li>Выполните <code>docker compose config</code>. Сверьте имена сервисов, переменные, рабочий каталог и mount.</li><li>Проверьте <code>docker compose ps</code> и логи. Отделите живой процесс от готовой зависимости.</li><li>Изнутри <code>api</code> проверьте имя <code>db</code>, порт и безопасное значение переменной. Не заменяйте проверку хостовым <code>localhost</code>.</li><li>Сверьте путь файла внутри контейнера с <code>working_dir</code>, командой запуска и target bind mount.</li><li>Проверьте named volume. Для пустого старта используйте отдельную подтверждённую процедуру.</li><li>Измените одну границу и повторите тот же запрос. Запишите, какой факт изменился.</li></ol>\n<h2>Ограничения механизма</h2>\n<p>Compose не исправляет несовместимые версии, права доступа, ошибки миграций и неверные credentials. Сервисное имя не делает сеть доступной процессу, который запустили вне проекта. Bind mount не синхронизирует произвольные пути. Named volume не является резервной копией. Эти условия проверяют отдельно.</p>\n<p>Пример не запускался в конкретном проекте читателя и не доказывает production-надежность. Он ограничен локальной схемой из API, PostgreSQL, внутренней сети и двух видов хранилища. Реальная готовность требует своих образов, миграций, прав, версии Compose и проверки отказа зависимости. Учебный пароль и команду запуска нельзя переносить без адаптации.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Локальная среда готова, когда один сценарий на чистом checkout даёт четыре результата: Compose показывает ожидаемую конфигурацию; <code>api</code> разрешает <code>db</code>; приложение устанавливает соединение; нужный файл читается внутри контейнера. Отдельно зафиксируйте сохранение данных после пересоздания и разрешённый чистый старт. Если результат заменён фразой «контейнер запущен», проверка не закончена.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.docker.com/compose/how-tos/networking/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Networking in Compose</a> — сеть проекта и поиск сервисов по имени.</li><li><a href=\"https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Set, use, and manage variables in a Compose file with interpolation</a> — подстановка переменных и команда <code>docker compose config</code>.</li><li><a href=\"https://docs.docker.com/engine/storage/bind-mounts/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Bind mounts</a> — граница между путём хоста и путём внутри контейнера.</li></ul>"
|
||
}
|