DOCKER
#docker#networking#containers#backend

Docker networking: почему localhost не работает между контейнерами

Объяснение работы сетей Docker: почему localhost внутри контейнера — это не хост и как правильно подключаться к сервисам.

Как использовать

  1. Скопируйте нужный фрагмент кода.
  2. Вставьте в свой проект и при необходимости измените под задачу.
  3. Проверьте зависимости и окружение (версии, переменные).

Почему приложение в одном контейнере не может подключиться к базе по 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_URLdb, не 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 — это сам контейнер приложения. Служба БД работает в другом контейнере, для приложения это другой хост.

Решение:

  1. Использовать в Docker окружении хост = имя сервиса (как в docker-compose: db, mysql, postgres и т.п.).
  2. Задавать хост через переменные окружения (DB_HOST, DATABASE_URL и т.д.) и в Compose подставлять имя сервиса.
  3. Для локального запуска без Docker оставить localhost в .env.local или в дефолтах для разработки; в Docker — не переопределять эти переменные на localhost.

Проверка из контейнера приложения:

# Имя сервиса резолвится в IP
docker compose exec app ping -c 1 db

# Или через getent
docker compose exec app getent hosts db

Ссылки: