{ "index": 287, "slug": "editorial-2020-01-mechanism-docker-local", "title": "Почему локальный Docker ломается: адреса, файлы и состояние", "excerpt": "Контейнер может быть запущен и всё равно не видеть базу, файл или переменную. Разбираем границы Compose и порядок проверки, который отделяет проблему хоста от проблемы контейнера.", "contentHtml": "

Проблема обычно выглядит так: контейнер api имеет статус Up, но запрос к базе завершается ошибкой; приложение ищет файл и получает ENOENT; переменная есть в терминале, но внутри процесса её нет. Цена ошибки — потерянное время и неверное исправление. Команда меняет Dockerfile, удаляет том или добавляет повторные попытки, хотя причина лежит в адресе, пути монтирования или способе передачи конфигурации.

\n

Главный тезис прост: Docker изолирует процесс и описывает его окружение, но не делает все пространства одинаковыми. Хост, Docker daemon, файловая система контейнера, сеть Compose и окружение процесса имеют разные правила. Нужно назвать наблюдателя для каждого факта. Тогда localhost, путь файла и значение переменной перестают быть двусмысленными.

\n

Сначала восстановите маршрут запроса

\n

Возьмём учебный стек из двух сервисов. Браузер на хосте обращается к опубликованному порту api. Процесс api ищет базу по имени db во внутренней сети Compose. PostgreSQL хранит данные в named volume. Запрос проходит через три адреса и два типа хранилища. Статус контейнера сообщает только о запуске процесса. Он не доказывает готовность базы, правильность переменной или наличие файла по нужному пути.

\n

У слова localhost нет одного смысла. На хосте оно означает хост, где работает браузер или скрипт. В контейнере api оно означает сам контейнер api. Оно не означает контейнер db. В стандартной сети Compose сервисы находят друг друга по именам сервисов, поэтому клиент внутри api подключается к db:5432. Опубликованный порт нужен клиенту с хоста, а не соседнему контейнеру.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
ECONNREFUSED localhost:5432 внутри apiКлиент обращается к себеПоказать host внутри apiИспользовать db в общей сети
ENOTFOUND dbПроцесс вне сети Composedocker compose ps и docker inspectЗапустить сервисы одним проектом
ENOENT /srv/api/server.jsПуть mount не совпал с командойСверить pwd, mount и working_dirСогласовать пути хоста и контейнера
Переменная пустаяПодстановка не стала env процессаdocker compose config и проверка внутриЯвно задать environment или env_file
Остались старые данныеNamed volume пережил контейнерdocker volume lsОписать безопасный чистый старт
\n

Минимальная конфигурация

\n

Ниже учебный пример. Он показывает связи, а не готовую production-среду. API получает рабочий каталог, код с хоста и адрес базы. PostgreSQL не публикует порт наружу: его клиент находится в той же сети. Пароль dev-only демонстрационный и не должен переходить в реальную среду.

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

Имя db работает только внутри сети, к которой подключён api. Compose создаёт сеть проекта и регистрирует сервисы во внутреннем DNS. IP контейнера не является контрактом: после пересоздания он может измениться. Поэтому адрес базы задают именем сервиса, а не результатом разового docker inspect. Если API запустили отдельно через docker run, связь с сетью Compose могла исчезнуть.

\n

depends_on выражает порядок запуска, но не готовность PostgreSQL принимать соединения. База может ещё выполнять инициализацию. Учебный пример должен проверять реальное подключение после старта. Статус Up не равен готовности.

\n
\"Схема
Иллюстрация разделяет хостовый порт, процесс API, сервисное имя базы и постоянное хранилище. Один localhost не заменяет эти границы.
\n

Почему переменная есть, но приложение её не видит

\n

Compose сначала собирает итоговую конфигурацию. Для подстановки он может взять значение из оболочки или файла .env. Только явная передача в environment или env_file делает значение окружением процесса. Поэтому проверка должна иметь два наблюдения: итоговый YAML и фактическое окружение внутри api.

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

Первая команда показывает, что Compose подставил в конфигурацию. Последняя проверяет DNS из пространства контейнера. Между ними остаётся вопрос: использует ли приложение эту переменную. Для этого смотрят безопасный диагностический лог или выполняют известный endpoint. Не печатайте весь env, если рядом есть секреты. Наличие значения и право показать значение — разные решения.

\n

Файл на хосте не равен файлу в контейнере

\n

Bind mount связывает конкретный путь хоста с конкретным путём внутри контейнера. Если проект смонтирован в /app, а команда запуска ищет /srv/api/server.js, процесс не обязан увидеть код. Монтирование поверх непустой директории также скрывает содержимое образа под mount.

\n

Проверьте вместе working_dir, путь в command и target mount. Затем зайдите в контейнер: выполните pwd, ls -la и проверку нужного файла. Проверка рабочей копии на хосте отвечает только на вопрос о хосте. Она не доказывает, что контейнер получил тот же файл.

\n

Named volume решает другую задачу. Он хранит данные вне жизненного цикла контейнера. Поэтому пересоздание db не обязано создавать пустую базу. Это удобно для локальной работы и опасно для эксперимента, которому нужно чистое состояние. Удаление volume разрушительно: сначала зафиксируйте имя и подтвердите, что данные можно потерять.

\n

Порядок проверки

\n
  1. Опишите один сбой: клиент, адрес, путь, команда и точный текст ошибки. Не начинайте с «Docker не работает».
  2. Определите наблюдателя: хостовый shell, браузер, процесс api или база.
  3. Выполните docker compose config. Сверьте имена сервисов, переменные, рабочий каталог и mount.
  4. Проверьте docker compose ps и логи. Отделите живой процесс от готовой зависимости.
  5. Изнутри api проверьте имя db, порт и безопасное значение переменной. Не заменяйте проверку хостовым localhost.
  6. Сверьте путь файла внутри контейнера с working_dir, командой запуска и target bind mount.
  7. Проверьте named volume. Для пустого старта используйте отдельную подтверждённую процедуру.
  8. Измените одну границу и повторите тот же запрос. Запишите, какой факт изменился.
\n

Ограничения механизма

\n

Compose не исправляет несовместимые версии, права доступа, ошибки миграций и неверные credentials. Сервисное имя не делает сеть доступной процессу, который запустили вне проекта. Bind mount не синхронизирует произвольные пути. Named volume не является резервной копией. Эти условия проверяют отдельно.

\n

Пример не запускался в конкретном проекте читателя и не доказывает production-надежность. Он ограничен локальной схемой из API, PostgreSQL, внутренней сети и двух видов хранилища. Реальная готовность требует своих образов, миграций, прав, версии Compose и проверки отказа зависимости. Учебный пароль и команду запуска нельзя переносить без адаптации.

\n

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

\n

Локальная среда готова, когда один сценарий на чистом checkout даёт четыре результата: Compose показывает ожидаемую конфигурацию; api разрешает db; приложение устанавливает соединение; нужный файл читается внутри контейнера. Отдельно зафиксируйте сохранение данных после пересоздания и разрешённый чистый старт. Если результат заменён фразой «контейнер запущен», проверка не закончена.

\n

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

\n" }