8 lines
16 KiB
JSON
8 lines
16 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>Сценарий: 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>"
|
||
}
|