Docker 容器里 localhost 为什么连不上 MySQL/Redis?一文讲透 Compose 网络、端口与排错

本文定位:可复现教程 + 故障排查手册

你将一次讲清:容器中的 localhostHOST_PORT:CONTAINER_PORT、Compose DNS、service_healthyhost.docker.internal 与六步排错路径。

阅读路线: 先理解地址模型 → 再运行示例 → 最后按错误类型排查。

明明数据库已经启动,宿主机也能连接,应用放进 Docker 后却报 Connection refused;把地址从 localhost 改成容器 IP,重启后又失效——这类问题的根源通常不是数据库,而是没有分清“当前机器是谁”。

无论连接 MySQL、PostgreSQL、Redis、RabbitMQ 还是另一个后端服务,只要你真正理解下面三件事,大部分 Docker 网络问题都能快速定位:

  1. 容器中的 localhost 指向谁;
  2. 15432:5432 两个端口分别给谁使用;
  3. Compose 为什么应该使用服务名,而不是固定容器 IP。

本文给出一套可以复用的判断模型、完整 Compose 示例和六步排错命令。


一、最重要的结论:每个容器都有自己的 localhost

先看最常见的错误配置:

services:
  api:
    environment:
      DATABASE_URL: postgresql://demo:demo@localhost:5432/demo

  db:
    image: postgres:18-alpine

api 进程运行在容器中时,localhost127.0.0.1 指向的是 api 容器自己,不是宿主机,也不是 db 容器。

Docker 中 localhost 与网络命名空间的关系

可以把宿主机和每个容器想成独立的房间。每个房间都有一部号码相同的“内部电话”——127.0.0.1,但这部电话只能拨回当前房间。

因此:

代码运行位置localhost 指向
Windows、macOS 或 Linux 宿主机宿主机自己
api 容器api 容器自己
db 容器db 容器自己

如果 PostgreSQL 在 db 容器的 5432 端口监听,而 api 容器访问 localhost:5432,操作系统会在 api 容器内部寻找 5432 端口。那里没有 PostgreSQL,结果通常就是:

Connection refused

正确地址应当是:

postgresql://demo:demo@db:5432/demo

这里的 db 是 Compose 文件中的服务名。


二、15432:5432 到底哪一个端口给容器用?

Compose 中常见的端口配置:

services:
  db:
    image: postgres:18-alpine
    ports:
      - "15432:5432"

它的含义是:

HOST_PORT:CONTAINER_PORT
宿主机端口:容器端口

也就是说,宿主机的 15432 被转发到 db 容器的 5432

宿主机端口与容器端口的两条访问路径

两种访问场景不能混用:

场景 A:宿主机访问数据库

在宿主机运行数据库客户端:

psql -h localhost -p 15432 -U demo -d demo

路径是:

宿主机 localhost:15432
        ↓ 端口映射
db 容器 5432

场景 B:api 容器访问 db 容器

容器之间在同一个 Compose 网络内直接通信:

db:5432

它们不需要绕到宿主机的 15432,也不依赖 ports

记忆规则:宿主机访问容器使用发布端口;容器访问容器使用服务名和容器端口。

甚至可以不发布数据库端口:

services:
  api:
    # api 与 db 在同一网络中,仍然可以访问 db:5432

  db:
    image: postgres:18-alpine
    expose:
      - "5432"

expose 主要承担声明和文档作用;同一网络内的容器通信并不依赖把端口发布到宿主机。只有宿主机或外部网络需要直接访问数据库时,才使用 ports


三、Compose 服务名为什么比容器 IP 可靠?

执行 docker compose up 时,Compose 默认会:

  1. 创建一个名为 <项目名>_default 的网络;
  2. 将没有显式指定网络的服务加入该网络;
  3. 通过 Docker 内置 DNS 注册服务名;
  4. 让同一网络中的容器通过服务名互相发现。

例如:

services:
  api:
    build: .
  db:
    image: postgres:18-alpine

api 可以直接解析主机名 db。在用户自定义网络中,Docker 的内置 DNS 地址通常是 127.0.0.11

Docker 官方文档:Compose 默认网络与服务发现

图源:Docker Docs《Networking in Compose》,截图保留了官方示例中的 postgres://db:5432 与宿主机 postgres://localhost:8001 对比。

容器被重新创建后,IP 可能从 172.20.0.3 变成 172.20.0.7,但服务名仍然是 db。因此不要这样配置:

postgresql://demo:demo@172.20.0.3:5432/demo

应当始终写:

postgresql://demo:demo@db:5432/demo

这一点也解释了为什么“先用 docker inspect 找 IP,再把 IP 写进配置”只是临时绕过问题,并不是正确修复。


四、一个可以复现的正确示例

目录结构:

docker-network-demo/
├── compose.yaml
├── Dockerfile
├── requirements.txt
└── app.py

1. compose.yaml

services:
  api:
    build: .
    environment:
      DATABASE_URL: postgresql://demo:demo@db:5432/demo
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8000:8000"
    networks:
      - backend

  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: demo
      POSTGRES_PASSWORD: demo
      POSTGRES_DB: demo
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U demo -d demo"]
      interval: 3s
      timeout: 3s
      retries: 10
      start_period: 5s
    ports:
      - "15432:5432"
    networks:
      - backend

networks:
  backend:

注意数据库连接串:

postgresql://demo:demo@db:5432/demo
  • db:Compose 服务名;
  • 5432:PostgreSQL 在容器内部监听的端口;
  • 没有使用 localhost
  • 没有使用宿主机发布端口 15432

2. Dockerfile

FROM python:3.13-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .
CMD ["python", "app.py"]

3. requirements.txt

flask==3.1.2
psycopg[binary]==3.2.9

4. app.py

import os

import psycopg
from flask import Flask, jsonify

app = Flask(__name__)
database_url = os.environ["DATABASE_URL"]


@app.get("/")
def index():
    with psycopg.connect(database_url) as connection:
        with connection.cursor() as cursor:
            cursor.execute(
                "select current_database(), inet_server_addr(), inet_server_port()"
            )
            database, server_address, server_port = cursor.fetchone()

    return jsonify(
        {
            "status": "ok",
            "database": database,
            "server_address": str(server_address),
            "server_port": server_port,
        }
    )


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

这里 Flask 必须监听 0.0.0.0。如果只监听 127.0.0.1,即使发布了 8000:8000,从容器外部也可能无法访问。

5. 启动与验证

docker compose up --build

浏览器访问:

http://localhost:8000

预期得到类似结果:

{
  "database": "demo",
  "server_address": "172.20.0.2",
  "server_port": 5432,
  "status": "ok"
}

由于本文运行环境没有安装 Docker,本示例完成了 Python 语法检查和 Compose 结构检查,但没有在本文环境中实际拉取镜像运行;以上 IP 仅为格式示例,真实地址由 Docker 动态分配。


五、depends_on 不等于“数据库已经可以连接”

Docker 官方文档:Compose 启动顺序与 service_healthy

图源:Docker Docs《Control startup and shutdown order in Compose》。官方明确区分了“容器正在运行”和“服务已经 ready”。

下面的短写法只表达启动顺序:

depends_on:
  - db

它能让 Compose 先启动 db 容器,再启动 api,但“容器进程已启动”不代表“数据库已完成初始化并可以接受连接”。

更可靠的写法是为数据库定义健康检查,再使用 service_healthy

db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U demo -d demo"]
    interval: 3s
    timeout: 3s
    retries: 10
    start_period: 5s

api:
  depends_on:
    db:
      condition: service_healthy
api 容器 healthcheck db 容器 Docker Compose api 容器 healthcheck db 容器 Docker Compose loop [直到检查通过] 创建并启动 db 执行 pg_isready starting / unhealthy healthy 创建并启动 api 连接 db:5432

生产系统中,应用自身仍应实现连接超时、有限重试和断线重连,因为数据库可能在运行期间重启。


六、不要把三类连接错误混为一谈

Docker 连接失败错误类型决策树

1. Name or service not known

这通常表示 DNS 解析失败,重点检查:

  • 服务名是否拼错;
  • 两个服务是否真的共享网络;
  • 是否在一个 Compose 项目中;
  • 跨项目时是否加入同一个 external network。

验证:

docker compose exec api getent hosts db

2. Connection refused

这意味着目标地址通常已经可达,但目标端口没有进程接受连接。重点检查:

  • 数据库是否已经启动完成;
  • 连接的是容器端口还是宿主机端口;
  • 程序是否监听在正确端口;
  • 服务是否只绑定了 127.0.0.1
  • 容器健康检查是否通过。

3. Connection timed out

超时更像是数据包没有得到响应,重点检查:

  • 两个容器是否加入同一网络;
  • 防火墙、安全组或网络策略;
  • 路由是否正确;
  • 目标地址是否指向了错误网段。

4. password authentication failed

这已经不是网络层错误。它说明客户端大概率已经到达数据库,应转向检查:

  • 用户名、密码、数据库名;
  • 环境变量是否被 Compose 正确展开;
  • 旧数据卷中是否保留了以前的账号配置。

七、六条命令,从外到内定位问题

第一步:检查容器状态

docker compose ps

确认容器是否运行、健康检查是否通过、端口是否按预期发布。

第二步:检查最终生效配置

docker compose config

它可以发现环境变量展开、多个 Compose 文件合并、缩进等问题。不要只看编辑器里的原始 YAML,要看 Compose 最终解析后的配置。

第三步:检查网络成员

docker network ls
docker network inspect <项目名>_backend

Containers 中确认通信双方是否同时存在。

第四步:从调用方容器验证 DNS

docker compose exec api getent hosts db

如果 db 无法解析,先修服务名和网络,不要继续调整数据库密码。

第五步:验证 TCP 端口

docker compose exec api nc -vz db 5432

部分精简镜像没有 nc,可以临时使用同网络调试容器:

docker run --rm --network <项目名>_backend nicolaka/netshoot nc -vz db 5432

第六步:查看目标服务日志

docker compose logs --tail=100 db

重点寻找初始化失败、数据目录权限、端口占用、配置错误和认证失败。


八、需要访问宿主机服务时怎么办?

有些数据库并不在容器中,而是运行在宿主机。此时容器仍然不能用 localhost 访问宿主机。

Docker 官方文档:容器通过 host.docker.internal 访问宿主机

图源:Docker Docs《Explore networking how-tos on Docker Desktop》,包含官方 host.docker.internal 示例命令。

在 Docker Desktop 中,可以使用:

host.docker.internal

例如:

redis://host.docker.internal:6379

在 Linux Docker Engine 中,可显式增加 host-gateway 映射:

services:
  api:
    extra_hosts:
      - "host.docker.internal:host-gateway"

然后应用仍然使用:

host.docker.internal

注意宿主机服务必须监听可被 Docker 网桥访问的地址;如果它只监听宿主机的 127.0.0.1,容器依然可能连接失败。


九、用网络分段减少不必要的横向访问

容器能够通信的前提是共享网络。利用这一点,可以让架构更符合最小权限原则。

services:
  proxy:
    image: nginx:alpine
    ports:
      - "80:80"
    networks:
      - frontend

  api:
    build: .
    networks:
      - frontend
      - backend

  db:
    image: postgres:18-alpine
    networks:
      - backend

networks:
  frontend:
  backend:

通信关系如下:

发布端口 80

frontend 网络

backend 网络

不共享 backend,不能直连

外部用户

proxy

api

db

这样:

  • 外部只接触 proxy
  • proxy 可以访问 api
  • api 同时连接前端和后端网络;
  • db 不加入前端网络;
  • proxy 无法直接访问数据库。

不要为了“省事”把所有服务都加入所有网络,也不要把数据库端口无条件发布到 0.0.0.0


十、常见错误配置速查

错误写法为什么错推荐写法
容器内连接 localhost:5432指向当前容器自己db:5432
容器内连接 db:15432使用了宿主机发布端口db:5432
写死 172.20.0.3重建后 IP 可能变化使用服务名 db
只有 depends_on: [db]不等待数据库真正就绪healthcheck + service_healthy
服务只监听 127.0.0.1其他容器无法访问监听 0.0.0.0 或容器接口
两个服务不共享网络DNS 与路由均不成立加入同一命名网络
所有端口都用 ports 发布增大暴露面只发布外部确实需要的端口

总结

Docker 网络排错不需要背很多命令,先建立正确的地址模型:

  1. localhost 永远指向当前网络命名空间自己;
  2. 宿主机访问容器使用 HOST_PORT
  3. 容器之间使用 SERVICE:CONTAINER_PORT
  4. 服务名稳定,容器 IP 不稳定;
  5. 共享网络才具备直接通信条件;
  6. 先判断 DNS、拒绝、超时还是认证错误,再检查对应层。

下次再遇到 Connection refused,不要先重装 Docker,也不要急着把端口全部暴露。依次检查:

DNS → 网络成员 → 端口监听 → 应用就绪 → 认证配置

在哪一步失败,就优先修哪一层。


参考资料


更多推荐