{ "index": 287, "slug": "editorial-2020-01-mechanism-docker-local", "title": "Почему локальный Docker ломается: адреса, файлы и состояние", "excerpt": "Контейнер может быть запущен и всё равно не видеть базу, файл или переменную. Разбираем границы Compose и порядок проверки, который отделяет проблему хоста от проблемы контейнера.", "contentHtml": "
Проблема обычно выглядит так: контейнер api имеет статус Up, но запрос к базе завершается ошибкой; приложение ищет файл и получает ENOENT; переменная есть в терминале, но внутри процесса её нет. Цена ошибки — потерянное время и неверное исправление. Команда меняет Dockerfile, удаляет том или добавляет повторные попытки, хотя причина лежит в адресе, пути монтирования или способе передачи конфигурации.
Главный тезис прост: Docker изолирует процесс и описывает его окружение, но не делает все пространства одинаковыми. Хост, Docker daemon, файловая система контейнера, сеть Compose и окружение процесса имеют разные правила. Нужно назвать наблюдателя для каждого факта. Тогда localhost, путь файла и значение переменной перестают быть двусмысленными.
Возьмём учебный стек из двух сервисов. Браузер на хосте обращается к опубликованному порту api. Процесс api ищет базу по имени db во внутренней сети Compose. PostgreSQL хранит данные в named volume. Запрос проходит через три адреса и два типа хранилища. Статус контейнера сообщает только о запуске процесса. Он не доказывает готовность базы, правильность переменной или наличие файла по нужному пути.
У слова localhost нет одного смысла. На хосте оно означает хост, где работает браузер или скрипт. В контейнере api оно означает сам контейнер api. Оно не означает контейнер db. В стандартной сети Compose сервисы находят друг друга по именам сервисов, поэтому клиент внутри api подключается к db:5432. Опубликованный порт нужен клиенту с хоста, а не соседнему контейнеру.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
ECONNREFUSED localhost:5432 внутри api | Клиент обращается к себе | Показать host внутри api | Использовать db в общей сети |
ENOTFOUND db | Процесс вне сети Compose | docker 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 | Описать безопасный чистый старт |
Ниже учебный пример. Он показывает связи, а не готовую production-среду. API получает рабочий каталог, код с хоста и адрес базы. PostgreSQL не публикует порт наружу: его клиент находится в той же сети. Пароль dev-only демонстрационный и не должен переходить в реальную среду.
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 могла исчезнуть.
depends_on выражает порядок запуска, но не готовность PostgreSQL принимать соединения. База может ещё выполнять инициализацию. Учебный пример должен проверять реальное подключение после старта. Статус Up не равен готовности.
Compose сначала собирает итоговую конфигурацию. Для подстановки он может взять значение из оболочки или файла .env. Только явная передача в environment или env_file делает значение окружением процесса. Поэтому проверка должна иметь два наблюдения: итоговый YAML и фактическое окружение внутри api.
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, если рядом есть секреты. Наличие значения и право показать значение — разные решения.
Bind mount связывает конкретный путь хоста с конкретным путём внутри контейнера. Если проект смонтирован в /app, а команда запуска ищет /srv/api/server.js, процесс не обязан увидеть код. Монтирование поверх непустой директории также скрывает содержимое образа под mount.
Проверьте вместе working_dir, путь в command и target mount. Затем зайдите в контейнер: выполните pwd, ls -la и проверку нужного файла. Проверка рабочей копии на хосте отвечает только на вопрос о хосте. Она не доказывает, что контейнер получил тот же файл.
Named volume решает другую задачу. Он хранит данные вне жизненного цикла контейнера. Поэтому пересоздание db не обязано создавать пустую базу. Это удобно для локальной работы и опасно для эксперимента, которому нужно чистое состояние. Удаление volume разрушительно: сначала зафиксируйте имя и подтвердите, что данные можно потерять.
api или база.docker compose config. Сверьте имена сервисов, переменные, рабочий каталог и mount.docker compose ps и логи. Отделите живой процесс от готовой зависимости.api проверьте имя db, порт и безопасное значение переменной. Не заменяйте проверку хостовым localhost.working_dir, командой запуска и target bind mount.Compose не исправляет несовместимые версии, права доступа, ошибки миграций и неверные credentials. Сервисное имя не делает сеть доступной процессу, который запустили вне проекта. Bind mount не синхронизирует произвольные пути. Named volume не является резервной копией. Эти условия проверяют отдельно.
\nПример не запускался в конкретном проекте читателя и не доказывает production-надежность. Он ограничен локальной схемой из API, PostgreSQL, внутренней сети и двух видов хранилища. Реальная готовность требует своих образов, миграций, прав, версии Compose и проверки отказа зависимости. Учебный пароль и команду запуска нельзя переносить без адаптации.
\nЛокальная среда готова, когда один сценарий на чистом checkout даёт четыре результата: Compose показывает ожидаемую конфигурацию; api разрешает db; приложение устанавливает соединение; нужный файл читается внутри контейнера. Отдельно зафиксируйте сохранение данных после пересоздания и разрешённый чистый старт. Если результат заменён фразой «контейнер запущен», проверка не закончена.
docker compose config.