Files
progcode/editorial/agent-rewrites/287.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
15 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 &quot;DATABASE_HOST=%s\\n&quot; &quot;$DATABASE_HOST&quot;'\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>"
}