从零到专业的 Docker:100 个示例,涵盖 Dockerfile、Compose 与 CI/CD

从零到专业的 Docker:100 个示例,涵盖 Dockerfile、Compose 与 CI/CD

Docker 把应用及其所需的一切打包进容器,使同一套软件在笔记本、服务器和云端都以相同方式运行。本文通过 100 个示例,带你从第一个容器走到面向生产的配置。你将学习 Docker CLI、镜像与仓库、使用多阶段构建编写 Dockerfile、数据卷与网络、用于多容器应用的 Docker Compose、配置与机密、资源限制与健康检查、容器安全、调试、CI/CD 流水线,以及对 Kubernetes 和 Swarm 的初步认识。需要坦诚说明:准备本文所用的计算机上没有安装 Docker,因此这 100 个示例都没有在这里实际运行。每条命令和文件都按照 Docker 官方语法编写,并通过阅读进行了检查。在依赖它们之前,请你自己逐一运行。

需要准备的东西

Windows 或 macOS 上的 Docker Desktop,或 Linux 上的 Docker Engine 加 Docker Compose 插件(能运行 docker compose version 即可)。安装了 Docker 扩展的 Visual Studio Code 等编辑器会更方便。入门只需基本的命令行知识。部分示例使用示例 Python 或 Node.js 项目,部分示例使用私有仓库,因此请使用 registry.example.com 之类的占位符,并且不要在命令中粘贴真实密码。

如何使用示例

bash 示例使用 Docker CLI 和 Docker Compose。Dockerfile 示例保存为项目文件夹中名为 Dockerfile 的文件。YAML 示例保存为 compose.yaml 或正文中指定的 Kubernetes 文件。

知识点 1: Docker 入门

容器是一个隔离的进程,拥有自己的文件系统和网络,由镜像创建。学习核心概念,检查安装,并运行你的第一个容器。

镜像、容器与仓库

镜像是由多个层组成的只读模板。容器是镜像的运行实例。Docker Hub 或 Azure Container Registry 等仓库保存镜像,供其他机器下载。

容器的优势

容器把应用与其库打包在一起,因此在每台机器上行为一致。它们几秒钟即可启动,并与宿主机共享内核,所以比完整的虚拟机占用的内存少得多。

检查安装

运行 docker version 可同时查看客户端和服务端信息。如果没有 server 那一行,说明 Docker Desktop 或 Docker 服务尚未运行,请先启动它,再运行任何容器命令。

示意图

Docker 如何把应用从 Dockerfile 送到正在运行的容器:Dockerfile 被构建为镜像,镜像推送到仓库,主机拉取后以容器方式运行
Docker 如何把应用从 Dockerfile 送到正在运行的容器:Dockerfile 被构建为镜像,镜像推送到仓库,主机拉取后以容器方式运行

示例 1: 确认已安装 Docker

确认客户端能够连接到 Docker 引擎。

docker --version
docker info --format "server: {{.ServerVersion}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 2: 运行 hello-world

用一条命令下载并运行你的第一个容器。

docker run hello-world

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 3: 拉取镜像

下载镜像,但不启动容器。

docker pull nginx:1.27

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 4: 列出本地镜像

查看这台计算机上保存了哪些镜像。

docker images
docker image ls --filter "reference=nginx"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 5: 检查镜像

读取镜像的体系结构和操作系统。

docker image inspect nginx:1.27 --format "arch={{.Architecture}} os={{.Os}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 6: 查看磁盘占用

查看镜像、容器和卷占用了多少空间。

docker system df

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 2: 运行与管理容器

docker run 命令从镜像创建容器。你将发布端口、读取日志、在容器内执行命令,并清理不再需要的容器。

分离模式与交互模式

-d 让容器在后台运行,-it 则连接终端。--rm 会在容器停止时将其删除,适合短时间的实验。

端口与名称

-p 宿主机端口:容器端口 用于发布端口,因此 8080:80 会把宿主机的 8080 端口转发到容器的 80 端口。--name 为容器指定易读的名称,便于之后的命令引用。

生命周期与清理

已停止的容器会保留其数据,直到你删除它。用 docker ps -a 查看,用 docker rm 删除。在经常构建的机器上,应定期清理。

示例 7: 在后台运行容器

以分离模式启动容器并列出它。

docker run -d --name web nginx:1.27
docker ps

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 8: 发布端口

从宿主机访问容器内的 Web 服务器。

docker run -d --name web-8080 -p 8080:80 nginx:1.27
curl http://localhost:8080

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 9: 启动临时 shell

使用 --rm,退出时自动删除容器。

docker run -it --rm alpine:3.20 sh

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 10: 列出容器

显示正在运行和已停止的容器。

docker ps
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 11: 读取容器日志

显示最后几行,或实时跟踪输出。

docker logs --tail 50 web
docker logs -f web

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 12: 在容器中运行命令

在运行中的容器里打开 shell 或执行一条命令。

docker exec -it web sh
docker exec web cat /etc/nginx/nginx.conf

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 13: 停止、启动和重启

控制容器的生命周期。

docker stop web
docker start web
docker restart web

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 14: 删除容器

删除不再需要的容器。

docker rm -f web web-8080
docker container prune -f

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 3: 镜像与仓库

镜像有名称、标签和摘要。标签可以改变,摘要不能改变,因此要准确地标记、共享和复现软件,两者都需要。

标签与摘要

latest 只是一个标签,每当发布者更新时它都会移动。以 sha256 开头的长哈希摘要能唯一标识某一个镜像,这是让部署可复现的方法。

推送到仓库

登录后,用完整的仓库地址为镜像打标签并推送。其他机器就可以按名称拉取同一镜像。绝不要推送包含机密的镜像。

查看镜像历史

docker history 显示每一层、生成该层的指令及其大小。它可以帮你找到过大的层,并确认密码从未被写入镜像。

示例 15: 为镜像打标签

给镜像起一个你自己的名称和版本。

docker tag nginx:1.27 mycompany/nginx:1.0.0
docker images mycompany/nginx

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 16: 推送镜像到仓库

上传镜像,让其他机器可以拉取。

docker login registry.example.com
docker tag mycompany/nginx:1.0.0 registry.example.com/team/nginx:1.0.0
docker push registry.example.com/team/nginx:1.0.0

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 17: 推送到 Azure Container Registry

用 Azure CLI 登录,然后推送到你的仓库。

az acr login --name <registry-name>
docker tag mycompany/nginx:1.0.0 <registry-name>.azurecr.io/nginx:1.0.0
docker push <registry-name>.azurecr.io/nginx:1.0.0

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 18: 运行本地仓库

在自己的计算机上启动私有仓库用于测试。

docker run -d -p 5000:5000 --restart unless-stopped --name registry registry:2
docker tag nginx:1.27 localhost:5000/nginx:1.27
docker push localhost:5000/nginx:1.27

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 19: 用摘要固定镜像

摘要是不会改变的指纹,不像标签那样会变。

docker pull nginx:1.27
docker image inspect nginx:1.27 --format "{{index .RepoDigests 0}}"
docker run -d nginx@sha256:<digest-from-the-line-above>

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 20: 查看镜像的层

看每条指令生成了哪一层,以及大小。

docker history nginx:1.27
docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" nginx:1.27

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 4: 编写 Dockerfile

Dockerfile 是一份文本配方。每条指令都会生成一层,因此指令的顺序和内容决定了镜像大小、重新构建的速度,以及容器启动时运行的内容。

核心指令

FROM 选择基础镜像。COPY 和 RUN 添加文件和命令。WORKDIR 为后续指令设置目录。CMD 和 ENTRYPOINT 定义容器运行的内容。

ARG 与 ENV 的区别

ARG 只在构建镜像期间存在。ENV 会保留在镜像中,运行中的容器可以看到它。构建时的选项用 ARG,程序读取的设置用 ENV。

构建与打标签

docker build -t 名称:标签 . 会把目录发送给构建器。只有 Dockerfile 复制的文件才会被使用,因此应使用 .dockerignore 文件保持构建上下文精简。

示例 21: 最小的 Dockerfile

最简单的可用配方:基础镜像加一条命令。

FROM alpine:3.20
CMD ["echo", "Hello from a container"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 22: 构建并运行镜像

把 Dockerfile 变成镜像并启动。

docker build -t hello-image:1.0 .
docker run --rm hello-image:1.0

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 23: Python 应用的 Dockerfile

安装依赖,并把代码复制到精简镜像中。

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 24: Node.js 应用的 Dockerfile

安装生产依赖并暴露端口。

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 25: ARG 与 ENV

设置构建时的值和运行时的环境变量。

FROM alpine:3.20
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ENV APP_PORT=8080
CMD ["sh", "-c", "echo version=$APP_VERSION port=$APP_PORT"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 26: 传入构建参数

无需修改文件,在构建时更改版本。

docker build --build-arg APP_VERSION=1.4.2 -t vars-image:1.4.2 .
docker run --rm -e APP_PORT=9000 vars-image:1.4.2

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 27: WORKDIR 与 COPY

设置工作目录,并把文件复制进镜像。

FROM alpine:3.20
WORKDIR /srv/site
COPY ./site/ ./
# ADD can unpack archives, but COPY is clearer for plain files
RUN ls -la /srv/site

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 28: ENTRYPOINT 与 CMD

选择固定的程序及其默认参数。

FROM alpine:3.20
RUN apk add --no-cache curl
ENTRYPOINT ["curl", "-s"]
CMD ["https://example.com"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 5: 镜像最佳实践

好的镜像体积小、可复现、构建快、运行安全。多阶段构建、层的排列顺序、固定版本、非 root 用户和健康检查涵盖了大部分实践。

多阶段构建

构建阶段包含编译器和工具,最终阶段只复制结果。最终镜像因此很小,也不包含攻击者可能利用的源代码或构建工具。

层缓存

当指令及其输入未变化时,Docker 会复用该层;一旦出现第一处变化,其后的所有层都会重新构建。应先复制依赖文件并安装,再复制源代码。

用户、版本与检查

以数字形式的非 root 用户运行,固定基础镜像版本,添加所有者和版本标签,并使用 HEALTHCHECK,让 Docker 能够报告应用是否真正正常工作。

示例 29: 用 .dockerignore 排除文件

让密钥、Git 数据和依赖不进入构建。

cat > .dockerignore <<'EOF'
.git
node_modules
*.log
.env
EOF
docker build -t app:1.0 .

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 30: Go 的多阶段构建

在大镜像中编译,只交付编译好的程序。

FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod ./
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .

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

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 31: 前端的多阶段构建

构建静态资源,再由 Nginx 提供服务。

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 32: 以非 root 用户运行

应用被攻击时降低损失。

FROM python:3.12-slim
RUN useradd --create-home --uid 10001 appuser
WORKDIR /home/appuser/app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 33: 固定基础镜像版本

避免 latest 标签变化带来的意外。

# Avoid "latest": it changes without warning
FROM python:3.12.5-slim-bookworm
# Pin system packages when the build must be reproducible
RUN apt-get update && apt-get install -y --no-install-recommends curl=8.* \
 && rm -rf /var/lib/apt/lists/*

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 34: 为缓存安排层的顺序

先复制依赖文件,让构建保持快速。

FROM node:20-alpine
WORKDIR /app
# Dependencies change less often than source code, so copy them first
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 35: 添加镜像标签

在镜像中记录标题、版本和负责人。

FROM alpine:3.20
LABEL org.opencontainers.image.title="sample-api" \
      org.opencontainers.image.version="1.4.2" \
      org.opencontainers.image.maintainer="[email protected]"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 36: HEALTHCHECK

让 Docker 知道应用是否正常工作。

FROM nginx:1.27-alpine
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost/ || exit 1

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 6: 数据卷与持久化数据

容器是可以丢弃的,但数据库和上传的文件不能丢弃。数据卷和绑定挂载把数据保存在容器可写层之外,因此可以在重启和升级后保留。

命名卷

命名卷由 Docker 管理,存放在容器之外。适合用于数据库:删除容器后数据仍然保留,并且可以用辅助容器进行备份。

绑定挂载

绑定挂载把你电脑上的目录映射到容器中。开发时很方便,代码修改会立即生效,但容器会直接访问宿主机文件,因此如果只需要读取,请使用 :ro。

备份与临时数据

通过运行一个挂载该卷并写出 tar 文件的小容器来备份数据卷。对于应保存在内存中、并在容器停止时消失的临时文件,使用 tmpfs。

示例 37: 创建命名卷

保存即使容器删除也必须保留的数据。

docker volume create app-data
docker volume ls
docker volume inspect app-data

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 38: 数据库使用命名卷

容器重启后仍能安全保存 PostgreSQL 数据。

docker run -d --name db -e POSTGRES_PASSWORD=<strong-password> \
  -v app-data:/var/lib/postgresql/data postgres:16
docker exec db psql -U postgres -c "select 1"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 39: 绑定挂载宿主机文件夹

开发时把你电脑上的文件夹共享给容器。

docker run --rm -it -v "$(pwd)/site:/usr/share/nginx/html:ro" -p 8081:80 nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 40: 只读挂载

让容器可以读取配置,但不能修改。

docker run --rm -v /srv/config:/etc/app:ro alpine:3.20 ls -l /etc/app

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 41: 备份卷

用辅助容器把卷中的数据打包成 tar 文件。

docker run --rm -v app-data:/data -v "$(pwd):/backup" alpine:3.20 \
  tar czf /backup/app-data-backup.tgz -C /data .

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 42: 恢复卷

把备份的数据放回新的卷中。

docker volume create app-data-restored
docker run --rm -v app-data-restored:/data -v "$(pwd):/backup" alpine:3.20 \
  tar xzf /backup/app-data-backup.tgz -C /data

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 43: 用 tmpfs 保存临时数据

临时文件保存在内存中,停止时即删除。

docker run --rm --tmpfs /tmp:rw,size=64m alpine:3.20 sh -c "echo scratch > /tmp/x && cat /tmp/x"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 7: 容器网络

Docker 为容器提供独立的网络。用户自定义网络让容器可以通过名称互相发现,而端口发布则决定哪些服务可以从外部访问。

桥接网络与用户自定义网络

默认的桥接网络只为容器分配 IP 地址。用户自定义的桥接网络还提供 DNS,因此一个容器可以通过名称访问另一个容器,例如 http://api 或 redis://cache。

安全地发布端口

默认情况下,端口会发布到所有网络接口。绑定到 127.0.0.1 可以让数据库只对你的电脑可见。EXPOSE 只是文档说明,并不会真正开放端口。

多个网络

一个容器可以加入多个网络。把数据库放在后端网络,把 Web 代理放在前端网络,这样只有需要数据库的服务才能访问它。

示例 44: 查看并检查网络

查看 Docker 创建的网络及其地址范围。

docker network ls
docker network inspect bridge --format "{{range .IPAM.Config}}{{.Subnet}}{{end}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 45: 创建自定义网络

让相关容器拥有自己的私有网络。

docker network create app-net
docker run -d --name api --network app-net nginx:1.27
docker run --rm --network app-net alpine:3.20 wget -qO- http://api

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 46: 通过名称找到容器

在网络中把容器名当作主机名使用。

docker run -d --name cache --network app-net redis:7-alpine
docker run --rm --network app-net redis:7-alpine redis-cli -h cache ping

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 47: 把容器连接到第二个网络

让一个服务同时访问两个相互隔离的网络。

docker network create back-net
docker network connect back-net api
docker inspect api --format "{{json .NetworkSettings.Networks}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 48: 只在一个接口上发布

让数据库只能从这台计算机访问。

docker run -d -p 127.0.0.1:5432:5432 --name pg -e POSTGRES_PASSWORD=<strong-password> postgres:16

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 49: EXPOSE 与 publish 的区别

EXPOSE 只是文档说明,-p 才能让宿主机访问。

FROM node:20-alpine
# EXPOSE only documents the port; it does not open it to the host
EXPOSE 3000
CMD ["node", "server.js"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 50: 使用宿主机网络

在 Linux 的特殊场景下共享宿主机的网络栈。

docker run --rm --network host alpine:3.20 sh -c "ip addr show eth0 | head -3"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 8: Docker Compose 基础

Compose 用一个 YAML 文件描述多容器应用。一条命令即可启动、停止并查看项目中的每个服务、网络和数据卷。

compose 文件

services 键列出每个容器。每个服务都有 image 或 build 部分,以及 ports、environment、volumes 和 networks。Compose 会为项目自动创建共享网络。

常用命令

docker compose up -d 启动项目,docker compose ps 显示状态,docker compose logs -f 持续跟踪输出,docker compose down 停止并删除容器及默认网络。

覆盖文件

compose.yaml 保存共享设置,compose.override.yaml 会被自动合并。把本地修改放在覆盖文件中,使主文件在每个环境中保持一致。

示例 51: 最小的 Compose 文件

用一个 YAML 文件描述一个 Web 服务。

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 52: 启动和停止 Compose 项目

一次启动或关闭整个多容器应用。

docker compose up -d
docker compose ps
docker compose down

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 53: 在 Compose 中构建镜像

让 Compose 根据 Dockerfile 构建镜像。

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "3000:3000"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 54: Compose 中的环境变量

在文件中为每个服务设置参数。

services:
  api:
    build: ./api
    environment:
      APP_ENV: development
      LOG_LEVEL: info

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 55: 等待其他服务

用 depends_on 控制启动顺序。

services:
  api:
    build: ./api
    depends_on:
      - db
  db:
    image: postgres:16

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 56: Compose 中的命名卷

项目重启后仍保留数据库文件。

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: <strong-password>
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 57: 日志、exec 与一次性命令

在项目内运行测试或工具。

docker compose logs -f api
docker compose exec db psql -U postgres
docker compose run --rm api npm test

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 58: 开发用的覆盖文件

把生产配置与本地修改分开管理。

# compose.yaml holds the shared settings
services:
  api:
    build: ./api
# compose.override.yaml is merged automatically on "docker compose up"
services:
  api:
    volumes:
      - ./api:/app
    environment:
      DEBUG: "1"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 9: 面向真实应用的 Compose

真实应用包括 Web 层、数据库、缓存和代理。Compose 把它们连接起来,等待它们就绪,并允许你对需要更多容量的部分进行扩展。

Web、数据库与缓存

让每个服务只承担一项职责,把数据库文件存放在命名卷中,并通过名称访问每个服务。连接字符串应通过环境变量传入,而不是硬编码主机名。

启动顺序与健康状态

depends_on 只等待容器启动。应为数据库添加 healthcheck,并使用 condition: service_healthy,使应用只在数据库接受连接后才启动。

扩展与隔离

docker compose up --scale 可以改变无状态服务的副本数量。拆分网络,让公开代理可以访问 API,但不能直接访问数据库。

示例 59: 可选服务的 profile

只在需要时启动管理工具。

services:
  api:
    build: ./api
  pgadmin:
    image: dpage/pgadmin4
    profiles: ["tools"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 60: 启动某个 profile

从命令行启用可选服务。

docker compose up -d
docker compose --profile tools up -d

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 61: 带 PostgreSQL 的 Web 应用

包含数据库和持久化数据的完整应用。

services:
  web:
    build: ./web
    ports:
      - "8000:8000"
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: shop
      POSTGRES_USER: shop
      POSTGRES_PASSWORD: <strong-password>
    volumes:
      - pg-data:/var/lib/postgresql/data
volumes:
  pg-data:

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 62: 添加 Redis 作为缓存

通过服务名连接高速键值存储。

services:
  web:
    build: ./web
    environment:
      REDIS_URL: redis://cache:6379
  cache:
    image: redis:7-alpine
    command: ["redis-server", "--save", "", "--appendonly", "no"]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 63: 用 Nginx 做反向代理

通过一个入口把 Web 流量转发给应用。

services:
  proxy:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
  app:
    build: ./app

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 64: 等待数据库健康

数据库可以接受查询后才启动应用。

services:
  api:
    build: ./api
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: <strong-password>
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 10: 配置与机密

配置因环境而异,而机密绝不能进入镜像或 Git。使用环境变量、env 文件和机密文件,并在程序中安全地读取它们。

环境变量与 .env 文件

Compose 会读取 .env 文件,用于替换 compose 文件中的变量。把 .env 加入 .gitignore,并提交带占位值的 .env.example 文件,让其他开发者知道需要设置什么。

以文件形式提供机密

基于文件的机密会挂载到容器内的 /run/secrets 下。从文件读取可以使其值不出现在 docker inspect 的输出或进程列表中,而环境变量可能会泄露到那里。

绝不把机密烘焙进镜像

复制到某一层中的值,即使后续步骤将其删除,仍会保留在镜像历史中。应在运行时传入机密,用 docker history 检查历史,并轮换任何曾被推送的机密。

示例 65: 扩展服务

运行多个工作进程副本。

docker compose up -d --scale worker=3
docker compose ps worker

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 66: 前端与后端网络

让数据库不暴露在公共网络上。

services:
  proxy:
    image: nginx:1.27-alpine
    networks: [front]
    ports: ["80:80"]
  api:
    build: ./api
    networks: [front, back]
  db:
    image: postgres:16
    networks: [back]
networks:
  front:
  back:

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 67: 从 .env 加载变量

把本地的值放在 Compose 文件之外。

cat > .env <<'EOF'
POSTGRES_PASSWORD=<strong-password>
APP_PORT=8000
EOF
docker compose config

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 68: 使用 env_file

从文件中加载整组设置。

services:
  api:
    build: ./api
    env_file:
      - ./config/api.env

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 69: Compose 的 secret 文件

把密码交给服务,而不写入镜像。

services:
  api:
    build: ./api
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 70: 在 Python 中读取配置

使用默认值,并把环境变量字符串转换为数字。

import os

db_host = os.environ.get("DB_HOST", "localhost")
db_port = int(os.environ.get("DB_PORT", "5432"))
print(f"connect to {db_host}:{db_port}")

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 71: 在 Python 中读取 secret 文件

从容器内的 secret 路径读取密码。

from pathlib import Path

secret_file = Path("/run/secrets/db_password")
password = secret_file.read_text(encoding="utf-8").strip()
print("password length:", len(password))

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 72: 运行时设置

为一次运行覆盖参数,并检查镜像历史。

docker run --rm -e LOG_LEVEL=debug -e DB_HOST=db.internal app:1.0
docker history --no-trunc app:1.0 | grep -i password || echo "no password in the layers"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 11: 资源、健康与日志

如果不加限制,容器可以占用宿主机的全部内存和 CPU。限制、重启策略、健康检查和日志轮转能让服务保持稳定,并防止磁盘被占满。

内存与 CPU 限制

--memory 会停止使用过多内存的容器,--cpus 限制其可用的处理器时间份额。应根据 docker stats 测得的实际用量设置限制,而不是凭猜测。

重启策略

unless-stopped 会在崩溃或宿主机重启后重启容器,但不会在你手动停止后重启。它适用于长期运行的服务,而且应修复反复崩溃的根本原因,而不是依赖重启。

日志与健康

Docker 把容器输出保存在日志驱动中。用 max-size 和 max-file 限制 json-file 的大小。健康状态显示 starting、healthy 或 unhealthy,负载均衡器和编排器都可以利用它。

示例 73: 内存限制

防止失控进程占满内存。

docker run -d --name api-mem --memory 256m --memory-swap 256m nginx:1.27-alpine
docker stats --no-stream api-mem

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 74: CPU 限制

让容器只能使用半个 CPU 核心。

docker run -d --name api-cpu --cpus 0.5 nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 75: 重启策略

服务崩溃或机器重启后自动恢复。

docker run -d --name worker --restart unless-stopped app:1.0
docker inspect worker --format "{{.HostConfig.RestartPolicy.Name}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 76: 限制日志大小

防止日志文件占满磁盘。

docker run -d --name api-logs \
  --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 77: 读取健康状态

查看容器是健康、启动中还是不健康。

docker ps --format "table {{.Names}}\t{{.Status}}"
docker inspect --format "{{.State.Health.Status}}" api

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 78: 清理未使用的对象

安全地删除已停止的容器和悬空镜像。

docker container prune -f
docker image prune -f
docker volume ls -qf dangling=true

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 12: 容器安全

容器与宿主机共享内核,粗心的配置可能让攻击者获得超出预期的访问权限。最小权限、只读文件系统、扫描以及保持基础镜像更新,都能降低风险。

最小权限

以非 root 用户运行,用 --cap-drop ALL 去掉不需要的 Linux 能力,只添加确实需要的能力。使用 no-new-privileges 阻止进程获得更多权限。

只读文件系统

--read-only 阻止容器修改自己的文件。对于少数必须可写的目录(例如 /tmp),使用 tmpfs 挂载。

扫描与更新

用 Docker Scout 或其他扫描工具扫描镜像,定期重新构建以获取已修复的基础镜像,并在发现严重漏洞时让构建失败。除非有特定理由,否则不要使用 --privileged 运行容器。

示例 79: 去掉 Linux 能力

移除应用不需要的权限。

docker run -d --name locked --cap-drop ALL --cap-add NET_BIND_SERVICE -p 80:80 nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 80: 只读根文件系统

防止容器写入自身的系统文件。

docker run -d --name ro-app --read-only --tmpfs /tmp --tmpfs /var/run nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 81: 以数字用户运行

使用用户 ID,而不是依赖用户名。

docker run --rm --user 10001:10001 alpine:3.20 id

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 82: 阻止权限提升

防止进程获得更多权限。

docker run -d --name hardened --security-opt no-new-privileges:true nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 83: 扫描镜像漏洞

查找镜像内软件包中已知的 CVE。

docker scout cves nginx:1.27-alpine
docker scout quickview nginx:1.27-alpine

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 84: Compose 中的安全设置

在一个服务里组合所有加固选项。

services:
  api:
    build: ./api
    user: "10001:10001"
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    tmpfs: [/tmp]

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 13: 调试与故障排查

容器失败时,答案就在它的日志、inspect 数据、事件和进程中。遵循系统化的步骤,大多数问题几分钟内即可找到。

日志、事件与 inspect

先读 docker logs。docker events 显示 Docker 做了什么以及何时做的,docker inspect 显示退出码、环境变量、挂载和网络,便于把正在运行的容器与预期进行比较。

资源与进程检查

docker stats 显示每个容器的 CPU 和内存,docker top 列出进程。退出码 137 通常表示内核因内存占用过多而终止了进程,可以通过 OOMKilled 确认。

网络调试

使用 --network container:名称 在同一网络命名空间中运行工具镜像,然后在其中使用 curl、dig 和 ping。这样可以在不改变任何东西的情况下,测试容器真实看到的网络。

示例 85: 跟踪 Docker 事件

实时观察容器的启动、停止和失败。

docker events --filter type=container --since 10m

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 86: 查看资源使用

查看每个容器的 CPU 和内存。

docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 87: 列出容器内的进程

不打开 shell 也能看到内部正在运行什么。

docker top web

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 88: 从 inspect 读取单个字段

获取退出码,以及内核是否杀掉了进程。

docker inspect web --format "{{.State.ExitCode}} {{.State.OOMKilled}}"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 89: 在容器与宿主机之间复制文件

取出配置文件,或把修复放进容器。

docker cp web:/etc/nginx/nginx.conf ./nginx.conf
docker cp ./nginx.conf web:/etc/nginx/nginx.conf

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 90: 用网络工具镜像调试

共享容器的网络,并用 curl 和 dig 测试。

docker run --rm -it --network container:web nicolaka/netshoot
# inside the tools shell: curl -v http://localhost:80

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 14: 镜像的 CI/CD 流水线

每当代码变化时,流水线都会构建、测试、扫描并推送镜像。自动化让每个镜像都能追溯到某个提交,并使每次发布都保持一致。

在 CI 中构建与推送

流水线使用短期有效的凭据登录,构建镜像,运行测试,然后用包含提交号的标签推送。相同的步骤适用于 GitHub Actions 和 Azure Pipelines。

标签策略

用 Git 提交号为每个镜像打标签以便追溯,只有在发布获批时才移动版本标签。在生产环境中避免依赖 latest,因为它会隐藏正在运行的代码版本。

运行器维护

CI 运行器会积累层和卷。应按计划清理构建缓存,并设置缓存大小上限,防止构建机器磁盘被占满而出现不可预测的失败。

示例 91: 在 GitHub Actions 中构建并推送

每次推送到 main 时自动构建镜像。

name: build-image
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/example/app:${{ github.sha }}

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 92: 在 Azure Pipelines 中构建镜像

使用带服务连接的 Docker 任务。

trigger:
  - main
pool:
  vmImage: ubuntu-latest
steps:
  - task: Docker@2
    inputs:
      command: buildAndPush
      containerRegistry: my-acr-connection
      repository: sample-api
      tags: |
        $(Build.BuildId)

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 93: 用 Git 提交号给镜像打标签

把每个镜像追溯到确切的源代码。

SHA=$(git rev-parse --short HEAD)
docker build -t app:$SHA .
docker tag app:$SHA app:latest
echo "built app:$SHA"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 94: 清理 CI 运行器上的构建缓存

防止构建机器磁盘耗尽。

docker builder prune --filter until=72h -f
docker image prune -a --filter until=168h -f

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

知识点 15: 生产环境与编排

小型服务使用单台主机即可。当需要多台机器、自愈和滚动更新时,Kubernetes 或 Docker Swarm 等编排器会为你运行容器。

生产检查清单

固定镜像版本,设置内存限制,定义重启策略,添加健康检查,把端口绑定到预期的网络接口,把机密保存在镜像之外,并把日志集中存放。

Kubernetes 基础

Deployment 保持指定数量的副本运行并执行滚动更新。Service 为所有副本提供一个稳定的地址。同一个容器镜像无需修改即可同时在 Compose 和 Kubernetes 中运行。

Swarm 及其适用场景

Docker Swarm 以 stack 的形式使用类似 Compose 的文件,并在多台机器上运行服务。Kubernetes 生态更庞大,在托管云服务中很常见,因此应选择你的团队能够运维的工具。

示例 95: 生产环境的 Compose 设置

固定版本、限制内存、设置重启并检查健康。

services:
  api:
    image: registry.example.com/team/api:1.4.2
    restart: unless-stopped
    env_file: ./config/api.env
    ports:
      - "127.0.0.1:8000:8000"
    deploy:
      resources:
        limits:
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 3s
      retries: 3

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 96: Kubernetes Deployment

在集群中以三个副本运行同一镜像。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: registry.example.com/team/api:1.4.2
          ports:
            - containerPort: 8000
          resources:
            limits:
              memory: 512Mi
              cpu: "500m"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 97: Kubernetes Service

为所有副本提供集群内稳定的地址。

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8000
  type: ClusterIP

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 98: 部署 Docker Swarm 栈

在多台机器上运行同一组服务。

docker swarm init
docker stack deploy -c stack.yml shop
docker service ls

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 99: 用 Docker SDK 检查容器

在 Python 代码中读取容器状态。

import docker

client = docker.from_env()
for c in client.containers.list():
    state = c.attrs["State"].get("Health", {}).get("Status", "no healthcheck")
    print(c.name, state)

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

示例 100: 发布检查清单脚本

如果镜像以 root 运行或存在严重 CVE,则阻止发布。

#!/usr/bin/env bash
set -euo pipefail
IMAGE="$1"
docker image inspect "$IMAGE" > /dev/null
USER_ID=$(docker run --rm --entrypoint id "$IMAGE" -u)
[ "$USER_ID" != "0" ] || { echo "image runs as root"; exit 1; }
docker scout cves --exit-code --only-severity critical "$IMAGE"
echo "release checks passed for $IMAGE"

*此示例未在本机运行,因为准备本文的计算机没有安装 Docker。*

实验:构建、运行并发布一个小型 Web 应用

把本文的内容整合进一个项目:一个小型 Web 应用、使用多阶段构建的 Dockerfile、包含数据库和数据卷的 Compose 文件、健康检查,以及发布前检查。

1. 创建一个包含小型应用和 requirements.txt 文件的目录,然后编写一个 Dockerfile,以非 root 用户运行该应用并包含 HEALTHCHECK。

2. 添加 .dockerignore 文件,并用版本标签构建镜像,例如 docker build -t lab-web:1.0.0 .

3. 编写 compose.yaml,包含 Web 服务以及使用命名卷和 healthcheck 的 PostgreSQL 服务,然后用 docker compose up -d 启动。

4. 用 docker compose ps 检查健康状态,用 docker compose logs -f web 读取日志。

5. 对镜像运行 docker scout cves,通过更新基础镜像修复能修复的问题,然后重新构建。

6. 对你的镜像运行第 100 个示例中的发布检查清单,如果检查失败就停止发布。

7. 用 docker compose down 停止项目,只有在不再需要数据时才删除命名卷。

下一步

完成实验后,请阅读你所选运行时的官方 Docker 文档,在独立项目中练习,并在服务需要多台机器时深入学习一个编排器。把本文的示例保存在代码仓库中,这样每次 Docker 更新后都能重新运行。

总结

Docker 把应用变成可以在任何地方以相同方式运行的镜像。你学会了运行和管理容器,用标签和摘要固定镜像,使用多阶段构建编写高效的 Dockerfile,把数据保存在数据卷中,用网络连接服务,用 Compose 描述整个应用,安全地处理配置与机密,限制资源,加固容器,进行系统化调试,用 CI/CD 自动化构建,并为生产编排做好准备。