📌 本文由 11 篇相关文章智能合并整理而成
Docker Compose 与 Kubernetes 的对比
Docker Compose 与 Kubernetes 的对比
定位差异
| 对比维度 | Docker Compose | Kubernetes |
|---|---|---|
| 定位 | 单机容器编排 | 分布式容器编排 |
| 复杂度 | 低,几分钟上手 | 高,学习曲线陡峭 |
| 规模 | 单台主机 | 成百上千节点 |
| 高可用 | 手动或借助额外工具 | 原生支持 |
| 自动扩缩容 | 手动 scale | 自动 HPA |
| 服务发现 | DNS(单机) | DNS + Service + Ingress |
| 存储 | Volume/Bind Mount | PV/PVC/StorageClass |
| 网络 | 简单 bridge | CNI 插件(Calico/Flannel) |
| 自我修复 | restart: always | ReplicaSet + Liveness Probe |
| 滚动更新 | 有限支持 | 原生支持 |
| 密钥管理 | env_file/secrets | Secret/ConfigMap |
适用场景
什么时候用 Compose
# 本地开发:快速启动依赖服务
docker compose up -d
# CI 测试:启动数据库和测试用依赖
docker compose run test
# 小型项目:单机部署足够
# 如果不需要多机扩缩容和高可用
Compose 的优势场景:
– 开发环境
– 本地测试
– CI/CD 集成测试
– 单机部署的小型应用
– 原型验证
什么时候用 Kubernetes
# 生产环境多机部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
spec:
containers:
- name: web
image: myapp:latest
resources:
limits:
memory: "512Mi"
cpu: "500m"
Kubernetes 的优势场景:
– 多主机生产环境
– 大规模微服务
– 需要高可用和自愈
– 自动扩缩容
– 复杂网络策略
– 多租户
核心概念映射
| Compose 概念 | Kubernetes 对应概念 |
|---|---|
| Service | Deployment + Service |
| volumes | PersistentVolumeClaim |
| networks | NetworkPolicy |
| depends_on | Init Container + Readiness Probe |
| ports: “80:80” | Service (NodePort/LoadBalancer) |
| environment | ConfigMap / Secret |
| healthcheck | Liveness / Readiness Probe |
| scale | HPA (HorizontalPodAutoscaler) |
| restart | ReplicaSet 自动恢复 |
| build | Docker build + 镜像仓库 |
从 Compose 迁移到 Kubernetes
步骤 1:容器化应用
确保应用有完整的 Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
步骤 2:创建 Kubernetes Deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: app
spec:
replicas: 3
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: registry.example.com/myapp:1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DB_HOST
value: "postgres-service"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
步骤 3:创建 Service
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
selector:
app: app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
步骤 4:配置 ConfigMap
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "warn"
REDIS_URL: "redis://redis-service:6379"
步骤 5:持久化存储
# k8s/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Compose 转 Kubernetes 工具
Kompose
# 自动将 docker-compose.yml 转换为 Kubernetes 资源
kompose convert -f docker-compose.yml
# 直接部署到 Kubernetes
kompose up -f docker-compose.yml
DevSpace
# 在 Kubernetes 中获得类似 Compose 的开发体验
devspace init
devspace dev
选择建议
团队规模与技能
| 团队情况 | 推荐方案 |
|---|---|
| 小团队、无运维 | Docker Compose |
| 有运维但无 K8s 经验 | Docker Swarm 或托管 K8s |
| 有 K8s 运维团队 | Kubernetes |
项目阶段
# 原型/PoC 阶段:Compose
docker compose up -d
# 产品上线初期(单机):Compose
docker compose -f docker-compose.prod.yml up -d
# 规模化后(多机):Kubernetes
kubectl apply -f k8s/
面试要点
Q:Kubernetes 比 Compose 强在哪里?
A:Kubernetes 解决了多主机场景下的自动调度、服务发现、负载均衡、自动扩缩容、自我修复、滚动更新、配置管理等问题。Compose 只解决单机多容器编排问题。
Q:小项目一定要用 Kubernetes 吗?
A:不一定。如果只有 1-2 台主机,团队没有 K8s 经验,Docker Compose + 简单运维脚本完全可以满足需求。不要为了用 K8s 而用 K8s。
Q:从 Compose 迁移到 Kubernetes 的最大挑战是什么?
A:学习曲线和运维复杂度。需要理解 Pod、Service、Ingress、PV/PVC、ConfigMap/Secret、RBAC 等大量新概念,同时需要维护 etcd、kube-apiserver、kube-scheduler 等组件。
Docker Compose 滚动更新策略
Docker Compose 滚动更新策略
什么是滚动更新
滚动更新(Rolling Update)是指逐步用新版本替换旧版本实例的过程。与一次性全部替换相比,滚动更新的优势是:
- 零停机:始终有实例在提供服务
- 风险可控:先更新少量实例观察效果
- 快速回滚:发现问题可以立即回退
Compose 中的滚动更新配置
update_config 配置
services:
web:
image: registry.example.com/myapp:${TAG:-latest}
deploy:
replicas: 5
update_config:
parallelism: 1 # 同时更新 1 个实例
delay: 10s # 每个批次间隔 10 秒
failure_action: pause # 失败时暂停更新
monitor: 30s # 监控新实例 30 秒
max_failure_ratio: 0.2 # 允许 20% 的实例更新失败
order: start-first # 先启动新实例,再停止旧实例
参数详解
| 参数 | 说明 | 默认值 | 推荐值 |
|---|---|---|---|
| parallelism | 同时更新的实例数 | 1 | 1-3(取决于总副本数) |
| delay | 批次间等待时间 | 0s | 10-30s |
| failure_action | 失败时的处理策略 | pause | pause(生产) |
| monitor | 检查新实例健康的时间 | 0s | 30-60s |
| max_failure_ratio | 允许的失败比例 | 0 | 0.2-0.3 |
| order | 更新顺序 | stop-first | start-first |
使用限制
重要:
deploy.update_config在标准的docker compose up中不生效。它主要用于 Docker Stack(Swarm 模式)。
使用 Docker Stack 进行滚动更新
# 先部署为 Stack
docker stack deploy -c docker-compose.yml myapp
# 更新镜像版本(手动触发滚动更新)
docker service update --image registry.example.com/myapp:v2 myapp_web
# 更新环境变量
docker service update --env-add NODE_ENV=production myapp_web
# 查看更新状态
docker service ps myapp_web
使用 docker compose 实现手工滚动更新
如果你使用 docker compose 而非 docker stack,可以手动模拟滚动更新:
方案一:逐个更新
#!/bin/bash
# manual-rolling-update.sh
SERVICE="web"
NEW_IMAGE="myapp:v2"
INSTANCES=$(docker compose ps --services | grep $SERVICE)
echo "开始滚动更新..."
# 获取当前运行中的容器列表
for container in $(docker compose ps $SERVICE --format "{{.Name}}" | sort); do
echo "更新容器: $container"
# 使用新镜像创建新容器
docker compose rm -fs $container
docker compose up -d --no-recreate $SERVICE
# 等待新容器就绪
sleep 15
# 检查健康状态
if docker compose ps $SERVICE | grep -q "healthy"; then
echo "✅ 容器更新成功"
else
echo "❌ 容器更新失败,回滚..."
# 回滚逻辑
break
fi
done
echo "滚动更新完成"
方案二:蓝绿部署
# docker-compose.yml
services:
web-blue:
image: myapp:v1
ports:
- "3001:3000"
networks:
- app-net
web-green:
image: myapp:v2
ports:
- "3002:3000"
networks:
- app-net
结合负载均衡器切换流量:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- app-net
回滚策略
自动回滚
services:
web:
image: myapp:latest
deploy:
update_config:
failure_action: rollback # 失败时自动回滚
monitor: 60s
order: start-first
rollback_config:
parallelism: 1
delay: 10s
monitor: 30s
手动回滚
# 回滚到上一个版本
docker service rollback myapp_web
# 指定回滚到特定版本
docker service update --image myapp:v1 myapp_web
零停机部署完整示例
# docker-compose.yml
services:
# 反向代理(始终只有一个实例)
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
# 无状态应用(可滚动更新)
web:
image: ${REGISTRY}/myapp:${TAG:-latest}
expose:
- "3000"
environment:
NODE_ENV: ${NODE_ENV:-production}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
failure_action: pause
monitor: 30s
order: start-first
max_failure_ratio: 0.3
rollback_config:
parallelism: 1
delay: 5s
monitor: 30s
对应的 Nginx 配置:
upstream web_backend {
server web:3000;
}
server {
listen 80;
location / {
proxy_pass http://web_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
实际部署流程
# 1. 初始部署
docker stack deploy -c docker-compose.yml myapp
# 2. 推送新版本
docker build -t myapp:v2 .
docker push registry.example.com/myapp:v2
# 3. 触发滚动更新
docker service update \
--image registry.example.com/myapp:v2 \
--update-parallelism 1 \
--update-delay 10s \
myapp_web
# 4. 监控进度
watch -n 2 docker service ps myapp_web
# 5. 确认健康状态
docker service inspect myapp_web --format '{{json .UpdateStatus}}'
面试要点
Q:滚动更新和蓝绿部署的区别?
A:滚动更新是逐步替换实例(新旧共存),蓝绿部署同时运行两套完整环境,流量瞬间切换。滚动更新资源消耗更低,蓝绿部署回滚更快。
Q:更新顺序 start-first 和 stop-first 的区别?
A:start-first 先启动新实例再停止旧实例(需要额外资源),stop-first 先停止旧实例再启动新实例(可能短时间无可用实例)。零停机场景应使用 start-first。
Q:滚动更新失败时的故障处理策略有哪些?
A:三种策略:pause(暂停更新,等待人工处理)、continue(忽略失败继续更新)、rollback(自动回滚到上一个版本)。
多 Compose 文件实现环境隔离
多 Compose 文件实现环境隔离
为什么需要多 Compose 文件
在实际开发中,不同环境(开发、测试、预发布、生产)的配置差异很大:
| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
| 端口映射 | 3000:3000 | 随机端口 |
| 卷挂载 | 代码热更新 | 只读、无代码目录 |
| 日志级别 | debug | warn |
| 资源限制 | 无 | 严格限制 |
| 环境变量 | 测试凭据 | 生产凭据 |
| 额外服务 | 管理后台、调试工具 | 无 |
通过多个 Compose 文件叠加使用,优雅分离这些差异。
Compose 文件叠加机制
Docker Compose 支持同时指定多个 Compose 文件,后面的文件会覆盖前面的:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
如果出现配置冲突,后加载的文件优先级更高。
典型的文件拆分方案
project/
├── docker-compose.yml # 基础配置(所有环境共享)
├── docker-compose.override.yml # 开发环境(默认自动加载)
├── docker-compose.prod.yml # 生产环境
├── docker-compose.staging.yml # 预发布环境
├── docker-compose.test.yml # 测试环境
├── .env # 开发环境变量
├── .env.production # 生产环境变量
└── .env.staging # 预发布环境变量
基础配置(所有环境共享)
# docker-compose.yml
services:
web:
image: nginx:alpine
depends_on:
- app
app:
build: .
image: myapp:${TAG:-latest}
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DB_NAME:-myapp}
POSTGRES_USER: ${DB_USER:-postgres}
volumes:
- pg-data:/var/lib/postgresql/data
volumes:
pg-data:
开发环境配置(override 文件)
# docker-compose.override.yml
# 开发环境特有的配置
# 注意:这个文件会被 docker compose up 自动加载
services:
web:
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app # 代码热更新
- /app/node_modules # 排除 node_modules
ports:
- "3000:3000"
environment:
NODE_ENV: development
DEBUG: "true"
LOG_LEVEL: debug
db:
ports:
- "5432:5432" # 暴露数据库端口方便调试
environment:
POSTGRES_PASSWORD: devpassword
# 开发环境特有的服务
adminer:
image: adminer
ports:
- "8081:8080"
depends_on:
- db
生产环境配置
# docker-compose.prod.yml
services:
web:
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
restart: always
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
app:
image: registry.example.com/myapp:${TAG}
environment:
NODE_ENV: production
LOG_LEVEL: warn
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
db:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
cpus: "2.0"
memory: 4G
secrets:
db_password:
file: ./secrets/db_password.txt
测试环境配置
# docker-compose.test.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.test
environment:
NODE_ENV: test
CI: "true"
volumes:
- .:/app
command: ["npm", "test"]
# 测试专用的内存数据库
db:
image: postgres:15-alpine
tmpfs:
- /var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: testpassword
使用方式
开发环境
# 自动加载 docker-compose.override.yml
docker compose up -d
# 加载顺序:docker-compose.yml → docker-compose.override.yml
# 或显式指定
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
生产环境
# 不使用 override 文件
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# 同时指定环境变量文件
docker compose --env-file .env.production \
-f docker-compose.yml \
-f docker-compose.prod.yml \
up -d
预发布环境
docker compose --env-file .env.staging \
-f docker-compose.yml \
-f docker-compose.staging.yml \
up -d
CI/CD 测试
docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm app
覆盖规则详解
完全覆盖
# docker-compose.yml
services:
web:
image: nginx:1.20
ports:
- "80:80"
# docker-compose.prod.yml 覆盖
services:
web:
image: nginx:1.25 # 覆盖镜像版本
ports:
- "80:80"
- "443:443" # 新增端口
数组合并
# 数组类型会合并而非覆盖
# docker-compose.yml
services:
app:
environment:
- NODE_ENV=development
- DEBUG=true
# docker-compose.prod.yml
services:
app:
environment:
- NODE_ENV=production # 覆盖 NODE_ENV
- SECRET_KEY=xxx # 新增
# 最终结果
# NODE_ENV=production
# DEBUG=true ← 保留了
# SECRET_KEY=xxx ← 新增
覆盖规则总结
| 配置类型 | 覆盖行为 |
|---|---|
| 标量(字符串、数字) | 完全覆盖 |
| map(如 environment) | 合并,相同 key 覆盖 |
| 列表(如 volumes) | 合并,避免冲突 |
| services 层级 | 合并,按 service name |
最佳实践
- 基础文件不做环境假设:使用变量代替硬编码值
- override 文件提交到 Git:方便新成员快速上手开发环境
- 生产文件不提交敏感信息:生产凭据通过 .env 文件或密钥管理服务注入
- 限制文件数量:通常 2-3 个文件足够,过多会导致配置难以追踪
- 使用环境变量文件:避免在 Compose 文件中写死环境差异
面试常考
Q:docker-compose.override.yml 为什么会被自动加载?
A:这是 Compose 的设计约定。执行 docker compose up 时,默认加载同级目录下的 docker-compose.override.yml。可以通过 --no-ansi 禁用,或用 -f 手动控制。
Q:多 Compose 文件环境中,数组和 map 的覆盖规则是什么?
A:map(如 environment)合并并覆盖相同 key。数组(如 depends_on)合并去重。标量完全覆盖。
Q:生产环境要避免使用 override 文件吗?
A:是的。生产环境应该显式指定 Compose 文件:docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d。避免 override 文件被意外加载。
Docker Compose 常用命令
Docker Compose 常用命令
命令概览
| 命令 | 用途 | 频率 |
|---|---|---|
up |
创建并启动服务 | ⭐⭐⭐⭐⭐ |
down |
停止并删除服务 | ⭐⭐⭐⭐⭐ |
ps |
查看服务状态 | ⭐⭐⭐⭐ |
logs |
查看日志 | ⭐⭐⭐⭐ |
exec |
在运行中的容器执行命令 | ⭐⭐⭐⭐ |
build |
构建或重新构建服务 | ⭐⭐⭐ |
pull |
拉取服务镜像 | ⭐⭐⭐ |
restart |
重启服务 | ⭐⭐⭐ |
start/stop |
启动/停止服务 | ⭐⭐⭐ |
run |
运行一次性命令 | ⭐⭐⭐ |
top |
查看运行中的进程 | ⭐⭐ |
images |
列出使用的镜像 | ⭐⭐ |
config |
验证并查看 Compose 配置 | ⭐⭐ |
scale |
设置服务实例数 | ⭐⭐ |
port |
查看端口映射 | ⭐ |
详细命令说明
up – 创建并启动服务
# 基本用法
docker compose up -d
# 构建镜像后启动
docker compose up -d --build
# 强制重新创建容器
docker compose up -d --force-recreate
# 不启动依赖的服务
docker compose up -d --no-deps web
# 设置服务副本数
docker compose up -d --scale web=3 --scale worker=2
# 指定 Compose 文件
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
down – 停止并删除服务
# 停止容器并删除网络(默认保留卷)
docker compose down
# 删除所有内容(包括卷)
docker compose down -v
# 删除镜像
docker compose down --rmi all
# 删除 orphan 容器
docker compose down --remove-orphans
# 保留卷但删除其他所有资源
docker compose down --timeout 30
ps – 查看服务状态
# 查看所有服务的状态
docker compose ps
# 查看所有服务(包括停止的)
docker compose ps -a
# 查看特定服务
docker compose ps web
# 显示详细信息
docker compose ps --services
docker compose ps --filter status=running
logs – 查看日志
# 查看所有服务的日志
docker compose logs
# 跟踪日志输出
docker compose logs -f
# 查看特定服务日志
docker compose logs -f web
# 只看最后 N 行
docker compose logs --tail=100
# 带时间戳
docker compose logs -t
# 只看特定时间段的日志
docker compose logs --since=2024-01-01 --until=2024-01-02
exec – 执行命令
# 在运行的容器中执行命令
docker compose exec web sh
# 以特定用户执行
docker compose exec -u root web sh
# 设置工作目录
docker compose exec -w /app web npm test
# 设置环境变量
docker compose exec -e DEBUG=true web node app.js
run – 运行一次性命令
与 exec 不同,run 会启动一个新的容器实例:
# 运行一次性命令
docker compose run app npm test
# 运行并删除容器
docker compose run --rm app npm install
# 覆盖端口映射
docker compose run --service-ports web
# 不启动依赖的服务
docker compose run --no-deps web sh
# 指定工作目录
docker compose run -w /app web bash
build – 构建镜像
# 构建所有服务
docker compose build
# 构建特定服务
docker compose build web
# 构建时不使用缓存
docker compose build --no-cache
# 构建并设置参数
docker compose build --build-arg VERSION=1.0.0 web
# 并行构建
docker compose build --parallel
config – 验证配置
# 验证 Compose 文件语法
docker compose config
# 查看完整的配置(变量已替换)
docker compose config
# 保存合并后的配置到文件
docker compose config > merged-config.yml
# 查看服务列表
docker compose config --services
# 查看卷列表
docker compose config --volumes
# 安静模式(仅返回状态码)
docker compose config -q
组合命令技巧
更新服务
# 完全更新流程
docker compose pull # 拉取最新镜像
docker compose build # 重新构建
docker compose up -d # 启动新容器
docker compose logs -f # 监控日志
重启策略
# 优雅重启
docker compose restart web --timeout 30
# 滚动重启(逐个)
for service in web worker queue; do
docker compose restart $service
sleep 5
done
清理和重置
# 完全重置开发环境
docker compose down -v # 删除所有
docker compose build # 重新构建
docker compose up -d # 重新启动
# 清理未使用的资源
docker compose down --remove-orphans
docker system prune -f
常用快捷键和技巧
# 在日志中搜索
docker compose logs -f | grep ERROR
# 组合查看
docker compose ps && docker compose top
# 压缩日志查看
docker compose logs --tail=50 -f web app
# 导出日志到文件
docker compose logs > all-logs.txt
调试和排错命令
# 查看端口映射
docker compose port web 80
# 查看容器 IP
docker compose inspect web | grep IPAddress
# 查看资源使用
docker compose top web
# 检查网络
docker compose run --rm alpine ping db
面试要点
Q:docker compose up 和 docker compose start 有什么区别?
A:up 会创建并启动容器(如果不存在则创建,否则重建)。start 只启动已存在的停止状态的容器。up 更常用。
Q:docker compose down 和 docker compose stop 的区别?
A:stop 只停止容器,保留容器和网络等资源。down 会停止并删除容器、网络等资源(默认保留卷)。生产环境中数据库的操作要谨慎使用 down。
Q:docker compose exec 和 docker compose run 的区别?
A:exec 在已运行的容器中执行命令。run 创建一个新的临时容器执行命令。exec 适合调试,run 适合运行一次性任务。
使用 scale 扩展服务实例
使用 scale 扩展服务实例
什么场景需要扩展实例
- 高并发访问:Web 服务需要多实例分担流量
- 异步处理:Worker 服务需要多实例加速任务处理
- 高可用:多实例保证单点故障不影响服务
- 资源隔离:不同类型任务分配不同数量 Worker
scale 的基本用法
传统 scale 命令
# docker-compose.yml
services:
web:
image: nginx
ports:
- "80:80"
worker:
image: myworker
# 将 web 扩展到 3 个实例
docker compose up -d --scale web=3
# 将 worker 扩展到 5 个实例
docker compose up -d --scale worker=5
# 同时扩展多个服务
docker compose up -d --scale web=3 --scale worker=5
Compose V2 的 deploy/replicas
services:
web:
image: nginx
deploy:
mode: replicated
replicas: 3
ports:
- "80:80"
注意:deploy.replicas 在 docker compose up 时不会自动生效,需要使用 docker stack deploy。
扩展后的容器命名
扩展后,容器按照 {项目}_{服务}_{序号} 格式命名:
docker compose up -d --scale web=3
# 容器列表
myproject_web_1
myproject_web_2
myproject_web_3
端口映射与扩展
services:
web:
image: nginx
ports:
- "80:80" # ❌ 扩展到多个实例时会冲突!
当扩展 web 实例时,端口映射会冲突。解决方法:
方法一:动态端口映射
services:
web:
image: nginx
ports:
- "80" # 只写容器端口,主机端口随机分配
docker compose up -d --scale web=3
# 输出:
# Creating myproject_web_1 ... done
# Creating myproject_web_2 ... done
# Creating myproject_web_3 ... done
# 查看端口
docker compose port web 80
# 0.0.0.0:32768
方法二:使用负载均衡器
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
web:
image: mywebapp
expose:
- "3000"
# 没有 ports 映射
nginx.conf 配置反向代理:
upstream web_backend {
server myproject_web_1:3000;
server myproject_web_2:3000;
server myproject_web_3:3000;
}
server {
listen 80;
location / {
proxy_pass http://web_backend;
}
}
方法三:使用 Traefik
services:
traefik:
image: traefik:v2.10
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
web:
image: mywebapp
labels:
- "traefik.http.routers.web.rule=Host(`example.com`)"
expose:
- "3000"
无状态设计的重要性
要成功扩展实例,服务必须无状态:
# ❌ 有状态服务(无法扩展)
services:
web:
image: myapp
volumes:
- ./uploads:/app/uploads # 文件存储在本地
# 扩展到多个实例后,文件不同步
# ✅ 无状态服务(可扩展)
services:
web:
image: myapp
volumes:
- shared-uploads:/app/uploads # 共享存储
volumes:
shared-uploads:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.100,rw
device: ":/exports/uploads"
scale 与资源管理
资源限制
services:
web:
image: nginx
deploy:
replicas: 3
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
在 Compose 中默认不生效
deploy配置在docker compose up模式下默认不生效。它主要用于 Docker Stack(Swarm 模式)。
在 docker compose up 中限制资源需要:
# 使用 --compatibility 标志(不完全支持)
docker compose --compatibility up -d
实际扩展策略
基于需求的扩展
# 开发环境:单实例
docker compose up -d
# 测试环境:中等规模
docker compose up -d --scale web=2 --scale worker=2
# 压力测试:大规模
docker compose up -d --scale web=10 --scale worker=20
监控驱动的扩展
# 根据 CPU 使用率动态调整
while true; do
cpu=$(docker stats --no-stream --format "{{.CPUPerc}}" myproject_web_1 | sed 's/%//')
if (( $(echo "$cpu > 80" | bc -l) )); then
docker compose up -d --scale web=$(($(docker compose ps web | wc -l) + 1))
fi
sleep 60
done
面试常考
Q:docker compose scale 和 Kubernetes 的自动扩缩容有什么区别?
A:Compose scale 是手动操作,需要人工判断和触发。Kubernetes 的 HPA(Horizontal Pod Autoscaler)可以根据 CPU/内存等指标自动调整副本数,更加智能化。
Q:扩展服务实例时端口冲突怎么解决?
A:使用动态端口映射(只指定容器端口)、通过负载均衡器(如 Nginx、Traefik)分发流量、或者使用内部网络 + expose。
Q:有状态服务(如数据库)可以用 scale 吗?
A:一般情况下不能直接扩展数据库实例。数据库扩展需要主从复制或分片集群方案,不能简单用 scale 命令。scale 主要适用于无状态服务。
Compose 中的端口映射和卷挂载
Compose 中的端口映射和卷挂载
端口映射(Port Mapping)
基本语法
services:
web:
image: nginx
ports:
- "80:80" # HOST:CONTAINER
- "443:443"
端口映射的多种写法
services:
web:
image: nginx
ports:
# 长语法(推荐,更清晰)
- target: 80
published: 8080
protocol: tcp
mode: host
# 短语法
- "8080:80" # 指定主机端口
- "80" # 随机分配主机端口
- "0.0.0.0:8080:80" # 绑定特定 IP
- "127.0.0.1:8080:80" # 仅本地访问
- "8080:80/udp" # UDP 协议
- "3000-3005:3000-3005" # 端口范围
- "80-85:80-85/tcp" # 带协议的端口范围
使用变量
services:
web:
image: nginx
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
# .env
HTTP_PORT=8080
HTTPS_PORT=8443
端口暴露(expose)
expose 只在 Compose 内部网络中暴露端口,不对宿主机开放:
services:
app:
image: myapp
expose:
- "3000" # 仅内部网络可访问
- "4000-4005" # 端口范围
# ports:
# - "8080:3000" # 如果需要外部访问才用 ports
卷挂载(Volume Mounting)
基本语法
services:
db:
image: postgres:15
volumes:
- pg-data:/var/lib/postgresql/data # 命名卷
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # bind mount
- /data/backups:/backups # 宿主机绝对路径
长语法(推荐)
--mount 风格的长语法更清晰,在 Compose 3.2+ 中可用:
services:
web:
image: nginx
volumes:
- type: volume
source: html-data
target: /usr/share/nginx/html
read_only: true
volume:
nocopy: true
- type: bind
source: ./nginx.conf
target: /etc/nginx/nginx.conf
read_only: true
- type: tmpfs
target: /cache
tmpfs:
size: 100000000 # 100MB
挂载卷的常见用法
services:
# 开发环境:代码热更新
app:
build: .
volumes:
- .:/app # 整个项目挂载
- /app/node_modules # 排除 node_modules(使用容器内的)
# 配置文件挂载(只读)
web:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
# 日志持久化
api:
image: myapi
volumes:
- logs:/var/log/app
# socket 文件共享
docker-client:
image: docker:cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
绑定挂载的变量
services:
app:
image: myapp
volumes:
- ${SRC_DIR:-./src}:/app/src:${VOLUME_MODE:-rw}
- ${CONFIG_FILE:-./config/default.json}:/app/config.json:ro
综合示例
services:
# Nginx:反向代理 + 静态资源
nginx:
image: nginx:alpine
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- type: bind
source: ./nginx.conf
target: /etc/nginx/nginx.conf
read_only: true
- type: volume
source: static-assets
target: /usr/share/nginx/html
read_only: true
- type: bind
source: ./ssl
target: /etc/nginx/ssl
read_only: true
depends_on:
- app
# 应用服务
app:
build: .
ports:
- "3000" # 随机端口(仅用于调试)
expose:
- "3000" # 内部网络访问
volumes:
- type: bind
source: ./src
target: /app/src
- type: volume
source: uploads
target: /app/uploads
- type: volume
source: app-logs
target: /app/logs
environment:
NODE_ENV: ${NODE_ENV:-development}
# 数据库
db:
image: postgres:15
ports:
- "127.0.0.1:5432:5432" # 仅本地访问
volumes:
- type: volume
source: pg-data
target: /var/lib/postgresql/data
- type: bind
source: ./init.sql
target: /docker-entrypoint-initdb.d/init.sql
read_only: true
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_PASSWORD: ${DB_PASS:-devpassword}
# Redis 缓存
redis:
image: redis:7-alpine
ports:
- "6379" # 随机端口
volumes:
- type: volume
source: redis-data
target: /data
command: ["redis-server", "--appendonly", "yes"]
volumes:
static-assets:
uploads:
app-logs:
pg-data:
driver: local
redis-data:
常见问题
端口冲突
# 错误:端口已被占用
Error response from daemon: driver failed programming external connectivity...
解决方案:
– 使用变量配置端口
– 用 -p 随机端口或换端口
卷权限问题
# 挂载的目录权限不对
# 容器内以非 root 用户运行,但挂载的卷中文件归 root 所有
解决方案:
services:
app:
image: node:18
user: "1000:1000" # 指定与宿主机相同的 UID
volumes:
- .:/app
只读模式
volumes:
- type: bind
source: ./config
target: /app/config
read_only: true # 容器无法修改,提高安全性
面试要点
Q:ports 和 expose 的区别?
A:ports 将容器端口映射到宿主机,外部可以访问。expose 仅在 Compose 内部网络中暴露端口,外部不能访问。expose 更像文档声明,告诉其他服务这个容器有某个端口。
Q:卷挂载时 :ro 是做什么的?
A::ro 表示只读挂载(read only),容器内无法修改挂载的文件。用于配置文件等不需要容器修改的挂载。
Q:Bind Mount 和 Volume 在 Compose 中的写法有何不同?
A:Bind Mount 使用宿主机路径(./data:/app/data),Volume 使用卷名(volume-name:/app/data)。使用长语法时通过 type: bind 或 type: volume 区分。
depends_on 依赖管理
depends_on 依赖管理
为什么需要 depends_on
在多服务应用中,服务之间通常有启动依赖关系:
- 应用服务需要在数据库启动后才能正常连接
- Web 服务器需要在应用服务启动后才能配置反向代理
- 迁移任务需要在数据库就绪后才能执行
depends_on 的作用就是告诉 Compose 服务之间的启动和关闭顺序。
基本用法
简单依赖
services:
web:
image: nginx
depends_on:
- app
app:
image: myapp:latest
depends_on:
- db
db:
image: postgres:15
启动顺序:db → app → web
关闭顺序:web → app → db
多层依赖
services:
api:
build: .
depends_on:
- redis
- db
- migration
migration:
image: myapp-migrate
depends_on:
- db
redis:
image: redis:alpine
db:
image: postgres:15
Compose 会解析依赖树,按拓扑排序的顺序启动服务。
depends_on 的限制
最基本的 depends_on 只能控制启动顺序,无法保证服务已就绪。
services:
app:
image: myapp
depends_on:
- db
# 问题:db 容器启动后,MySQL 进程可能还没准备好
上面的配置中,db 容器启动了(Docker 认为状态是 running),但 MySQL 可能还在初始化。app 尝试连接数据库时仍然会失败。
condition 配置(Compose V2+)
使用 condition 可以设置更精确的就绪条件:
services:
app:
build: .
depends_on:
db:
condition: service_healthy # 等待健康检查通过
redis:
condition: service_started # 默认行为
migration:
condition: service_completed_successfully # 等待成功完成
condition 类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
service_started |
默认,容器启动即为就绪 | 对就绪要求不高的服务 |
service_healthy |
等待 healthcheck 通过 | 数据库、中间件 |
service_completed_successfully |
等待服务退出(exit 0) | 一次性任务 |
完整示例:等待就绪
services:
web:
build: .
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
migrations:
condition: service_completed_successfully
migrations:
build:
context: .
dockerfile: Dockerfile.migrations
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
environment:
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
depends_on 对启动速度的影响
services:
web:
depends_on:
- api
api:
depends_on:
- db
- cache
- queue
# 这三个服务可以并行启动
db:
cache:
queue:
启动流程:
1. db、cache、queue 同时启动
2. api 在 db、cache、queue 全部启动后启动
3. web 在 api 启动后启动
depends_on 在关闭时的行为
Compose 严格按照依赖的逆序关闭服务:
# 执行 docker compose down
# 1. 停止 web
# 2. 停止 app
# 3. 停止 migration
# 4. 停止 db
这在数据库场景中非常重要——确保应用先断开连接,然后再关闭数据库。
常见问题和注意事项
循环依赖
# 错误示例:循环依赖
services:
service-a:
depends_on:
- service-b
service-b:
depends_on:
- service-a
Compose 会报错并拒绝启动。设计服务时应避免循环依赖。
等待自定义服务
services:
app:
image: myapp
depends_on:
init-db:
condition: service_completed_successfully
# 初始化数据库的一次性任务
init-db:
image: myapp
command: ["npm", "run", "db:init"]
environment:
- DB_HOST=db
depends_on:
db:
condition: service_healthy
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
使用 wait-for-it 脚本
如果不使用 Compose V2 的 condition 功能,可以结合 wait-for-it 脚本:
services:
app:
image: myapp
depends_on:
- db
command: ["./wait-for-it.sh", "db:5432", "--", "npm", "start"]
最佳实践
- 总是使用 healthcheck:即使有 depends_on,也要为关键服务配置 healthcheck
- 优先使用 condition: service_healthy:而不是依赖默认的启动顺序
- 单独定义初始化任务:使用
service_completed_successfully模式 - 避免深层依赖:超过 3 层的依赖链会让启动时间不可控
- 考虑使用 init containers:在 Kubernetes 中可用 Init Container 模式
面试常问
Q:depends_on 能保证服务真正可用吗?
A:基本版不能。仅保证容器状态为 running。需要结合 healthcheck 和 condition: service_healthy 才能确保服务真正就绪。
Q:什么是循环依赖?如何解决?
A:A 依赖 B、B 依赖 A。解决方法:提取公共组件、使用服务发现、使用消息队列解耦。
Q:docker compose 启动和关闭顺序分别是怎样的?
A:启动时按依赖拓扑正序(先启动被依赖的),关闭时按逆序。同一个依赖级别的服务可以并行启停。
Docker Compose 是什么
Docker Compose 是什么
从问题出发
如果你需要部署一个 Web 应用,通常涉及多个组件:
- 前端:Nginx 静态资源服务器
- 后端:Node.js/Python/Java 应用
- 数据库:MySQL/PostgreSQL
- 缓存:Redis
- 队列:RabbitMQ
手动用 docker run 启动每个容器需要写大量命令行参数。如果涉及 10 个以上容器,管理难度呈指数级上升。
Docker Compose 就是为了解决这个问题而生的。
Docker Compose 的定义
Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过一个 YAML 文件,你可以声明式地描述整个应用栈,然后一行命令启动所有服务。
# docker-compose.yml
services:
web:
image: nginx:alpine
ports:
- "80:80"
app:
build: .
environment:
- DB_HOST=db
db:
image: postgres:15
volumes:
- pg-data:/var/lib/postgresql/data
volumes:
pg-data:
启动全部服务只需:docker compose up -d
核心概念
Project(项目)
Compose 将一组相关的服务组织为一个项目。每个项目有唯一的名称,默认以包含 Compose 文件的目录名命名。
# 在 myapp 目录中运行
docker compose up -d
# 项目名为 myapp
# 容器名为 myapp_web_1, myapp_app_1, myapp_db_1
Service(服务)
服务是 Compose 的基本调度单元,每个服务对应一个或多个容器实例。一个服务由镜像、端口、环境变量、卷等配置定义。
Container(容器)
由 Compose 创建的具体容器实例。命名格式为:{project}_{service}_{replica}
主要功能
1. 声明式配置
将所有容器的配置集中在一个 YAML 文件中,支持版本控制和团队协作。
2. 一键启停
# 启动所有服务
docker compose up -d
# 停止所有服务
docker compose down
3. 自动网络管理
Compose 自动为项目创建隔离的网络,服务名可直接作为主机名用于服务发现。
4. 环境隔离
同一台主机上可以运行多个 Compose 项目,它们互不干扰。这在开发/测试/生产环境隔离中非常有用。
5. 扩展能力
使用 scale 或 replicas 轻松扩展某个服务的实例数。
Compose V1 vs V2
| 对比项 | Compose V1 | Compose V2 |
|---|---|---|
| 命令 | docker-compose |
docker compose |
| 实现 | Python 独立程序 | Go 插件,集成到 Docker CLI |
| 性能 | 较慢 | 更快 |
| 维护 | 已停止更新 | 活跃维护 |
| 推荐 | ❌ 不推荐 | ✅ 强烈推荐 |
从 2023 年起,建议总是使用 docker compose(不带横线)。
典型的使用场景
开发环境
services:
app:
build: .
volumes:
- .:/app # 代码热更新
ports:
- "3000:3000"
db:
image: postgres:15
environment:
POSTGRES_DB: dev
CI/CD 测试
services:
test:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
- db
- redis
db:
image: postgres:15-alpine
redis:
image: redis:alpine
# CI 中运行测试
docker compose run --rm test pytest
一体化演示环境
services:
prometheus:
image: prom/prometheus
grafana:
image: grafana/grafana
node-exporter:
image: prom/node-exporter
Compose 的局限性
- 单机部署:Docker Compose 只能在一台主机上运行
- 无自动扩缩容:不支持根据负载自动调整实例数
- 无自愈:不会自动恢复失败的容器
- 不适合大规模集群:超过几十个服务管理会变得复杂
当需要多机部署、自动扩缩容、服务发现时,应考虑 Kubernetes 或 Docker Swarm。
为什么面试常问 Compose
- 它是 Docker 生态中最重要的工具之一
- 考察是否了解多容器编排的概念
- 考察 YAML 文件组织能力
- 考察从开发到部署的完整链路理解
总结
Docker Compose 是开发阶段和中小规模部署场景中不可或缺的工具。它通过一个 YAML 文件定义完整的应用栈,让多容器应用的启动、停止、扩展变得极其简单。学会 Compose,是掌握 Docker 应用编排的第一步。
Compose 文件的基本结构
Compose 文件的基本结构
YAML 基础
Docker Compose 使用 YAML 格式定义配置文件。默认文件名是 docker-compose.yml(推荐)或 docker-compose.yaml。
YAML 基础规则:
# 缩进使用空格(不用 Tab)
# 键值对用冒号+空格
# 列表用短横线+空格
# 注释用 #
key: value
list:
- item1
- item2
Compose 文件的顶层结构
一个标准的 Compose 文件由以下几个顶级区块组成:
version: "3.9" # (可选)Compose 文件格式版本
services: # 服务定义(核心)
web:
# 服务配置...
networks: # 网络定义
frontend:
backend:
volumes: # 数据卷定义
data:
logs:
configs: # (Swarm 模式)配置
secrets: # (Swarm 模式)密钥
version 字段
version: "3.9"
注意:从 Compose V2 开始,version 字段变为可选。官方建议新项目中省略 version 字段。
services – 核心部分
services 是 Compose 文件中最重要的部分,定义了所有需要运行的容器配置。
networks – 网络定义
定义服务之间如何互连。不定义时,Compose 会创建一个默认网络。
volumes – 数据卷定义
定义命名的数据卷,供 services 中的各个服务挂载使用。
services 中的常见配置项
services:
web:
# --- 镜像相关 ---
image: nginx:alpine # 使用已有镜像
build: ./app # 从 Dockerfile 构建
build:
context: ./app
dockerfile: Dockerfile.prod
args:
BUILD_VERSION: 1.0
# --- 端口映射 ---
ports:
- "80:80" # HOST:CONTAINER
- "443:443"
- "3000-3005:3000-3005" # 端口范围
# --- 环境变量 ---
environment:
- NODE_ENV=production
- DB_HOST=db
env_file:
- ./config/env.common
- ./config/env.production
# --- 数据卷 ---
volumes:
- app-data:/var/www/html # 命名卷
- ./src:/app/src # bind mount
- ./config.json:/app/config.json:ro
# --- 网络 ---
networks:
- frontend
- backend
# --- 依赖 ---
depends_on:
- db
- redis
# --- 健康检查 ---
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
# --- 资源限制 ---
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
# --- 其他常用配置 ---
container_name: my-web # 自定义容器名
restart: always # 重启策略
command: ["nginx", "-g", "daemon off;"] # 覆盖 CMD
entrypoint: [] # 覆盖 ENTRYPOINT
user: "1000:1000" # 运行用户
working_dir: /app # 工作目录
labels:
- "com.example.description=My web app"
完整示例
services:
# Web 服务
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- static-files:/usr/share/nginx/html
networks:
- frontend
depends_on:
- app
restart: unless-stopped
# 应用服务
app:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DB_URL=postgresql://db:5432/myapp
- REDIS_URL=redis://cache:6379
env_file:
- .env.production
volumes:
- app-logs:/var/log/app
networks:
- frontend
- backend
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
# 数据库
db:
image: postgres:15-alpine
volumes:
- pg-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
restart: always
# Redis 缓存
cache:
image: redis:7-alpine
volumes:
- cache-data:/data
networks:
- backend
restart: always
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 禁止外部访问
volumes:
pg-data:
driver: local
static-files:
app-logs:
cache-data:
secrets:
db_password:
file: ./secrets/db_password.txt
文件版本兼容性
| Compose 版本 | Docker Engine 版本 | 主要特性 |
|---|---|---|
| 3.9 | 20.10.0+ | 最新稳定版 |
| 3.8 | 19.03.0+ | GPU 支持 |
| 3.7 | 18.06.0+ | Deploy 增强 |
| 3.5 | 17.12.0+ | Compose 文件补丁 |
推荐使用 3.9 或直接省略 version 字段。
最佳实践
- 保持文件简洁:每个服务只定义必要的配置
- 使用 env_file:环境变量分离到单独文件
- 合理使用 depends_on:明确启动顺序
- 为所有命名卷和网络显式声明:增强可读性
- healthcheck 必不可少:确保依赖服务真正就绪
- 使用 .env 文件:通过环境变量实现不同环境的配置切换
面试常问
Q:version 字段的重要性?
A:早期版本必须指定。Compose V2 开始变为可选,如果不写则使用最新特性。但为了显式声明格式兼容性,建议写上使用的版本号。
Q:services、networks、volumes 三个顶级配置的关系?
A:services 定义容器,通过 networks 连接网络(实现服务发现和隔离),通过 volumes 持久化和共享数据。它们共同构成了应用的完整基础设施。
端口映射影响防火墙
端口映射影响防火墙
面试题
Docker 的端口映射会绕过宿主机防火墙吗?如何正确配置防火墙与 Docker 端口映射的关系?
标准答案
Docker 的端口映射通过 iptables NAT 规则实现,通常情况下会绕过宿主机防火墙的 INPUT 链。这是因为 iptables 的包处理流程中,DNAT(端口映射)发生在 PREROUTING 链,早于 INPUT 链。
Docker 的 iptables 策略
# Docker 启动时自动修改 iptables 规则
# 查看 NAT 表中的 DOCKER 链
iptables -t nat -L DOCKER -n -v
# 查看 Filter 表中的 DOCKER 链
iptables -L DOCKER -n -v
# Chain DOCKER (1 references)
# target prot opt source destination
# ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:80
数据包流程分析
外部请求 → 宿主机 eth0:8080
↓
PREROUTING 链(NAT 表)
├── Docker 生成的 DNAT 规则:
│ 目标地址改为 172.17.0.2:80
│ 这是 FORWARD 的关键!
↓
路由决策(目标地址已变成容器 IP)
├── 目标不是本机 → 走 FORWARD 链
↓
FORWARD 链(Filter 表)
├── Docker 自动添加 ACCEPT 规则
└── 你的 INPUT 链规则不生效!
↓
POSTROUTING 链
↓
容器内服务
关键点: 数据包被 DNAT 后目标地址变为容器 IP,不再走 INPUT 链(INPUT 处理目标是宿主机本地的包),而是走 FORWARD 链。默认 INPUT 链规则不控制 FORWARD 链。
问题演示
# 场景:宿主机开启了 UFW 防火墙,只允许 22 端口
ufw default deny
ufw allow ssh
ufw enable
# 启动 Docker 容器映射到 8080 端口
docker run -d -p 8080:80 nginx
# 即使 UFW 没有放行 8080
# 外部仍然可以访问 8080!
curl http://<宿主机IP>:8080
# ✅ 能访问到容器内的 Nginx
解决方案
方案一:修改 Docker iptables 配置
# /etc/docker/daemon.json
{
"iptables": false # 禁止 Docker 自动管理 iptables
}
systemctl restart docker
# 注意:关闭 iptables 后需要手动配置所有规则
# 包括:端口映射、容器网络、容器间通信
# 副作用:容器无法访问外网且端口映射失效
缺点: 需要手动维护所有规则,复杂度高,不推荐。
方案二:在 Docker 前加反向代理
# 最推荐的方案
# 只暴露反向代理(Nginx 或 HAProxy)的端口
# 容器不直接暴露端口
# 方式:容器间通过自定义网络通信
docker network create proxy-net
docker run -d --name nginx-proxy \
--network proxy-net \
-p 80:80 \
-p 443:443 \
nginx:alpine
docker run -d --name myapp \
--network proxy-net \
myapp:latest
# 外部只能访问到 80/443,看不到具体应用的端口
方案三:在宿主机上通过 iptables 策略控制
# 在 DOCKER-USER 链中添加规则(推荐)
# DOCKER-USER 是 Docker 提供的自定义链,Docker 不会覆盖它
# 只允许特定 IP 访问 8080 端口
iptables -I DOCKER-USER -p tcp --dport 8080 ! -s 10.0.0.0/8 -j DROP
# 限制来源 IP
iptables -I DOCKER-USER -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT
iptables -I DOCKER-USER -p tcp --dport 8080 -j DROP
# DOCKER-USER 链的保存
# 安装 iptables-persistent
apt install iptables-persistent
netfilter-persistent save
最佳实践
# 生产环境的安全配置
# 1. 不要关闭 Docker 的 iptables
# 2. 使用 DOCKER-USER 链控制访问
# 3. 限制端口绑定 IP
# 4. 使用反向代理统一入口
# 完整的 DOCKER-USER 规则示例
iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 \
-s 10.0.0.0/8 -j ACCEPT
iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 \
-s 192.168.0.0/16 -j ACCEPT
iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -j DROP
# 绑定到内网 IP(减少暴露面)
docker run -d -p 192.168.1.100:8080:80 nginx
UFW 用户的特别说明
# UFW 默认无法控制 Docker 暴露的端口
# 需要在 UFW 配置中修改
# 方法:修改 /etc/default/ufw
# 将 DEFAULT_FORWARD_POLICY 改为 "ACCEPT"
# 或者添加针对 FORWARD 链的规则
# 更简单的方式:用 DOCKER-USER 链代替 UFW
面试高频问题
问:Docker 端口映射为什么能绕过 UFW?
因为 UFW 默认只控制 INPUT 链(入站到本机的包),而 Docker 的端口映射包经过 DNAT 后目标地址变成容器 IP,走的是 FORWARD 链。UFW 的 FORWARD 链策略默认是 ACCEPT,所以”漏”过去了。
问:DOCKER-USER 链和 DOCKER 链有什么区别?
DOCKER 链由 Docker 自动管理,每次容器启停都会清空重建。DOCKER-USER 链是 Docker 保留给用户的,Docker 不会修改它。所以用户的防火墙规则应该加在 DOCKER-USER 链。
问:关闭 iptables 有什么副作用?
容器无法访问外网、端口映射失效、容器间通信受限。除非你有完整的替代方案(如手动配置网络),否则不要关闭 Docker 的 iptables 管理。
总结
Docker 端口映射通过 FORWARD 链绕过宿主机 INPUT 链的防火墙规则(包括 UFW)。正确做法不是关掉 Docker 的 iptables,而是用 DOCKER-USER 链添加准入控制。生产环境推荐反向代理 + 内部网络策略的组合方案。
端口映射 -p 原理
端口映射 -p 原理
面试题
Docker 中的
-p端口映射是如何实现的?底层使用了什么技术?
标准答案
docker run -p 将宿主机的端口映射到容器的端口,使得外部可以通过宿主机 IP:端口访问容器内的服务。其底层基于 Linux 的 iptables DNAT(Destination NAT)技术。
基本用法
# 最简单的端口映射
docker run -d -p 8080:80 nginx
# 宿主机 8080 → 容器 80
# 指定 IP 绑定
docker run -d -p 127.0.0.1:8080:80 nginx
# 只监听本地,不对外开放
# 绑定随机端口
docker run -d -P nginx
# -P(大写)映射所有 EXPOSE 端口到宿主机随机端口
# 映射 UDP 端口
docker run -d -p 8080:80/udp nginx
# 多个端口映射
docker run -d \
-p 80:80 \
-p 443:443 \
-p 3306:3306 \
nginx
底层原理:iptables DNAT
# 执行端口映射后,Docker 会在 iptables 中添加 DNAT 规则
# 启动一个带端口映射的容器
docker run -d -p 8080:80 --name web nginx
# 查看 DNAT 规则
iptables -t nat -L DOCKER -n -v
# Chain DOCKER (2 references)
# pkts bytes target prot opt in out source destination
# 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0
# tcp dpt:8080 to:172.17.0.2:80
# 这条规则的意思是:
# 从非 docker0 接口进来的、访问宿主机 8080 端口的 TCP 数据包
# 目标地址改为 172.17.0.2:80(容器的 IP 和端口)
完整的数据包路径
# 请求:外部用户 → curl http://host_ip:8080
# 1. 数据包到达宿主机 eth0
# 源:client_ip:random_port
# 目标:host_ip:8080
# 2. iptables PREROUTING 链
# 命中 DOCKER 规则的 DNAT
# 目标改为 172.17.0.2:80
# 3. 路由决策
# 目标 172.17.0.2 → 通过 docker0
# 4. iptables FORWARD 链
# Docker 添加了 ACCEPT 规则允许转发
# 5. 数据包通过 docker0 → veth → 容器 eth0
# 容器 Nginx 收到请求
# 6. 响应返回
# Nginx 回复,源 172.17.0.2:80
# 经过 iptables 反向 SNAT
# 源改为 host_ip:8080
# 整个过程对容器和用户都是透明的
查看映射关系
# 方法 1:docker ps
docker ps --format "table {{.Names}}\t{{.Ports}}"
# NAMES PORTS
# web 0.0.0.0:8080->80/tcp
# 方法 2:docker inspect
docker inspect web --format '{{json .NetworkSettings.Ports}}' | jq
# {
# "80/tcp": [
# {
# "HostIp": "0.0.0.0",
# "HostPort": "8080"
# }
# ]
# }
# 方法 3:查看 iptables(最底层)
iptables -t nat -L DOCKER -n
# 方法 4:查看端口监听
ss -tlnp | grep docker
# LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=1234))
docker-proxy 的作用
# 除了 iptables 规则,Docker 还会启动一个 docker-proxy 进程
# 查看 docker-proxy
ps aux | grep docker-proxy
# root 1234 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 \
# -host-port 8080 -container-ip 172.17.0.2 -container-port 80
# docker-proxy 的作用:
# 1. 监听宿主机端口
# 2. 将流量转发到容器
# 3. 在 iptables 不可用或用户空间模式下兜底
# 注意:在某些 Linux 发行版中,docker-proxy 不是必需的
# iptables 规则就足以完成转发
# docker-proxy 主要是为了兼容性(如 macOS/Windows 上的 Docker Desktop)
常见端口映射场景
1. 多服务单端口
# 不同服务映射到不同宿主机端口
docker run -d -p 8080:80 --name web1 nginx
docker run -d -p 8081:80 --name web2 nginx
# 访问:
# host_ip:8080 → web1
# host_ip:8081 → web2
2. 同一个宿主机端口不能重复
# ❌ 错误:端口冲突
docker run -d -p 8080:80 nginx
docker run -d -p 8080:80 nginx
# docker: Error response from daemon: driver failed programming
# external connectivity on endpoint: Bind for 0.0.0.0:8080 failed:
# port is already allocated.
# ✅ 正确:使用不同端口
docker run -d -p 8081:80 --name web2 nginx
3. 随机端口
# 让 Docker 分配随机宿主机端口
docker run -d -p 80 --name web nginx
# 或者
docker run -d -P nginx
# 查看分配的随机端口
docker port web
# 80/tcp -> 0.0.0.0:32768
# 随机端口范围:默认 32768-60999
# 可通过内核参数修改
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768 60999
端口映射与防火墙
# Docker 的端口映射会绕过主机防火墙?
# 不一定,取决于 iptables 规则顺序
# Docker 默认在 iptables 的 DOCKER 链中添加规则
# 如果主机防火墙(如 ufw)在 DOCKER 链之前处理,可能会拦截
# 查看规则顺序
iptables -L FORWARD -n --line-numbers
# 如果 ufw 影响了 Docker 端口映射
# 可以配置 ufw 放行 Docker 端口
ufw allow 8080/tcp
端口映射的安全问题
# 1. 只绑定到本地
docker run -d -p 127.0.0.1:8080:80 nginx
# 只允许本机访问,外部无法访问
# 2. 仅内部网络访问
docker run -d -p 10.0.0.100:8080:80 nginx
# 只允许内网用户访问
# 3. 考虑使用反向代理
# Nginx → Docker 容器(不直接暴露容器端口)
docker run -d --network internal --name app my-app
# 通过反向代理访问
面试官追问
问:-p 8080:80 和 -p 127.0.0.1:8080:80 有什么不同?
答:前者监听 0.0.0.0:8080(所有网络接口包括外网),后者只监听 127.0.0.1:8080(仅本地可访问)。安全要求较高的场景应绑定到特定 IP。
问:iptables DNAT 和 docker-proxy 的关系?
答:两者都是 Docker 端口映射的实现方式。Linux 上用 iptables 就够了,docker-proxy 是兜底方案。在 Docker Desktop(macOS/Windows)上主要靠 docker-proxy,因为 iptables 在这些平台上不可用。可以通过配置 "userland-proxy": false 禁用 docker-proxy。
问:容器内的端口需要先 EXPOSE 才能映射吗?
答:不需要。-p 可以映射任何端口,即使 Dockerfile 中没有 EXPOSE。EXPOSE 仅作为文档和 -P(大写)使用,不会影响 -p 的行为。
总结
端口映射的核心是 iptables DNAT 规则:宿主机端口收到数据包后,通过 DNAT 将目标地址改写为容器 IP:端口,然后通过 docker0 网桥转发到容器。理解这一点,就能灵活应对各种端口映射相关的问题。安全方面要注意绑定到特定 IP 避免暴露端口到公网。


暂无评论内容