{ "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 ищет PostgreSQL по имени db во внутренней сети Compose. База хранит данные в named volume. Запрос проходит через несколько границ, а статус контейнера сообщает только о запуске процесса. Он не доказывает готовность базы, правильность переменной или наличие файла по нужному пути.
Сначала разработчик повторяет запрос из браузера и записывает точный адрес и текст ошибки. Затем он выполняет ту же проверку из контейнера api. После этого сравнивает имя сервиса, порт и результат DNS. Если внутри api указан localhost, клиент обращается к самому api, а не к db. В стандартной сети Compose соседний сервис находится по имени 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 | Описать безопасный чистый старт |
Ниже учебный пример для контракта границ. В нём API получает рабочий каталог, код с хоста и адрес базы, а PostgreSQL не публикует порт наружу: его клиент находится в той же сети. В примерах сохранён вызов docker-compose, характерный для начала 2020 года; в современном Compose используется та же подкоманда после пробела — docker compose. Пароль dev-only демонстрационный и не должен переходить в реальную среду.
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:\nИмя db работает только внутри сети, к которой подключён api. Compose создаёт сеть проекта и публикует в её внутреннем DNS имена сервисов. IP контейнера не является контрактом: после пересоздания он может измениться. Поэтому адрес базы задают именем сервиса, а не результатом разового docker inspect. Если API запустили отдельно через docker run, связь с сетью Compose могла исчезнуть.
depends_on в такой записи задаёт порядок запуска, но не готовность PostgreSQL принимать соединения. База может ещё выполнять инициализацию. Сначала ждём результат проверки соединения или используем отдельную healthcheck-логику; после этого считаем зависимость готовой. Статус Up сам по себе готовность не подтверждает.
Compose сначала собирает итоговую конфигурацию. Для подстановки он может взять значение из оболочки или файла .env. Явная передача через environment или env_file делает значение окружением процесса. Поэтому нужны два наблюдения: что получил Compose и что реально видит api.
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\nПервая команда показывает итоговую конфигурацию после подстановки. Последняя проверяет 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Пример ограничен локальной схемой из API, PostgreSQL, внутренней сети и двух видов хранилища. Он не доказывает готовность конкретного проекта: свои образы, миграции, права, версию Compose и отказ зависимости нужно проверить отдельно. Учебный пароль и команду запуска нельзя переносить без адаптации.
\nЛокальная среда готова, когда один сценарий на чистой рабочей копии даёт четыре результата: Compose показывает ожидаемую конфигурацию; api разрешает db; приложение устанавливает соединение; нужный файл читается внутри контейнера. Отдельно зафиксируйте сохранение данных после пересоздания и разрешённый чистый старт. Если результат заменён фразой «контейнер запущен», проверка не закончена.
docker compose config.