Files
progcode/editorial/agent-rewrites/287.json
T

8 lines
16 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>Сценарий: API ищет базу не там</h2>\n<p>Возьмём воспроизводимый стек из двух сервисов. Браузер на хосте обращается к опубликованному порту <code>api</code>. Процесс <code>api</code> ищет PostgreSQL по имени <code>db</code> во внутренней сети Compose. База хранит данные в named volume. Запрос проходит через несколько границ, а статус контейнера сообщает только о запуске процесса. Он не доказывает готовность базы, правильность переменной или наличие файла по нужному пути.</p>\n<p>Сначала разработчик повторяет запрос из браузера и записывает точный адрес и текст ошибки. Затем он выполняет ту же проверку из контейнера <code>api</code>. После этого сравнивает имя сервиса, порт и результат DNS. Если внутри <code>api</code> указан <code>localhost</code>, клиент обращается к самому <code>api</code>, а не к <code>db</code>. В стандартной сети Compose соседний сервис находится по имени <code>db</code> и внутреннему порту <code>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>Ниже учебный пример для контракта границ. В нём API получает рабочий каталог, код с хоста и адрес базы, а PostgreSQL не публикует порт наружу: его клиент находится в той же сети. В примерах сохранён вызов <code>docker-compose</code>, характерный для начала 2020 года; в современном Compose используется та же подкоманда после пробела — <code>docker compose</code>. Пароль <code>dev-only</code> демонстрационный и не должен переходить в реальную среду.</p>\n<pre><code>version: '3.7'\nservices:\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:12-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 принимать соединения. База может ещё выполнять инициализацию. Сначала ждём результат проверки соединения или используем отдельную healthcheck-логику; после этого считаем зависимость готовой. Статус <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> делает значение окружением процесса. Поэтому нужны два наблюдения: что получил Compose и что реально видит <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 printenv DATABASE_HOST\ndocker-compose exec api getent hosts db</code></pre>\n<p>Первая команда показывает итоговую конфигурацию после подстановки. Последняя проверяет 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>Пример ограничен локальной схемой из API, PostgreSQL, внутренней сети и двух видов хранилища. Он не доказывает готовность конкретного проекта: свои образы, миграции, права, версию Compose и отказ зависимости нужно проверить отдельно. Учебный пароль и команду запуска нельзя переносить без адаптации.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Локальная среда готова, когда один сценарий на чистой рабочей копии даёт четыре результата: 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><li><a href='https://docs.docker.com/engine/storage/volumes/' target='_blank' rel='noopener noreferrer'>Docker Docs: Volumes</a> — жизненный цикл и свойства named volumes.</li></ul>"
}