Docker Compose与编排

📌 本文由 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

最佳实践

  1. 基础文件不做环境假设:使用变量代替硬编码值
  2. override 文件提交到 Git:方便新成员快速上手开发环境
  3. 生产文件不提交敏感信息:生产凭据通过 .env 文件或密钥管理服务注入
  4. 限制文件数量:通常 2-3 个文件足够,过多会导致配置难以追踪
  5. 使用环境变量文件:避免在 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.replicasdocker 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: bindtype: 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

启动顺序:dbappweb
关闭顺序:webappdb

多层依赖

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"]

最佳实践

  1. 总是使用 healthcheck:即使有 depends_on,也要为关键服务配置 healthcheck
  2. 优先使用 condition: service_healthy:而不是依赖默认的启动顺序
  3. 单独定义初始化任务:使用 service_completed_successfully 模式
  4. 避免深层依赖:超过 3 层的依赖链会让启动时间不可控
  5. 考虑使用 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. 扩展能力

使用 scalereplicas 轻松扩展某个服务的实例数。

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 的局限性

  1. 单机部署:Docker Compose 只能在一台主机上运行
  2. 无自动扩缩容:不支持根据负载自动调整实例数
  3. 无自愈:不会自动恢复失败的容器
  4. 不适合大规模集群:超过几十个服务管理会变得复杂

当需要多机部署、自动扩缩容、服务发现时,应考虑 Kubernetes 或 Docker Swarm。

为什么面试常问 Compose

  1. 它是 Docker 生态中最重要的工具之一
  2. 考察是否了解多容器编排的概念
  3. 考察 YAML 文件组织能力
  4. 考察从开发到部署的完整链路理解

总结

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 字段。

最佳实践

  1. 保持文件简洁:每个服务只定义必要的配置
  2. 使用 env_file:环境变量分离到单独文件
  3. 合理使用 depends_on:明确启动顺序
  4. 为所有命名卷和网络显式声明:增强可读性
  5. healthcheck 必不可少:确保依赖服务真正就绪
  6. 使用 .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 中没有 EXPOSEEXPOSE 仅作为文档和 -P(大写)使用,不会影响 -p 的行为。

总结

端口映射的核心是 iptables DNAT 规则:宿主机端口收到数据包后,通过 DNAT 将目标地址改写为容器 IP:端口,然后通过 docker0 网桥转发到容器。理解这一点,就能灵活应对各种端口映射相关的问题。安全方面要注意绑定到特定 IP 避免暴露端口到公网。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容