363 lines
9.3 KiB
Markdown
363 lines
9.3 KiB
Markdown
|
|
# Docker 网络数据库连接指南
|
|||
|
|
|
|||
|
|
## 问题描述
|
|||
|
|
|
|||
|
|
在后端服务连接运行在 Docker 网络中的数据库时,**不能使用 `localhost`** 作为连接地址。
|
|||
|
|
|
|||
|
|
## 原因分析
|
|||
|
|
|
|||
|
|
### 为什么使用 `localhost` 是错误的?
|
|||
|
|
|
|||
|
|
在 Docker 网络中,每个容器都有**独立的网络栈**:
|
|||
|
|
|
|||
|
|
- **`localhost` 在容器内部指向容器自身**(127.0.0.1 指向容器内部)
|
|||
|
|
- **不是指向同一网络中的其他容器**
|
|||
|
|
|
|||
|
|
当你的后端容器尝试连接 `localhost:3306` 时,它实际上是在寻找**自己的 3306 端口**,而不是数据库容器的端口。
|
|||
|
|
|
|||
|
|
#### 直观理解
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────┐
|
|||
|
|
│ Docker 网络(我的网络) │
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|||
|
|
│ │ Backend 容器 │ │ Database 容器 │ │
|
|||
|
|
│ │ │ │ │ │
|
|||
|
|
│ │ localhost ───────┼──→ │ 自己的 8000 端口 │ │
|
|||
|
|
│ │ (127.0.0.1) │ │ │ │
|
|||
|
|
│ │ │ │ │ │
|
|||
|
|
│ │ database ────────┼──→ │ 正确!指向 DB │ │
|
|||
|
|
│ │ (服务名) │ │ │ │
|
|||
|
|
│ └──────────────────┘ └──────────────────┘ │
|
|||
|
|
│ ↑ ↑ │
|
|||
|
|
│ └──────────┬───────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ 只能通过服务名通信 │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 实际会发生什么
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 在 backend 容器内执行
|
|||
|
|
# 尝试连接 localhost:3306
|
|||
|
|
# 结果:Connection refused
|
|||
|
|
# 原因:backend 自己没有在 3306 端口监听服务
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 类比说明
|
|||
|
|
|
|||
|
|
这就好比:
|
|||
|
|
- 你和朋友住在同一个大楼(Docker 网络)
|
|||
|
|
- 你想找朋友
|
|||
|
|
- 你大喊"我在这里"(使用 `localhost`)
|
|||
|
|
- 但实际上你需要喊的是"小明"(使用服务的名字)
|
|||
|
|
- 因为 `"我在这里"` 指的是你自己,不是小明
|
|||
|
|
|
|||
|
|
### Docker 的网络隔离机制
|
|||
|
|
|
|||
|
|
Docker 容器默认使用以下网络模式:
|
|||
|
|
|
|||
|
|
1. **Bridge 网络模式**(默认):
|
|||
|
|
- 每个容器有独立的 IP 地址
|
|||
|
|
- 容器间通过服务名互相发现
|
|||
|
|
- `localhost` 仅指向容器自身
|
|||
|
|
|
|||
|
|
2. **容器间的 DNS 解析**:
|
|||
|
|
```
|
|||
|
|
# 在 backend 容器内
|
|||
|
|
$ ping database
|
|||
|
|
PING database (172.19.0.2) // 解析到数据库容器的 IP
|
|||
|
|
|
|||
|
|
$ ping localhost
|
|||
|
|
PING localhost (127.0.0.1) // 指向自己
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **端口监听位置**:
|
|||
|
|
```
|
|||
|
|
Database 容器内部:MySQL 监听 0.0.0.0:3306
|
|||
|
|
Database 容器 IP:172.19.0.2
|
|||
|
|
Backend 容器不能访问:127.0.0.1:3306(自己的端口)
|
|||
|
|
Backend 容器应该访问:172.19.0.2:3306(通过服务名解析)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 结论
|
|||
|
|
|
|||
|
|
**使用 `localhost` 连接数据库必然失败**,因为它:
|
|||
|
|
- 永远指向调用方容器自己
|
|||
|
|
- 无法跨容器通信
|
|||
|
|
- 违反了 Docker 的网络隔离设计
|
|||
|
|
|
|||
|
|
**正确做法:使用服务名或容器名**,这样 Docker 的内置 DNS 才能正确解析到目标容器。
|
|||
|
|
|
|||
|
|
## 正确做法
|
|||
|
|
|
|||
|
|
### 1. 使用服务名(Service Name)
|
|||
|
|
|
|||
|
|
在 `docker-compose.yml` 中定义的服务名即可用作主机名:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
services:
|
|||
|
|
backend:
|
|||
|
|
build: ./backend
|
|||
|
|
ports:
|
|||
|
|
- "8000:8000"
|
|||
|
|
depends_on:
|
|||
|
|
- database
|
|||
|
|
|
|||
|
|
database:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
ports:
|
|||
|
|
- "3306:3306"
|
|||
|
|
environment:
|
|||
|
|
MYSQL_ROOT_PASSWORD: rootpassword
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**连接配置:**
|
|||
|
|
```python
|
|||
|
|
# 后端连接配置
|
|||
|
|
DATABASE_URL = "mysql://user:password@database:3306/dbname"
|
|||
|
|
# ^^^^^^^^ 使用服务名 "database"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 使用容器名
|
|||
|
|
|
|||
|
|
在 docker-compose 中为容器指定名称:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
database:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
container_name: my-mysql-db # 显式指定容器名
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**连接配置:**
|
|||
|
|
```python
|
|||
|
|
DATABASE_URL = "mysql://user:password@my-mysql-db:3306/dbname"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 网络别名(Network Alias)
|
|||
|
|
|
|||
|
|
如果有多个服务需要连接,可以设置网络别名:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
database:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
networks:
|
|||
|
|
appnet:
|
|||
|
|
aliases:
|
|||
|
|
- db
|
|||
|
|
- mysql
|
|||
|
|
|
|||
|
|
networks:
|
|||
|
|
appnet:
|
|||
|
|
driver: bridge
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**连接配置:**
|
|||
|
|
```python
|
|||
|
|
# 可以使用任一别名
|
|||
|
|
DATABASE_URL = "mysql://user:password@db:3306/dbname"
|
|||
|
|
# 或
|
|||
|
|
DATABASE_URL = "mysql://user:password@mysql:3306/dbname"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见错误
|
|||
|
|
|
|||
|
|
### ❌ 错误写法
|
|||
|
|
```python
|
|||
|
|
# 这会导致连接失败
|
|||
|
|
DATABASE_URL = "mysql://user:password@localhost:3306/dbname"
|
|||
|
|
# ^^^^^^^^^ 指向 backend 自己
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ✅ 正确写法
|
|||
|
|
```python
|
|||
|
|
# 使用 docker-compose 中的服务名
|
|||
|
|
DATABASE_URL = "mysql://user:password@database:3306/dbname"
|
|||
|
|
# ^^^^^^^^ 数据库服务名
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实际示例
|
|||
|
|
|
|||
|
|
### 示例 1:Spring Boot + MySQL
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# docker-compose.yml
|
|||
|
|
version: '3.8'
|
|||
|
|
services:
|
|||
|
|
springboot-app:
|
|||
|
|
build: .
|
|||
|
|
ports:
|
|||
|
|
- "8080:8080"
|
|||
|
|
environment:
|
|||
|
|
SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/mydb
|
|||
|
|
SPRING_DATASOURCE_USERNAME: user
|
|||
|
|
SPRING_DATASOURCE_PASSWORD: password
|
|||
|
|
depends_on:
|
|||
|
|
- mysql-db
|
|||
|
|
|
|||
|
|
mysql-db:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
environment:
|
|||
|
|
MYSQL_DATABASE: mydb
|
|||
|
|
MYSQL_USER: user
|
|||
|
|
MYSQL_PASSWORD: password
|
|||
|
|
MYSQL_ROOT_PASSWORD: rootpassword
|
|||
|
|
ports:
|
|||
|
|
- "3306:3306"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 2:Node.js + PostgreSQL
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# docker-compose.yml
|
|||
|
|
version: '3.8'
|
|||
|
|
services:
|
|||
|
|
api:
|
|||
|
|
build: ./api
|
|||
|
|
ports:
|
|||
|
|
- "3000:3000"
|
|||
|
|
environment:
|
|||
|
|
DATABASE_HOST: postgres
|
|||
|
|
DATABASE_PORT: 5432
|
|||
|
|
DATABASE_NAME: myapp
|
|||
|
|
depends_on:
|
|||
|
|
- postgres
|
|||
|
|
|
|||
|
|
postgres:
|
|||
|
|
image: postgres:14
|
|||
|
|
environment:
|
|||
|
|
POSTGRES_DB: myapp
|
|||
|
|
POSTGRES_USER: user
|
|||
|
|
POSTGRES_PASSWORD: password
|
|||
|
|
ports:
|
|||
|
|
- "5432:5432"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// .env
|
|||
|
|
DATABASE_URL=postgres://user:password@postgres:5432/myapp
|
|||
|
|
// ^^^^^^^^ 服务名
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 3:Python/Django + MongoDB
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# docker-compose.yml
|
|||
|
|
version: '3.8'
|
|||
|
|
services:
|
|||
|
|
web:
|
|||
|
|
build: .
|
|||
|
|
ports:
|
|||
|
|
- "8000:8000"
|
|||
|
|
environment:
|
|||
|
|
MONGO_HOST: mongodb
|
|||
|
|
MONGO_PORT: 27017
|
|||
|
|
depends_on:
|
|||
|
|
- mongodb
|
|||
|
|
|
|||
|
|
mongodb:
|
|||
|
|
image: mongo:6.0
|
|||
|
|
ports:
|
|||
|
|
- "27017:27017"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# settings.py
|
|||
|
|
MONGO_URI = "mongodb://mongodb:27017/mydb"
|
|||
|
|
# ^^^^^^^^ 使用服务名
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 调试技巧
|
|||
|
|
|
|||
|
|
### 1. 进入容器测试连接
|
|||
|
|
```bash
|
|||
|
|
# 进入后端容器
|
|||
|
|
docker exec -it <backend-container> bash
|
|||
|
|
|
|||
|
|
# 测试数据库连接
|
|||
|
|
ping database
|
|||
|
|
telnet database 3306
|
|||
|
|
nc -zv database 3306
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 查看网络信息
|
|||
|
|
```bash
|
|||
|
|
# 查看容器网络
|
|||
|
|
docker network inspect <network-name>
|
|||
|
|
|
|||
|
|
# 查看容器 IP 和别名
|
|||
|
|
docker inspect <database-container> --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 使用 docker network 连接
|
|||
|
|
```bash
|
|||
|
|
# 从宿主机通过容器 IP 连接(不推荐)
|
|||
|
|
docker inspect <database-container> | grep IPAddress
|
|||
|
|
|
|||
|
|
# 正确方式:从另一个容器连接
|
|||
|
|
docker run --rm --network <network-name> mysql:8.0 \
|
|||
|
|
mysql -h database -u user -ppassword dbname
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 特殊情况
|
|||
|
|
|
|||
|
|
### 从宿主机连接数据库
|
|||
|
|
|
|||
|
|
如果需要从**宿主机**(非 Docker 容器)连接数据库,使用:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
database:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
ports:
|
|||
|
|
- "3306:3306" # 映射到宿主机
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**连接方式:**
|
|||
|
|
```python
|
|||
|
|
# 从宿主机运行的应用
|
|||
|
|
DATABASE_URL = "mysql://user:password@localhost:3306/dbname"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**注意:** 这是唯一可以使用 `localhost` 的情况——从宿主机连接暴露端口的容器。
|
|||
|
|
|
|||
|
|
### 开发环境 vs 生产环境
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 开发环境:可能直接使用本地数据库
|
|||
|
|
if os.getenv("ENVIRONMENT") == "development":
|
|||
|
|
DATABASE_URL = "mysql://user:password@localhost:3306/devdb"
|
|||
|
|
else:
|
|||
|
|
# 生产环境:使用 Docker 网络中的数据库
|
|||
|
|
DATABASE_URL = "mysql://user:password@database:3306/proddb"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
1. **使用环境变量**:将数据库连接配置为环境变量,便于在不同环境间切换
|
|||
|
|
2. **使用服务名**:优先使用 docker-compose 中的服务名,保持配置一致性
|
|||
|
|
3. **健康检查**:使用 `depends_on` + 健康检查确保数据库就绪后再启动应用
|
|||
|
|
4. **网络隔离**:为不同环境使用不同的网络
|
|||
|
|
5. **安全性**:不要在代码中硬编码密码,使用 secrets 或环境变量
|
|||
|
|
|
|||
|
|
## 故障排查清单
|
|||
|
|
|
|||
|
|
- [ ] 检查 docker-compose.yml 中数据库服务的名称
|
|||
|
|
- [ ] 确认后端使用的是服务名而非 `localhost`
|
|||
|
|
- [ ] 确认数据库服务在同一个网桥网络中
|
|||
|
|
- [ ] 检查端口映射配置(如果从宿主机连接)
|
|||
|
|
- [ ] 验证容器间的网络连通性(使用 `ping` 或 `telnet`)
|
|||
|
|
- [ ] 检查防火墙规则
|
|||
|
|
- [ ] 查看容器日志确认数据库是否正常启动
|
|||
|
|
|
|||
|
|
## 参考资源
|
|||
|
|
|
|||
|
|
- [Docker Networking](https://docs.docker.com/network/)
|
|||
|
|
- [Docker Compose Service Discovery](https://docs.docker.com/compose/networking/)
|
|||
|
|
- [Connect to MySQL from another container](https://hub.docker.com/_/mysql)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**最后更新:** 2026-04-21
|