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 核实)
目录
- Docker高级指南 · 镜像构建高级技巧
- Docker高级指南 · Docker 安全加固
- Docker高级指南 · Docker Swarm 集群编排
- Docker高级指南 · 生产环境部署方案
- Docker高级指南 · CI/CD 集成
- Docker高级指南 · 容器内应用最佳实践
- Docker高级指南 · 性能调优
- Docker高级指南 · Docker 网络高级
- Docker高级指南 · 日常运维命令
- 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 的处理与其他进程不同:
- 未注册信号处理器的信号会被忽略(包括 SIGTERM),导致
docker stop无法停止容器 - 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
踩坑与最佳实践清单
镜像与构建
- 生产环境禁止使用
latesttag,任何时候应使用固定版本(如1.2.3或sha256:...),避免因镜像更新引入不一致行为 - 镜像层数限制:Union FS 的实际限制在不同驱动下各异(overlay2 约 128 层),复杂多阶段构建应保持最终镜像层数在合理范围内(通常不超过 20-30 层)
docker history会泄露ENV、ARG、RUN中的所有值,构建时的敏感信息必须使用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 不用 tag:FROM 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 . .