从零到专业的 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 服务尚未运行,请先启动它,再运行任何容器命令。
示意图
示例 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 自动化构建,并为生产编排做好准备。