Docker networking: почему localhost не работает между контейнерами
Объяснение работы сетей Docker: почему localhost внутри контейнера — это не хост и как правильно подключаться к сервисам.
Как использовать
- Скопируйте нужный фрагмент кода.
- Вставьте в свой проект и при необходимости измените под задачу.
- Проверьте зависимости и окружение (версии, переменные).
Почему приложение в одном контейнере не может подключиться к базе по localhost или 127.0.0.1, хотя на машине разработчика так работало? Потому что внутри контейнера localhost — это сам контейнер, а не хост и не другой контейнер. Ниже — по официальной документации Docker Networking.
Что такое localhost в контексте Docker
localhost и 127.0.0.1 — это loopback-адрес: обращение «к самому себе». На обычной машине это твой компьютер. В контейнере у каждого контейнера свой сетевой namespace: у него свой loopback, своя сетевая конфигурация.
Из документации: контейнер видит только сетевой интерфейс с IP, шлюзом, таблицей маршрутизации и DNS. Он не знает, что он «в Docker» и кто ещё в сети — для него 127.0.0.1 и localhost указывают на него самого.
В /etc/hosts внутри контейнера прописаны hostname контейнера и localhost. Дополнительные записи с хоста в контейнер не наследуются.
Почему 127.0.0.1 внутри контейнера — это сам контейнер
В документации явно сказано: при использовании флага --dns=127.0.0.1 это loopback самого контейнера, а не хоста.
Итог:
- На хосте:
127.0.0.1иlocalhost— твоя машина; сервисы на ней доступны по этому адресу. - В контейнере app:
127.0.0.1иlocalhost— это контейнер app; там слушает только то, что запущено внутри этого контейнера. - В контейнере db: свой
127.0.0.1— это контейнер db.
Поэтому строка подключения с 127.0.0.1 или localhost из приложения в контейнере ведёт к «самому себе», а не к контейнеру с базой. База в другом контейнере — это другой хост с точки зрения сети.
Как Docker DNS разрешает имена сервисов
На default bridge контейнеры по имени друг друга не видят — только по IP. На user-defined network (созданной через docker network create или в Docker Compose по умолчанию) работает embedded DNS Docker.
Из документации:
- Контейнеры в user-defined сети могут общаться по имени контейнера (или по имени сервиса в Compose).
- Embedded DNS сервер обрабатывает разрешение имён внутри сети; внешние запросы он пробрасывает на DNS хоста.
Имя сервиса в docker-compose.yml (или имя контейнера при docker run --name) и есть то самое «имя хоста», по которому нужно подключаться из другого контейнера в той же сети. Например, если сервис называется db, подключаться нужно к db, а не к localhost.
Правильный способ подключения (service name)
Подключаться к другому контейнеру по имени сервиса (контейнера) в той же сети. Порт — внутренний порт контейнера (например, 5432 для PostgreSQL, 3306 для MySQL), не проброшенный на хост.
Примеры строк подключения:
- PostgreSQL:
postgresql://db:5432/mydb(сервис/контейнерdb, порт 5432 внутри контейнера). - MySQL:
mysql://db:3306/mydbили хостdb, порт 3306.
Имя db резолвится в IP контейнера с базой в той же Docker-сети.
Пример docker-compose.yml
Одна сеть по умолчанию, два сервиса: приложение и база. Подключение по имени сервиса.
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:secret@db:5432/appdb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
И app, и db попадают в одну user-defined сеть; имя db резолвится в IP контейнера с PostgreSQL. Хост в DATABASE_URL — db, не localhost.
Реальные примеры
Node.js → PostgreSQL
Неправильно (localhost внутри контейнера app):
// ❌ Будет подключаться к порту 5432 внутри контейнера app, а не к контейнеру db
const connectionString = "postgresql://postgres:secret@localhost:5432/appdb";
Правильно (имя сервиса):
// ✅ db — имя сервиса в docker-compose, порт 5432 — внутренний порт контейнера db
const connectionString =
process.env.DATABASE_URL || "postgresql://postgres:secret@db:5432/appdb";
В docker-compose у сервиса БД имя db, переменная:
environment:
DATABASE_URL: postgresql://postgres:secret@db:5432/appdb
Локально без Docker можно оставить localhost:5432 через отдельный .env или значение по умолчанию в коде в зависимости от NODE_ENV / флага.
PHP → MySQL
Неправильно:
// ❌ localhost внутри контейнера PHP — это сам контейнер PHP, MySQL там нет
$host = '127.0.0.1';
$dsn = "mysql:host=$host;port=3306;dbname=appdb";
Правильно:
// ✅ db — имя сервиса MySQL в docker-compose
$host = getenv('DB_HOST') ?: 'db';
$port = getenv('DB_PORT') ?: '3306';
$dbname = getenv('DB_NAME') ?: 'appdb';
$dsn = "mysql:host=$host;port=$port;dbname=$dbname";
docker-compose для PHP + MySQL:
services:
app:
image: php:8.2-fpm-alpine
volumes:
- ./src:/var/www/html
environment:
DB_HOST: db
DB_PORT: "3306"
DB_NAME: appdb
DB_USER: app
DB_PASSWORD: secret
depends_on:
- db
db:
image: mysql:8
environment:
MYSQL_DATABASE: appdb
MYSQL_USER: app
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
volumes:
- mysqldata:/var/lib/mysql
volumes:
mysqldata:
Подключение из PHP идёт к хосту db (имя сервиса), порт 3306 — внутренний порт контейнера MySQL.
Частая ошибка и правильное решение
Ошибка: В коде или в .env указан DB_HOST=localhost / 127.0.0.1. Локально на машине всё работает, в Docker приложение в контейнере не может подключиться к базе (connection refused, timeout).
Причина: В контейнере приложения localhost — это сам контейнер приложения. Служба БД работает в другом контейнере, для приложения это другой хост.
Решение:
- Использовать в Docker окружении хост = имя сервиса (как в
docker-compose:db,mysql,postgresи т.п.). - Задавать хост через переменные окружения (
DB_HOST,DATABASE_URLи т.д.) и в Compose подставлять имя сервиса. - Для локального запуска без Docker оставить
localhostв.env.localили в дефолтах для разработки; в Docker — не переопределять эти переменные на localhost.
Проверка из контейнера приложения:
# Имя сервиса резолвится в IP
docker compose exec app ping -c 1 db
# Или через getent
docker compose exec app getent hosts db
Ссылки: