Docker 高级指南

1. Docker高级指南 · 镜像构建高级技巧(/docker-gao-ji-zhi-nan/#%E9%95%9C%E5%83%8F%E6%9E%84%E5%BB%BA%E9%AB%98%E7%BA%A7%E6%8A%80%E5%B7%A7) 2. Docker高级指南 · Docker 安全加固(/docker-gao-ji-zhi-nan/#docker-%E5%AE%89%E5%85%A8%E5%8A%A0%E5%9B%BA) 3. Docker高级指南 · Docker Swarm 集群编排(/docker-gao-ji-zhi-nan/#do

分享

官方文档:https://docs.docker.com/
适用版本:Docker Engine 24+(2026-05-07 核实)

目录

  1. Docker高级指南 · 镜像构建高级技巧
  2. Docker高级指南 · Docker 安全加固
  3. Docker高级指南 · Docker Swarm 集群编排
  4. Docker高级指南 · 生产环境部署方案
  5. Docker高级指南 · CI/CD 集成
  6. Docker高级指南 · 容器内应用最佳实践
  7. Docker高级指南 · 性能调优
  8. Docker高级指南 · Docker 网络高级
  9. Docker高级指南 · 日常运维命令
  10. Docker高级指南 · 踩坑与最佳实践清单

镜像构建高级技巧

BuildKit 概述

BuildKit 是 Docker 官方的下一代构建引擎,相比传统构建引擎具有以下优势:并行构建阶段、高效缓存、秘密挂载、SSH 转发、跨平台构建支持。

启用 BuildKit

方式一:环境变量(临时生效)

DOCKER_BUILDKIT=1 docker build -t myapp .

方式二:Docker Desktop / dockerd 全局配置(永久生效)

编辑 /etc/docker/daemon.json

{
  "features": {
    "buildkit": true
  }
}

然后重启守护进程:

sudo systemctl restart docker

方式三:使用 docker buildx(BuildKit 的扩展命令行)

# 查看当前 builder
docker buildx ls

# 创建并使用新 builder(支持多平台)
docker buildx create --name mybuilder --use

# 查看 builder 详情
docker buildx inspect --bootstrap

RUN --mount 全类型详解

RUN --mount 是 BuildKit 提供的挂载机制,允许在构建步骤中挂载各类资源,而不会将这些资源写入镜像层。

type=cache(构建缓存)

将包管理器缓存目录持久化到宿主机,避免每次构建重新下载依赖。

# Node.js npm 缓存
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build
# Python pip 缓存
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
# apt 包缓存(Debian/Ubuntu)
FROM ubuntu:22.04
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y curl git

type=cache 参数说明:

参数 类型 默认值 说明
target string 必填 容器内挂载路径
id string target 路径 缓存标识符,相同 id 共享同一缓存
source string 宿主机源路径(不填则由 BuildKit 管理)
from string 从某个构建阶段的路径作为缓存源
sharing enum shared shared(多并发可读写)/ private(独占)/ locked(串行写)
readonly bool false 设为 true 则只读挂载,不写回
mode string 0755 缓存目录权限
uid int 0 缓存目录 owner UID
gid int 0 缓存目录 owner GID

type=bind(绑定挂载)

将构建上下文中的文件挂载进构建步骤,无需 COPY 即可使用,适合临时读取的文件(如依赖定义文件)。

FROM python:3.12-slim
WORKDIR /app
RUN --mount=type=bind,source=requirements.txt,target=/req.txt \
    pip install -r /req.txt
# requirements.txt 不会被复制进镜像
COPY src/ ./src/
# 挂载整个目录(只读)
FROM node:20-alpine
RUN --mount=type=bind,source=.,target=/src,readonly \
    cd /src && npm run test

type=bind 参数说明:

参数 类型 默认值 说明
target string 必填 容器内挂载路径
source string . 构建上下文中的源路径(相对路径)
from string 从某个构建阶段取文件,而非构建上下文
readonly bool false 是否只读挂载
rw bool false readonly 的反义写法

type=secret(安全注入密钥)

在构建时注入敏感信息(如 npm token、SSH 密钥配置),密钥内容不会写入任何镜像层,docker history 中不可见。

# 使用私有 npm registry
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci
COPY . .

构建命令:

# 从文件注入
docker build --secret id=npmrc,src=$HOME/.npmrc .

# 从环境变量注入(通过 /dev/stdin 或临时文件)
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /tmp/npmrc
docker build --secret id=npmrc,src=/tmp/npmrc .
rm /tmp/npmrc
# 访问私有 pip 源
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
    pip install -r requirements.txt

type=secret 参数说明:

参数 类型 默认值 说明
id string 必填 secret 标识符,与 --secret id= 对应
target string /run/secrets/ 容器内挂载路径
required bool false 设为 true 时 secret 不存在则构建失败
mode string 0400 文件权限
uid int 0 文件 owner UID
gid int 0 文件 owner GID

type=ssh(SSH agent 转发)

将宿主机的 SSH agent 转发进构建步骤,用于拉取私有 Git 仓库,私钥不会写入镜像。

FROM python:3.12-slim
RUN apt-get update && apt-get install -y git openssh-client
RUN mkdir -p -m 0600 ~/.ssh && \
    ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN --mount=type=ssh \
    git clone [email protected]:myorg/private-repo.git /app
WORKDIR /app
RUN pip install -r requirements.txt

构建命令:

# 确保 SSH agent 已加载密钥
eval $(ssh-agent)
ssh-add ~/.ssh/id_rsa

# 构建时转发 SSH agent
docker build --ssh default .

# 指定特定 socket
docker build --ssh default=/run/user/1000/keyring/ssh .

type=ssh 参数说明:

参数 类型 默认值 说明
id string default SSH socket 标识符
target string $SSH_AUTH_SOCK 容器内 socket 路径
required bool false 设为 true 时 SSH agent 不可用则失败
mode string 0600 socket 文件权限
uid int 0 socket owner UID
gid int 0 socket owner GID

type=tmpfs(临时内存文件系统)

在构建步骤中挂载 tmpfs,适合需要临时写入但不希望写入镜像层的场景。

FROM ubuntu:22.04
RUN --mount=type=tmpfs,target=/tmp/build \
    cd /tmp/build && \
    curl -o archive.tar.gz https://example.com/archive.tar.gz && \
    tar -xzf archive.tar.gz && \
    make install

type=tmpfs 参数说明:

参数 类型 默认值 说明
target string 必填 容器内挂载路径
size int 系统默认 tmpfs 大小(字节),0 表示不限制

多平台构建(Cross-Platform Build)

BuildKit 通过 QEMU 用户态模拟或交叉编译实现在单台机器上构建多平台镜像。

环境准备

# 安装 QEMU 用户态模拟(Linux 宿主机)
docker run --privileged --rm tonistiigi/binfmt --install all

# 创建支持多平台的 builder
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap

内置平台变量

BuildKit 在构建时自动注入以下 ARG,无需声明即可在 Dockerfile 中使用:

变量 说明 示例值
BUILDPLATFORM 构建机器平台 linux/amd64
BUILDOS 构建机器操作系统 linux
BUILDARCH 构建机器架构 amd64
BUILDVARIANT 构建机器变体 (arm 时为 v7 等)
TARGETPLATFORM 目标平台 linux/arm64
TARGETOS 目标操作系统 linux
TARGETARCH 目标架构 arm64
TARGETVARIANT 目标变体 v8

多平台 Dockerfile 示例

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build -o /app/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

多平台构建命令

# 构建并推送到 registry(必须推送,本地无法同时存储多平台)
docker buildx build \
    --platform linux/amd64,linux/arm64,linux/arm/v7 \
    -t myorg/myapp:latest \
    --push \
    .

# 仅构建不推送(测试用)
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t myorg/myapp:latest \
    .

# 导出到本地 tar 文件
docker buildx build \
    --platform linux/amd64 \
    -t myorg/myapp:latest \
    -o type=docker,dest=./myapp.tar \
    .

CI 多平台发布完整示例(GitHub Actions)

name: Multi-Platform Build

on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            myorg/myapp:latest
            myorg/myapp:${{ github.ref_name }}

构建缓存共享策略

--cache-from 和 --cache-to

缓存类型 --cache-to 示例 说明
registry type=registry,ref=myorg/myapp:cache 存储在 OCI registry
inline type=inline 嵌入到镜像本身(仅单平台)
local type=local,dest=/tmp/cache 存储在本地目录
gha type=gha GitHub Actions Cache(自动鉴权)
s3 type=s3,bucket=mybucket,region=us-east-1 存储在 S3
# 使用 registry 缓存
docker buildx build \
    --cache-from type=registry,ref=myorg/myapp:cache \
    --cache-to type=registry,ref=myorg/myapp:cache,mode=max \
    -t myorg/myapp:latest \
    --push \
    .

# mode=max 缓存所有中间层(推荐);mode=min 只缓存最终层

BuildKit 并行阶段执行

BuildKit 自动分析多阶段构建的依赖关系并行执行互不依赖的阶段:

# syntax=docker/dockerfile:1
FROM node:20 AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# 与 frontend-builder 并行执行
FROM golang:1.22 AS backend-builder
WORKDIR /backend
COPY backend/go.* ./
RUN go mod download
COPY backend/ .
RUN go build -o server .

FROM ubuntu:22.04
COPY --from=frontend-builder /frontend/dist /var/www/html
COPY --from=backend-builder /backend/server /usr/local/bin/server
CMD ["/usr/local/bin/server"]

使用 Heredoc 减少层数

# syntax=docker/dockerfile:1
FROM ubuntu:22.04

# 传统写法:多个 RUN 命令多个层,或用 && 拼接可读性差
# BuildKit heredoc 写法:单层,可读性好
RUN <<EOF
apt-get update
apt-get install -y curl git vim
rm -rf /var/lib/apt/lists/*
useradd -m -u 1001 appuser
mkdir -p /app && chown appuser:appuser /app
EOF

USER appuser
WORKDIR /app

Docker 安全加固

镜像安全

使用非 root 用户

默认情况下容器以 root 身份运行,这是不必要的权限暴露。

FROM node:20-alpine

# 方式一:使用基础镜像已有的非特权用户
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .
USER node

# 方式二:创建专用系统用户
FROM python:3.12-slim
RUN groupadd --gid 1001 appgroup && \
    useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser
WORKDIR /app
COPY --chown=appuser:appgroup requirements.txt ./
RUN pip install -r requirements.txt
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

useradd 关键参数:

参数 说明
--uid / -u 指定 UID(推荐 1000 以上,避免与系统用户冲突)
--gid / -g 指定主组 GID
--system / -r 创建系统用户(UID < 1000,无 home 目录)
--no-create-home / -M 不创建 home 目录(服务进程不需要)
--shell 登录 shell(服务用户设为 /bin/false 或 /sbin/nologin)

Distroless 镜像

Distroless 镜像不包含 shell、包管理器等工具,大幅减少攻击面。

# Go 应用(静态编译 + distroless)
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
# Node.js 应用
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
USER nonroot:nonroot
CMD ["src/index.js"]

常用 Distroless 镜像:

镜像 用途
gcr.io/distroless/static-debian12 Go / Rust 静态编译
gcr.io/distroless/base-debian12 需要 glibc 的应用
gcr.io/distroless/cc-debian12 需要 libstdc++ 的应用
gcr.io/distroless/python3-debian12 Python 应用
gcr.io/distroless/nodejs20-debian12 Node.js 20 应用
gcr.io/distroless/java21-debian12 Java 21 应用

镜像扫描

# Docker Scout(Docker 官方)
docker scout cves myorg/myapp:latest
docker scout recommendations myorg/myapp:latest

# Trivy(开源,功能全面)
trivy image myorg/myapp:latest
trivy image --severity HIGH,CRITICAL myorg/myapp:latest
trivy image --exit-code 1 --severity CRITICAL myorg/myapp:latest  # CI 中失败

# 扫描本地 Dockerfile
trivy config ./Dockerfile

# Snyk
snyk container test myorg/myapp:latest

固定镜像 Digest

使用 tag 可能因为镜像更新而拉取不同内容;使用 digest 确保不变性:

# 获取镜像 digest
docker inspect --format='{{index .RepoDigests 0}}' node:20-alpine
# 输出: node@sha256:abc123...

# 在 Dockerfile 中固定 digest
FROM node:20-alpine@sha256:a0b943f9b0e2e6f3d8c1234567890abcdef1234567890abcdef1234567890ab

运行时安全

安全运行参数综合示例

docker run \
    --read-only \
    --tmpfs /tmp:rw,size=64m,mode=1777 \
    --tmpfs /var/run:rw,size=8m \
    --cap-drop ALL \
    --cap-add NET_BIND_SERVICE \
    --security-opt no-new-privileges \
    --security-opt seccomp=./seccomp-profile.json \
    --user 1001:1001 \
    --pids-limit 100 \
    --memory 512m \
    --memory-swap 512m \
    --cpus 1.0 \
    -p 80:80 \
    myorg/myapp:latest

运行时安全参数说明:

参数 说明
--read-only 容器根文件系统只读,防止运行时写入
--tmpfs path 在指定路径挂载 tmpfs,允许临时写入
--cap-drop ALL 删除所有 Linux Capabilities
--cap-add CAP 仅添加所需的 Capability
--security-opt no-new-privileges 禁止进程通过 setuid/setgid 获得新权限
--security-opt seccomp=file 使用自定义 seccomp 配置文件限制系统调用
--security-opt apparmor=profile 应用 AppArmor profile
--user uid:gid 以指定 UID:GID 运行
--userns-remap=default 启用用户命名空间重映射(配置在 daemon.json)
--pids-limit n 限制容器内进程数,防止 fork bomb

Linux Capabilities 常用参考

Capability 说明 常见用途
NET_BIND_SERVICE 绑定 1024 以下端口 Web 服务器监听 80/443
NET_ADMIN 网络管理操作 VPN 客户端、网络工具
SYS_PTRACE 进程跟踪(ptrace 系统调用) 调试工具、性能分析
SYS_ADMIN 宽泛的系统管理操作(高危) 挂载文件系统等
CHOWN 修改文件所有者 入口脚本调整文件权限
SETUID / SETGID 修改进程 UID/GID sudo 类工具
DAC_OVERRIDE 绕过 DAC 权限检查 访问受限文件
KILL 向任意进程发送信号 进程管理工具
IPC_LOCK 锁定内存(mlock) 数据库、内存缓存
SYS_NICE 修改进程优先级 实时应用

自定义 Seccomp 配置文件

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
    "syscalls": [
        {
            "names": [
                "accept4", "access", "arch_prctl", "bind", "brk",
                "clock_gettime", "clone", "close", "connect",
                "epoll_create1", "epoll_ctl", "epoll_wait",
                "execve", "exit", "exit_group",
                "fcntl", "fstat", "futex", "getcwd", "getdents64",
                "getpid", "getppid", "getrandom", "getsockname",
                "getsockopt", "ioctl", "listen", "lseek", "mmap",
                "mprotect", "munmap", "nanosleep", "newfstatat",
                "openat", "pipe2", "poll", "prctl", "pread64",
                "read", "readlink", "recvfrom", "recvmsg", "rt_sigaction",
                "rt_sigprocmask", "rt_sigreturn", "sendmsg", "sendto",
                "set_robust_list", "setsockopt", "sigaltstack",
                "socket", "stat", "statfs", "tgkill", "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}
docker run --security-opt seccomp=./seccomp-profile.json myapp

用户命名空间重映射

/etc/docker/daemon.json 中配置:

{
    "userns-remap": "default"
}

重启 Docker 后,容器内的 root(UID 0)会映射为宿主机上的高 UID(如 100000),即使容器逃逸也无法获得宿主机 root 权限。

敏感数据处理

敏感信息传入方式安全对比

方式 写入镜像层 docker history 可见 运行时环境可见 推荐程度
Dockerfile ENV/ARG 硬编码 禁止
docker run -e KEY=VALUE 是(/proc/1/environ) 低风险场景可用
--env-file .env 注意文件权限
构建时 --secret(RUN --mount=type=secret) 仅构建步骤内 推荐(构建阶段)
Docker Secrets(Swarm) 文件(/run/secrets/) 推荐(运行时)
Vault / 外部 Secret Manager 应用主动读取 最佳(大规模)

Docker Swarm Secrets 示例

# 创建 secret
echo "supersecretpassword" | docker secret create db_password -
docker secret create tls_cert ./cert.pem

# 查看 secrets
docker secret ls
docker secret inspect db_password

在 service 中使用:

docker service create \
    --name myapp \
    --secret db_password \
    --secret source=tls_cert,target=tls.pem,mode=0400 \
    myorg/myapp:latest

应用内读取(secret 挂载在 /run/secrets/<name>):

# Python 示例
import os

def get_secret(secret_name: str) -> str:
    secret_path = f"/run/secrets/{secret_name}"
    if os.path.exists(secret_path):
        with open(secret_path, 'r') as f:
            return f.read().strip()
    # 回退到环境变量(开发环境)
    return os.environ.get(secret_name.upper(), '')

db_password = get_secret("db_password")

网络安全

自定义网络隔离

# docker-compose.yml
services:
  frontend:
    image: nginx:alpine
    networks:
      - public
      - internal

  api:
    image: myorg/api:latest
    networks:
      - internal
      - database

  db:
    image: postgres:16
    networks:
      - database

networks:
  public:
    driver: bridge
  internal:
    driver: bridge
    internal: false
  database:
    driver: bridge
    internal: true  # 无外部连接,只有连接此网络的容器才能访问

网络安全最佳实践

services:
  api:
    image: myorg/api:latest
    # 不使用 ports(不对外暴露端口),只有同网络的容器通过服务名访问
    expose:
      - "8000"
    networks:
      - internal
    # 避免 network_mode: host,除非性能敏感且信任容器内代码

Docker Swarm 集群编排

初始化集群

# 在 manager 节点执行
docker swarm init --advertise-addr 192.168.1.10

# 输出示例:
# Swarm initialized: current node (xxx) is now a manager.
# To add a worker to this swarm, run the following command:
#     docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377

# 在 worker 节点执行
docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377

# 查看集群节点
docker node ls

# 获取 manager/worker join token(忘记时)
docker swarm join-token manager
docker swarm join-token worker

# 提升 worker 为 manager
docker node promote <NODE-ID>

# 降级 manager 为 worker
docker node demote <NODE-ID>

docker swarm init 参数说明:

参数 类型 默认值 说明
--advertise-addr string 自动检测 其他节点连接此 manager 的地址(IP:port 或接口名)
--listen-addr string 0.0.0.0:2377 manager 监听地址
--data-path-addr string --advertise-addr VXLAN 数据平面地址(overlay 网络用)
--data-path-port uint32 4789 VXLAN UDP 端口
--force-new-cluster bool false 从单个节点强制创建新集群(灾难恢复)
--availability enum active active / pause / drain

核心概念

概念 说明
Node 集群中的 Docker 主机,分为 manager 和 worker
Manager 维护集群状态(Raft 一致性),响应 API 调用,调度任务
Worker 执行任务(运行容器)
Service 期望状态声明(镜像、副本数、网络、约束等)
Task 分配到节点的工作单元,包含一个容器实例
Stack 通过 Compose 文件定义的一组相关 Service
Overlay Network 跨节点的虚拟网络,Swarm 服务默认使用

Service 管理

# 创建 service
docker service create \
    --name web \
    --replicas 3 \
    -p 80:80 \
    --update-delay 10s \
    --update-parallelism 1 \
    --restart-condition on-failure \
    nginx:1.25

# 查看所有 service
docker service ls

# 查看 service 的 task 分布
docker service ps web

# 查看 service 详情
docker service inspect web
docker service inspect --pretty web

# 查看日志
docker service logs web
docker service logs -f --tail 100 web

# 更新镜像(滚动更新)
docker service update --image nginx:1.26 web

# 扩缩容
docker service scale web=5
docker service scale web=2 api=4

# 手动回滚
docker service rollback web

# 删除 service
docker service rm web

docker service create 完整参数参考

参数 类型 默认值 说明
--replicas int 1 副本数(replicated 模式)
--replicas-max-per-node int 0(不限) 每个节点最多副本数
--mode enum replicated replicated / global / replicated-job / global-job
--update-delay duration 0s 每个 task 更新之间的等待时间
--update-parallelism int 1 同时更新的 task 数量(0 = 全部同时)
--update-failure-action enum pause pause / continue / rollback
--update-max-failure-ratio float 0 允许失败的 task 比例(0~1)
--update-monitor duration 5s 每个 task 更新后观察是否失败的时间窗口
--update-order enum stop-first stop-first(先停旧再起新)/ start-first(先起新再停旧,零停机)
--rollback-delay duration 0s 回滚各 task 之间的等待时间
--rollback-parallelism int 1 并行回滚的 task 数量
--rollback-failure-action enum pause pause / continue
--rollback-max-failure-ratio float 0 回滚允许失败比例
--rollback-monitor duration 5s 回滚后监控时间窗口
--rollback-order enum stop-first stop-first / start-first
--restart-condition enum any none / on-failure / any
--restart-delay duration 5s 重启等待时间
--restart-max-attempts int 不限 最大重启次数
--restart-window duration 0(不限) 判断重启次数的时间窗口
--constraint string 节点约束(如 node.role==manager)
--placement-pref string 分布偏好(如 spread=node.labels.zone)
--limit-cpu float 不限 CPU 使用上限(核数)
--limit-memory bytes 不限 内存使用上限
--limit-pids int 不限 进程数上限
--reserve-cpu float 0 CPU 预留(调度时保证可用)
--reserve-memory bytes 0 内存预留
--network string 连接的 overlay 网络名
--publish / -p port 端口映射(host:container)
--secret string 挂载 Docker Secret
--config string 挂载 Docker Config
--env / -e string 环境变量
--mount string 卷挂载配置
--workdir / -w string 工作目录
--user / -u string 运行用户
--hostname string 容器 hostname
--log-driver string 继承 daemon 日志驱动
--log-opt string 日志驱动选项
--health-cmd string 健康检查命令
--health-interval duration 30s 健康检查间隔
--health-timeout duration 30s 健康检查超时
--health-retries int 3 健康检查失败重试次数
--health-start-period duration 0s 启动宽限期

Stack 部署

Stack 使用 Compose 文件格式,支持 Swarm 特有的 deploy 配置块。

# docker-compose.prod.yml
version: "3.9"

services:
  web:
    image: myorg/web:1.2.3
    networks:
      - frontend
      - backend
    ports:
      - "80:80"
      - "443:443"
    deploy:
      mode: replicated
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
        monitor: 15s
        order: start-first
      rollback_config:
        parallelism: 1
        delay: 5s
        failure_action: pause
        order: stop-first
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M
      placement:
        constraints:
          - node.role == worker
          - node.labels.zone == production
        preferences:
          - spread: node.labels.datacenter
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: postgres:16
    networks:
      - backend
    secrets:
      - db_password
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
          - node.labels.storage == ssd

networks:
  frontend:
    driver: overlay
  backend:
    driver: overlay
    internal: true

volumes:
  db_data:
    driver: local

secrets:
  db_password:
    external: true
# 部署 stack
docker stack deploy -c docker-compose.prod.yml myapp

# 查看 stack 列表
docker stack ls

# 查看 stack 下的所有 service
docker stack services myapp

# 查看 stack 下所有 task
docker stack ps myapp

# 删除 stack(不删除 volumes)
docker stack rm myapp

Overlay 网络

# 创建加密 overlay 网络
docker network create \
    --driver overlay \
    --opt encrypted \
    --attachable \
    --subnet 10.10.0.0/24 \
    myoverlay

# attachable 允许独立容器(非 service)连接此网络
# encrypted 启用 VXLAN 层的 AES-GCM 加密

overlay 网络参数说明:

参数 类型 默认值 说明
--opt encrypted bool false 启用数据平面加密(AES-128-GCM)
--attachable bool false 允许独立容器连接(非 service 也可用)
--ingress bool false 创建 Swarm routing mesh 的 ingress 网络
--subnet CIDR 自动分配 网络子网
--gateway IP 子网第一个可用 IP 网关 IP
--ip-range CIDR 子网范围 容器 IP 分配范围
--opt com.docker.network.driver.mtu int 1450 MTU 大小(overlay 默认降低以避免碎片)

Secrets 和 Configs

# 创建 secret(不可修改,只能删除重建)
echo "mypassword" | docker secret create db_password -
docker secret create ssl_cert ./cert.pem
openssl rand -base64 32 | docker secret create jwt_secret -

# 列出 secrets(内容不可查看)
docker secret ls
docker secret inspect db_password

# 删除 secret(需先从所有 service 移除)
docker secret rm db_password

# 创建 config(存储非敏感配置,内容可查看)
docker config create nginx_conf ./nginx.conf
docker config ls
docker config inspect --pretty nginx_conf

在 service 中使用 secret 和 config:

docker service create \
    --name myapp \
    --secret db_password \
    --secret source=ssl_cert,target=/etc/ssl/app.pem,mode=0400 \
    --config source=nginx_conf,target=/etc/nginx/nginx.conf \
    myorg/myapp:latest

完整数据库密码示例:

# 1. 创建 secret
openssl rand -base64 32 | tr -d '\n' | docker secret create postgres_password -

# 2. 创建数据库 service
docker service create \
    --name postgres \
    --network myapp_backend \
    --secret postgres_password \
    --env POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \
    --env POSTGRES_USER=myapp \
    --env POSTGRES_DB=myapp \
    --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
    --constraint node.labels.storage==ssd \
    postgres:16

# 3. 应用读取 secret
docker service create \
    --name api \
    --network myapp_backend \
    --secret postgres_password \
    --env DB_PASSWORD_FILE=/run/secrets/postgres_password \
    myorg/api:latest

生产环境部署方案

健康检查与自动恢复

Dockerfile 中定义健康检查

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD node healthcheck.js || exit 1

CMD ["node", "server.js"]
// healthcheck.js
const http = require('http');
const options = {
    host: 'localhost',
    port: 3000,
    path: '/health',
    timeout: 5000
};

const req = http.request(options, (res) => {
    process.exit(res.statusCode === 200 ? 0 : 1);
});

req.on('error', () => process.exit(1));
req.on('timeout', () => { req.destroy(); process.exit(1); });
req.end();

HEALTHCHECK 参数说明:

参数 类型 默认值 说明
--interval duration 30s 健康检查执行间隔
--timeout duration 30s 单次检查超时时间
--start-period duration 0s 启动宽限期(期间失败不计入 retries)
--start-interval duration 5s 宽限期内的检查间隔(Docker 25.0+)
--retries int 3 连续失败多少次后标记为 unhealthy

Compose 中的健康检查依赖链

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  api:
    image: myorg/api:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  web:
    image: myorg/web:latest
    depends_on:
      api:
        condition: service_healthy
    ports:
      - "80:80"

滚动更新与零停机部署

update_config 完整配置

services:
  api:
    image: myorg/api:latest
    deploy:
      replicas: 4
      update_config:
        parallelism: 1          # 每次更新 1 个副本
        delay: 10s              # 每个副本更新之间等待 10s
        failure_action: rollback  # 失败自动回滚
        monitor: 20s            # 每个副本更新后监控 20s
        max_failure_ratio: 0.1  # 允许 10% 失败率
        order: start-first      # 先启动新副本再停旧副本(零停机)
      rollback_config:
        parallelism: 2          # 并行回滚 2 个副本
        delay: 5s
        failure_action: pause
        monitor: 10s
        order: stop-first

更新操作流程

# 更新镜像
docker service update --image myorg/api:1.3.0 myapp_api

# 查看更新进度
docker service ps myapp_api

# 查看更新失败原因
docker service ps myapp_api --filter desired-state=shutdown --no-trunc

# 手动回滚
docker service rollback myapp_api

# 强制重新部署(不更改镜像,用于重新拉取 latest)
docker service update --force myapp_api

日志集中收集

Fluentd 日志驱动

# docker-compose.yml
services:
  api:
    image: myorg/api:latest
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.{{.Name}}"
        fluentd-async: "true"    # 异步发送,避免阻塞容器
        fluentd-buffer-limit: "10485760"  # 10MB 缓冲区

  fluentd:
    image: fluent/fluentd:v1.16
    ports:
      - "24224:24224"
    volumes:
      - ./fluent.conf:/fluentd/etc/fluent.conf
      - fluentd_log:/var/log/fluentd

volumes:
  fluentd_log:
<!-- fluent.conf -->
<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<filter docker.**>
  @type record_transformer
  <record>
    hostname "#{Socket.gethostname}"
    environment production
  </record>
</filter>

<match docker.**>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix docker
  include_timestamp true
  flush_interval 5s
</match>

Loki + Promtail(更轻量的选择)

# docker-compose.yml
services:
  loki:
    image: grafana/loki:2.9.0
    ports:
      - "3100:3100"
    volumes:
      - loki_data:/loki
      - ./loki-config.yml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:2.9.0
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  loki_data:
  grafana_data:
# promtail-config.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: [__meta_docker_container_name]
        target_label: container
      - source_labels: [__meta_docker_container_log_stream]
        target_label: stream

监控方案

cAdvisor + Prometheus + Grafana

# docker-compose.monitoring.yml
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.47.0
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    ports:
      - "8080:8080"
    privileged: true
    devices:
      - /dev/kmsg

  prometheus:
    image: prom/prometheus:v2.48.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'

  grafana:
    image: grafana/grafana:10.2.0
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/var/lib/grafana/dashboards
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus

volumes:
  prometheus_data:
  grafana_data:
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'docker-daemon'
    static_configs:
      - targets: ['host.docker.internal:9323']

关键监控指标:

指标名 说明 告警阈值建议
container_cpu_usage_seconds_total 容器 CPU 使用量(累计秒数) rate > limit 的 90%
container_memory_usage_bytes 容器内存使用量(字节) > limit 的 85%
container_memory_working_set_bytes 工作集内存(不含缓存,更准确) > limit 的 85%
container_network_receive_bytes_total 容器网络接收字节数 异常突增
container_network_transmit_bytes_total 容器网络发送字节数 异常突增
container_fs_usage_bytes 容器文件系统使用量 > 80%
container_restart_count 容器重启次数 > 3 次/小时
container_oom_events_total OOM 事件数 > 0

CI/CD 集成

GitHub Actions 完整 Workflow

# .github/workflows/docker-build-push.yml
name: Build and Deploy

on:
  push:
    branches: [main]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

env:
  REGISTRY_DOCKERHUB: docker.io
  REGISTRY_GHCR: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write

    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU(多平台支持)
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_DOCKERHUB }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_GHCR }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata(生成 tag 和 label)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME }}
            ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=sha-,format=short
            type=ref,event=branch
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            BUILD_DATE=${{ github.event.repository.updated_at }}
            VCS_REF=${{ github.sha }}
            VERSION=${{ steps.meta.outputs.version }}

      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload Trivy scan results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production

    steps:
      - name: Deploy to production via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
            docker service update \
              --image ghcr.io/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image-digest }} \
              --update-order start-first \
              myapp_api

GitLab CI 完整配置

# .gitlab-ci.yml
stages:
  - build
  - scan
  - test
  - push
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest

.docker_login: &docker_login
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  <<: *docker_login
  script:
    - |
      docker buildx create --use --name mybuilder
      docker buildx build \
        --platform linux/amd64,linux/arm64 \
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:cache \
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max \
        --tag $IMAGE_TAG \
        --push \
        .
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL $IMAGE_TAG
  allow_failure: false

test:
  stage: test
  image: docker:24
  services:
    - docker:24-dind
  <<: *docker_login
  script:
    - docker pull $IMAGE_TAG
    - docker run --rm $IMAGE_TAG npm test

push-latest:
  stage: push
  image: docker:24
  services:
    - docker:24-dind
  <<: *docker_login
  script:
    - docker pull $IMAGE_TAG
    - docker tag $IMAGE_TAG $IMAGE_LATEST
    - docker push $IMAGE_LATEST
    - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
        docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
      fi
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

deploy-production:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$PROD_SSH_KEY" | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - echo "$PROD_HOST_KEY" >> ~/.ssh/known_hosts
  script:
    - |
      ssh $PROD_USER@$PROD_HOST "
        docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD
        docker stack deploy -c /opt/myapp/docker-compose.prod.yml myapp
      "
  environment:
    name: production
    url: https://myapp.example.com
  rules:
    - if: $CI_COMMIT_TAG
      when: manual

镜像版本策略

# 在 CI 中同时推送多个 tag
IMAGE_BASE="myorg/myapp"
VERSION="1.2.3"
SHA=$(git rev-parse --short HEAD)

docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t ${IMAGE_BASE}:latest \
    -t ${IMAGE_BASE}:${VERSION} \
    -t ${IMAGE_BASE}:$(echo $VERSION | cut -d. -f1).$(echo $VERSION | cut -d. -f2) \
    -t ${IMAGE_BASE}:$(echo $VERSION | cut -d. -f1) \
    -t ${IMAGE_BASE}:${SHA} \
    --push \
    .

版本策略说明:

Tag 示例 用途
latest latest 跟踪主分支最新版本(生产不推荐直接使用)
完整语义化版本 1.2.3 精确锁定版本(生产推荐)
主次版本 1.2 接受补丁更新
主版本 1 接受次要更新
Commit SHA sha-a3f1b2c 唯一标识构建产物,可追溯

容器内应用最佳实践

优雅关闭(Graceful Shutdown)

为什么需要优雅关闭

Docker 停止容器时,先发送 SIGTERM,等待 stop_grace_period(默认 10s)后若进程未退出则发送 SIGKILL。应用应捕获 SIGTERM 完成清理工作(关闭连接、完成正在处理的请求等)再退出。

Node.js 优雅关闭

// server.js
const http = require('http');
const app = require('./app');

const server = http.createServer(app);
let isShuttingDown = false;

server.listen(3000, () => {
    console.log('Server listening on port 3000');
});

function gracefulShutdown(signal) {
    console.log(`Received ${signal}, starting graceful shutdown`);
    isShuttingDown = true;

    server.close((err) => {
        if (err) {
            console.error('Error during server close:', err);
            process.exit(1);
        }
        // 关闭数据库连接等
        console.log('Server closed, exiting');
        process.exit(0);
    });

    // 超时强制退出
    setTimeout(() => {
        console.error('Graceful shutdown timeout, forcing exit');
        process.exit(1);
    }, 9000); // 留 1s 给 Docker 的 10s 宽限期
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Python 优雅关闭

# main.py
import signal
import sys
import asyncio
import uvicorn
from fastapi import FastAPI

app = FastAPI()
shutdown_event = asyncio.Event()

@app.get("/health")
async def health():
    return {"status": "ok"}

@app.on_event("shutdown")
async def shutdown():
    print("Application shutting down")
    # 清理资源

def handle_sigterm(signum, frame):
    print(f"Received signal {signum}")
    shutdown_event.set()

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT, handle_sigterm)

if __name__ == "__main__":
    config = uvicorn.Config(
        "main:app",
        host="0.0.0.0",
        port=8000,
        timeout_graceful_shutdown=8
    )
    server = uvicorn.Server(config)
    server.run()

Go 优雅关闭

// main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    srv := &http.Server{Addr: ":8080", Handler: setupRoutes()}

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
    <-quit

    log.Println("Received shutdown signal, shutting down gracefully")

    ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server exited")
}

stop_grace_period 配置:

services:
  api:
    image: myorg/api:latest
    stop_grace_period: 30s  # 给应用足够时间完成优雅关闭
    stop_signal: SIGTERM    # 发送的信号(默认 SIGTERM)

容器内进程管理与 PID 1

PID 1 的特殊性

Linux 内核对 PID 1 的处理与其他进程不同:

  1. 未注册信号处理器的信号会被忽略(包括 SIGTERM),导致 docker stop 无法停止容器
  2. PID 1 需要回收僵尸子进程(wait),否则系统进程表会被僵尸进程填满

使用 tini 解决 PID 1 问题

FROM ubuntu:22.04

# 方式一:安装 tini
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .

# tini 作为 PID 1,管理应用进程
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]
# 方式二:从官方镜像获取 tini(Go 静态编译镜像)
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM ubuntu:22.04

COPY --from=xx /usr/bin/xx-info /usr/bin/xx-info
ARG TARGETPLATFORM
RUN ARCH=$(xx-info arch) && \
    curl -L https://github.com/krallin/tini/releases/download/v0.19.0/tini-${ARCH} \
    -o /tini && chmod +x /tini

ENTRYPOINT ["/tini", "--"]
CMD ["python", "app.py"]
# 方式三:运行时使用 --init 标志(Docker 自带 tini)
docker run --init myorg/myapp:latest
# Compose 中启用 init
services:
  api:
    image: myorg/api:latest
    init: true

配置外部化(12-Factor App)

环境变量 vs 配置文件 vs Secrets

配置类型 推荐传递方式 说明
非敏感配置(端口、日志级别) 环境变量 简单直接
非敏感配置文件(nginx.conf) Docker Config / Bind Mount 结构化配置
敏感信息(密码、API Key) Docker Secrets / Vault 不写入环境变量
平台信息(hostname、region) 环境变量(运行时注入) 12-Factor 原则
# 使用 Compose configs 管理非敏感配置文件
services:
  nginx:
    image: nginx:alpine
    configs:
      - source: nginx_config
        target: /etc/nginx/nginx.conf
        mode: 0444
    ports:
      - "80:80"

configs:
  nginx_config:
    file: ./nginx.conf   # Compose 模式

# Swarm 模式:
# configs:
#   nginx_config:
#     external: true

应用启动等待

方式对比

方式 适用场景 缺点
sleep 固定时间 演示 / 测试 不可靠,浪费时间
wait-for-it.sh 等待 TCP 端口可达 不检查服务就绪状态
depends_on + healthcheck Compose / Swarm 推荐,等待真正就绪
应用内重试 生产环境 最健壮,无需外部工具

wait-for-it.sh 用法

FROM node:20-alpine
RUN apk add --no-cache bash
COPY --chmod=755 wait-for-it.sh /usr/local/bin/wait-for-it
WORKDIR /app
COPY . .
RUN npm ci

ENTRYPOINT ["wait-for-it", "db:5432", "--", "wait-for-it", "redis:6379", "--"]
CMD ["node", "server.js"]

应用内重试(最健壮)

# db.py
import time
import psycopg2

def get_connection(max_retries: int = 10, delay: float = 2.0):
    for attempt in range(max_retries):
        try:
            conn = psycopg2.connect(
                host="db",
                database="myapp",
                user="myapp",
                password=open("/run/secrets/db_password").read().strip()
            )
            print(f"Connected to database on attempt {attempt + 1}")
            return conn
        except psycopg2.OperationalError as e:
            if attempt == max_retries - 1:
                raise
            wait = delay * (2 ** attempt)  # 指数退避
            print(f"Database not ready (attempt {attempt + 1}), retrying in {wait:.1f}s: {e}")
            time.sleep(wait)

性能调优

容器存储驱动

驱动 说明 推荐场景 性能特点
overlay2 OverlayFS,内核原生支持 生产首选,Linux 4.0+ 读写性能好,层缓存效率高
fuse-overlayfs 用户态 OverlayFS rootless Docker 性能略低于 overlay2
devicemapper 块设备,thin provisioning RHEL/CentOS 旧版 随机写性能好,配置复杂
btrfs B-tree 文件系统 需要快照等高级特性 碎片化后性能下降
zfs ZFS 文件系统 高可靠性需求 内存消耗大
vfs 直接复制,不共享层 测试环境 / CI 性能差,不推荐生产

查看和配置存储驱动:

# 查看当前存储驱动
docker info | grep "Storage Driver"

# 配置 overlay2(daemon.json)
{
    "storage-driver": "overlay2",
    "storage-opts": [
        "overlay2.override_kernel_check=true"
    ]
}

网络性能

MTU 设置

在云环境(AWS、GCP、Azure)中,物理网络 MTU 通常为 1500 字节,但 overlay 网络添加了额外的 VXLAN 头部(约 50 字节),若不降低 MTU 会导致数据包碎片化或丢失。

# 创建时指定 MTU
docker network create \
    --opt com.docker.network.driver.mtu=1450 \
    myoverlay

# 在 daemon.json 中全局设置默认 MTU
{
    "mtu": 1450
}
# Compose 中配置网络 MTU
networks:
  mynet:
    driver: overlay
    driver_opts:
      com.docker.network.driver.mtu: "1450"

host 网络模式

# 延迟敏感的服务(如高频交易、游戏服务器)可使用 host 模式
docker run --network host myorg/latency-sensitive-app:latest

注意:host 网络模式下容器直接使用宿主机网络栈,无 NAT 开销,但失去网络隔离,不推荐普通 Web 服务使用。

构建速度优化

.dockerignore 示例

# .dockerignore
.git
.gitignore
.github
*.md
*.log
.DS_Store
Thumbs.db

# Node.js
node_modules
npm-debug.log
.npm

# Python
__pycache__
*.py[cod]
.venv
venv
*.egg-info
dist
.tox
.coverage

# 构建产物
build
dist
out
target

# IDE
.idea
.vscode
*.swp
*.swo

# 测试
coverage
.nyc_output
test-results

# Docker
Dockerfile*
docker-compose*.yml

层缓存顺序优化

# 错误顺序:频繁变化的文件在前,导致缓存经常失效
FROM node:20-alpine
COPY . .             # 代码变化 -> 后续所有步骤重新执行
RUN npm ci           # 每次都重新安装

# 正确顺序:变化频率低的步骤在前
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./    # 依赖定义很少变化
RUN npm ci               # 缓存此层
COPY . .                 # 代码变化,仅从这里开始重新执行
RUN npm run build

内存和 CPU 调优

设置资源限制

services:
  api:
    image: myorg/api:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 256M
# 运行时设置(非 Swarm)
docker run \
    --cpus 2.0 \
    --memory 1g \
    --memory-swap 1g \     # 等于 --memory,禁止 swap
    --memory-swappiness 0 \ # 禁止 swap 使用
    myorg/api:latest

资源限制参数说明:

参数 类型 说明
--cpus float CPU 核数上限(1.5 表示 1.5 核)
--cpu-shares int CPU 权重(默认 1024,相对值)
--cpu-period int CPU CFS 周期(微秒,默认 100000)
--cpu-quota int CPU CFS 配额(微秒,--cpus 的底层实现)
--cpuset-cpus string 绑定到指定 CPU 核(如 "0,1" 或 "0-3")
--memory bytes 内存上限
--memory-swap bytes 内存+swap 上限(等于 --memory 时禁 swap)
--memory-reservation bytes 软性内存预留
--memory-swappiness int swap 使用倾向(0-100,0 最少 swap)
--oom-kill-disable bool 禁止 OOM killer(需同时设置 --memory)

JVM 容器化注意事项

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar

# 正确:启用容器感知,JVM 根据 cgroup limits 设置堆内存
# -XX:+UseContainerSupport 从 Java 10 开始默认启用
# -XX:MaxRAMPercentage 指定最大堆占容器内存的百分比
ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    "-XX:+ExitOnOutOfMemoryError", \
    "-jar", "app.jar"]

Docker 网络高级

Macvlan 网络

Macvlan 让容器拥有独立的 MAC 地址和 IP 地址,在物理网络中作为独立主机出现,无需 NAT,适合需要容器直接接入物理网络的场景。

# 创建 macvlan 网络
docker network create \
    --driver macvlan \
    --subnet 192.168.1.0/24 \
    --gateway 192.168.1.1 \
    --ip-range 192.168.1.128/25 \
    -o parent=eth0 \
    macvlan_net

# 运行容器并指定 IP
docker run -d \
    --network macvlan_net \
    --ip 192.168.1.200 \
    --name mycontainer \
    nginx:alpine
# Compose 中使用 macvlan
networks:
  macvlan_net:
    driver: macvlan
    driver_opts:
      parent: eth0
    ipam:
      config:
        - subnet: 192.168.1.0/24
          gateway: 192.168.1.1
          ip_range: 192.168.1.128/25

macvlan 限制:

  • 宿主机无法直接与 macvlan 容器通信(需使用 macvlan 子接口绕过)
  • 物理交换机需要开启混杂模式或支持多 MAC 地址

自定义 IPAM

networks:
  mynet:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.28.0.0/16
          gateway: 172.28.0.1
          ip_range: 172.28.5.0/24
          aux_addresses:
            reserved1: 172.28.5.1
            reserved2: 172.28.5.2

IPAM 配置说明:

字段 类型 说明
subnet CIDR 网络子网
gateway IP 网关 IP
ip_range CIDR 容器 IP 分配范围(subnet 的子集)
aux_addresses map 保留地址,不分配给容器

服务发现与负载均衡

Swarm 内置 DNS

Swarm 在 overlay 网络内运行嵌入式 DNS 服务器(127.0.0.11),容器使用服务名作为 hostname 进行服务发现。

# 在容器内测试 DNS 解析
docker exec -it <container-id> nslookup myservice

# 结果:返回 VIP(Virtual IP)或直接返回所有 task IP(DNSRR 模式)

VIP vs DNSRR 负载均衡

# VIP 模式(默认):DNS 返回单一虚拟 IP,负载均衡由内核 ipvs 完成
docker service create --name myservice --endpoint-mode vip nginx

# DNSRR 模式:DNS 轮询返回各 task IP,客户端负责负载均衡
docker service create --name myservice --endpoint-mode dnsrr nginx
模式 说明 适用场景
VIP(默认) 单一 VIP,内核 ipvs 负载均衡,健康检查自动剔除不健康节点 大多数场景
DNSRR DNS 轮询,客户端直连 task IP,需客户端支持 gRPC 等长连接协议

日常运维命令

清理命令完整参考

命令 说明
docker system prune 清理停止的容器、未使用的网络、悬空镜像和构建缓存
docker system prune -a 同上,但同时清理所有未被容器引用的镜像(不只悬空镜像)
docker system prune --volumes 同上,额外清理未使用的卷(数据会丢失,谨慎)
docker system prune -f 跳过确认提示
docker image prune 清理悬空镜像(无 tag 或无容器引用的中间层)
docker image prune -a 清理所有未被运行中容器引用的镜像
docker container prune 清理所有已停止的容器
docker network prune 清理未被任何容器使用的网络
docker volume prune 清理未被任何容器引用的卷
docker volume prune --filter label=env=dev 按标签过滤清理
docker builder prune 清理 BuildKit 构建缓存
docker builder prune --keep-storage 5GB 保留最新 5GB 缓存,清理其余
docker system df 显示 Docker 各类资源的磁盘占用
docker system df -v 详细列出每个镜像/容器/卷的占用

诊断与调试

docker inspect 高级用法

# 获取容器 IP 地址
docker inspect --format '{{.NetworkSettings.IPAddress}}' mycontainer

# 获取指定网络的 IP
docker inspect --format '{{.NetworkSettings.Networks.mynet.IPAddress}}' mycontainer

# 获取所有容器的 name 和 IP
docker inspect --format '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
    $(docker ps -q)

# 获取容器的环境变量
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' mycontainer

# 获取容器挂载信息
docker inspect --format '{{json .Mounts}}' mycontainer | jq .

# 获取容器的退出码和状态
docker inspect --format '{{.State.ExitCode}} {{.State.Status}}' mycontainer

# 获取容器重启次数
docker inspect --format '{{.RestartCount}}' mycontainer

# 获取 service 的 VIP
docker inspect --format '{{range .Endpoint.VirtualIPs}}{{.Addr}}{{end}}' \
    $(docker service inspect myservice -q)

docker stats 自定义格式

# 自定义 stats 输出列
docker stats --format \
    "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"

# 只查看指定容器
docker stats myapp_api myapp_db

# 单次快照(不持续刷新)
docker stats --no-stream

# 输出 JSON(机器可读)
docker stats --format '{{json .}}' --no-stream

docker stats 格式变量:

变量 说明
.Name 容器名
.ID 容器 ID
.CPUPerc CPU 使用百分比
.MemUsage 内存使用量 / 限制
.MemPerc 内存使用百分比
.NetIO 网络 I/O(接收 / 发送)
.BlockIO 块设备 I/O(读 / 写)
.PIDs 容器内进程数

其他诊断命令

# 监听 Docker 守护进程事件
docker events
docker events --filter type=container --filter event=die
docker events --since 1h --until now

# 查看容器文件系统变更(相对镜像的差异)
docker diff mycontainer
# A = 新增,C = 修改,D = 删除

# 查看容器内进程(无需进入容器)
docker top mycontainer
docker top mycontainer aux

# 查看镜像构建历史(可能泄露敏感信息)
docker history myorg/myapp:latest
docker history --no-trunc myorg/myapp:latest

# 使用 nsenter 进入容器命名空间(不依赖 docker exec,容器内无 shell 时有用)
PID=$(docker inspect --format '{{.State.Pid}}' mycontainer)
nsenter --target $PID --mount --uts --ipc --net --pid -- sh

# 导出容器文件系统为 tar
docker export mycontainer | tar -tv | head -50

# 将 image 保存为 tar(含所有层和 metadata)
docker save myorg/myapp:latest | gzip > myapp.tar.gz

# 从 tar 加载镜像
docker load < myapp.tar.gz

踩坑与最佳实践清单

镜像与构建

  • 生产环境禁止使用 latest tag,任何时候应使用固定版本(如 1.2.3sha256:...),避免因镜像更新引入不一致行为
  • 镜像层数限制:Union FS 的实际限制在不同驱动下各异(overlay2 约 128 层),复杂多阶段构建应保持最终镜像层数在合理范围内(通常不超过 20-30 层)
  • docker history 会泄露 ENVARGRUN 中的所有值,构建时的敏感信息必须使用 RUN --mount=type=secret,而非 ARG 传递
  • COPY 和 ADD 的区别:ADD 会自动解压 tar,并支持 URL,但行为不透明;除非需要解压,否则始终使用 COPY
  • 多阶段构建的 COPY --from 应使用阶段名(AS builder)而非阶段序号,序号在修改 Dockerfile 后容易错位

Swarm 与 Secrets

  • Swarm secrets 不支持热更新:更新 secret 内容后,需要删除旧 secret(先从 service 移除)、创建同名新 secret、重新部署 service,应用才能读取新值
  • Secret 名称全局唯一,删除后可重建同名 secret,但内容可以不同
  • Docker Config 与 Secret 的区别:Config 内容可通过 docker config inspect 查看,适合非敏感配置;Secret 内容无法查看,适合密码等敏感数据

网络

  • Overlay 网络 MTU 问题:在 AWS VPC(MTU 9001 大帧)以外的云环境,物理 MTU 通常为 1500。overlay 的 VXLAN 封装消耗约 50 字节,若不将 overlay MTU 设为 1450 或更低,会导致大数据包丢失或性能下降,表现为 HTTP 请求超时但 ping 正常
  • 避免在生产环境使用 --network host:失去网络隔离,容器内的端口冲突会影响宿主机,且安全风险高
  • 容器内 DNS 解析失败排查:检查容器的 /etc/resolv.conf,确认 nameserver 是 127.0.0.11(Docker 内置 DNS);overlay 网络需要 UDP 7946 和 4789 端口畅通

文件系统与存储

  • read_only: true 后应用写入 /tmp 会报错:必须同时配置 tmpfs 挂载,Compose 写法:
    read_only: true
    tmpfs:
      - /tmp:size=64m
      - /var/run:size=8m
    
  • 多容器写同一 NFS 卷的并发问题:NFS 的 POSIX 锁行为与本地文件系统不同,多个容器并发写入同一文件会产生竞态条件,需要应用层协调(如分布式锁)或改用支持并发写的分布式存储(如 CephFS)
  • bind mount 在 Docker Desktop(macOS / Windows)上比 Linux 慢很多:原因是 macOS/Windows 通过 gRPC-FUSE 或 VirtioFS 进行文件系统代理。开发环境可以使用 devcontainer 的 Volume Mount(数据在 Linux VM 内,不过宿主机 bind),或使用 Mutagen 同步

应用与 JVM

  • JVM 在容器内不感知 cgroup 限制的问题:Java 8u191 之前,JVM 根据宿主机物理内存(而非容器 limits)分配堆内存,导致在 2GB 内存限制的容器内 JVM 尝试分配 8GB 堆,触发 OOM kill。Java 10+ 默认启用 -XX:+UseContainerSupport,Java 8u191+ 需要手动添加
  • Node.js 内存限制:默认堆上限约 1.5GB(取决于 V8 版本),在大内存容器内运行大型应用时需要设置 --max-old-space-size
  • 容器内不要运行多个主要进程:每个容器应只有一个主进程,辅助进程(日志收集、metrics exporter)应作为独立 sidecar 容器

Compose 版本兼容

  • Compose v2(docker compose,内置于 Docker Desktop 和新版 Docker CLI)与 v1(docker-compose,独立 Python 包)存在行为差异:v2 默认开启 BuildKit;健康检查 depends_on 条件(service_healthy)在 v1 中需要 docker-compose >= 1.29.0;部分网络配置选项在 v2 中的优先级不同
  • Compose 文件的 version 字段在 Compose v2 中已被标记为废弃(deprecated),新文件可以不写此字段

安全

  • 不要在 Dockerfile 中 RUN apt-get install 后不清理缓存:每个 RUN 命令会创建一层,若在同一层安装并清理缓存(用 && 连接),则清理有效;若分开写则会保留缓存层
    # 正确
    RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
    
    # 错误(清理无效,缓存层已固化)
    RUN apt-get update && apt-get install -y curl
    RUN rm -rf /var/lib/apt/lists/*
    
  • 定期更新基础镜像:即使应用代码未变化,基础镜像中的 OS 包可能存在已知漏洞,应在 CI 中定期重建镜像

最佳实践

多阶段构建彻底分离编译与运行环境:在构建阶段安装所有编译依赖,最终镜像只 COPY --from=builder 产物,运行镜像无编译工具链,减少攻击面和体积。

--mount=type=cache 加速 CI 层缓存:BuildKit 的挂载缓存可跨构建保留包管理器缓存,不会污染镜像层:

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

生产镜像固定 digest 不用 tagFROM python:3.12-slim@sha256:abc123... 防止基础镜像在 CI 与生产环境间静默变更,tag 可变而 digest 不可变。

docker buildx bake 统一多平台构建配置:将 linux/amd64,linux/arm64 目标写入 docker-bake.hcl,一条命令推送多架构 manifest,替代多次手动 buildx build --platform

Seccomp 与 AppArmor 配置最小权限:生产容器显式加载定制 Seccomp 白名单(--security-opt seccomp=policy.json),禁止容器调用不需要的 syscall,降低容器逃逸风险。

镜像扫描集成 CI 门禁:在推送步骤后加 trivy image --exit-code 1 --severity HIGH,CRITICAL 扫描,CVSS ≥ 7 的漏洞阻断流水线,配合基础镜像定期重建实现持续安全更新。


常见陷阱

陷阱:多阶段构建中 COPY 路径错误导致文件缺失

现象: docker run 时报 No such file or directory,容器内找不到编译产物。
原因: COPY --from=builder 的源路径与构建阶段实际输出路径不一致,或忘记指定阶段名(默认从最后一个阶段拷贝,实际需要中间阶段)。
解决: 给每个构建阶段显式命名 AS builder,拷贝时确认路径;可先 docker build --target builder -t debug . 进入中间阶段调试。

陷阱:BuildKit 缓存 mount 在非 BuildKit 环境失效

现象: 本地用 --mount=type=cache 正常,CI 环境构建报 unknown flag: --mount
原因: 旧版 Docker 或未启用 BuildKit(DOCKER_BUILDKIT=1)的环境不支持 RUN --mount 语法。
解决: CI 环境设置 DOCKER_BUILDKIT=1(或 Docker 23+ 已默认开启);在 docker buildx build 命令中使用 --builder 指定支持 BuildKit 的驱动。

陷阱:镜像层缓存因 COPY 顺序不当频繁失效

现象: 每次构建都重新安装依赖,即使依赖文件没有变化。
原因: COPY . .pip install 之前执行,任何源码变化都使安装层缓存失效。
解决: 先只拷贝依赖声明文件,安装完成后再拷贝源码:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

参见

Docker初级指南
Docker中级指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog