学会 Docker 不在于记住所有命令,而在于把应用、运行时依赖和启动方式封装成一个可重复交付的镜像。开发、测试和生产应运行同一份镜像,只通过配置、数据卷和网络环境改变行为。
本文以一个 Go HTTP 服务为例,走完“构建镜像 -> 本地运行 -> 管理状态 -> 排障 -> 上线检查”的主路径。它不是 Docker 命令大全;每一节都对应服务交付中需要做出的一个决策。
1. 先建立三个对象的边界
| 对象 | 含义 | 应如何对待 |
|---|---|---|
| Dockerfile | 构建规则 | 纳入版本控制,像代码一样评审。 |
| 镜像 | 不可变的运行制品 | 构建一次,在不同环境复用并按版本或 digest 追踪。 |
| 容器 | 镜像的一次运行实例 | 可以停止、删除和重建;不要把重要状态只留在容器可写层。 |
容器不是虚拟机。一个容器通常只承载一个明确的进程职责,日志写到标准输出,配置由环境传入,持久化数据放到外部存储或挂载卷中。
2. 从 Dockerfile 构建可交付镜像
2.1 先定义运行契约
写 Dockerfile 前应回答:进程如何启动、监听哪个端口、读取哪些环境变量、是否需要写临时文件、数据保存在哪里、退出时如何停止。没有这些约束,镜像即使能启动,也难以稳定运行。
以下示例假设 Go 服务入口为 ./cmd/api,最终监听 8080:
# syntax=docker/dockerfile:1
FROM golang:1.24 AS build
WORKDIR /src
# 先复制依赖描述文件,提升依赖层缓存命中率。
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath -ldflags="-s -w" \
-o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
多阶段构建把编译器和源码留在构建阶段,只将二进制文件复制进运行镜像。ENTRYPOINT 固定启动程序;如果应用需要默认参数,可用 CMD 作为可覆盖参数:
ENTRYPOINT ["/app"]
CMD ["--port=8080"]
不要把凭据、私有密钥或环境配置 COPY 到镜像中。构建阶段需要私有依赖时,应使用受控的构建密钥或 CI 凭据,并确保它们不会进入最终镜像层。
2.2 用 .dockerignore 控制构建上下文
构建上下文会被发送给 Docker daemon。无关文件会拖慢构建,也可能意外进入镜像:
.git
.env
tmp/
coverage/
bin/
*.log
README.md
.dockerignore 是安全和性能边界,不是可有可无的优化。提交前应确认所需的源码、配置模板和生成文件没有被错误排除。
2.3 构建并检查制品
docker build -t example/api:2026-08-02 .
docker image inspect example/api:2026-08-02
镜像标签应对应可追踪版本,例如 Git 提交、发布版本或构建号。生产部署更适合记录镜像 digest,而不是依赖可变的 latest 标签。
3. 正确启动一个容器
本地运行时,把配置、端口和资源边界显式写出来:
docker run --rm \
--name api \
-p 8080:8080 \
--env-file .env.local \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--memory 512m \
--cpus 1 \
example/api:2026-08-02
这些参数并非每个服务都必须使用,但它们表达了几个重要事实:
-p 8080:8080将宿主机端口映射给容器端口;EXPOSE仅是文档元数据,不会自动开放端口。--env-file让配置在镜像外部管理;实际生产环境应由部署平台或密钥系统提供敏感值。--read-only能更早发现应用把状态错误写入容器文件系统的行为。- 内存和 CPU 上限应通过压测确定,而不是照抄示例数值。
容器停止不等于删除,删除容器会清除它的可写层。因此容器文件系统只适合临时状态。
4. 管理数据与网络
4.1 持久化数据使用外部存储或 volume
数据库、上传文件和需要跨重建保存的数据,应使用托管存储、对象存储或 Docker volume:
docker volume create postgres-data
docker run --name db \
--mount type=volume,src=postgres-data,dst=/var/lib/postgresql/data \
postgres:17
volume 由 Docker 管理,适合容器运行时数据。绑定挂载(bind mount)直接映射宿主机目录,更适合本地开发、配置文件或需要由宿主机工具读取的文件。绑定挂载依赖宿主机路径和权限,不应在生产环境中不加约束地使用。
4.2 服务间通信使用自定义网络
为一组相关服务创建用户定义的 bridge 网络:
docker network create app-net
docker run -d --name db --network app-net postgres:17
docker run --rm --network app-net example/api:2026-08-02
同一网络中的容器可通过容器名或服务名访问,例如 API 连接 db:5432。不要把数据库端口映射到宿主机,只是为了让同网络的应用访问它;端口映射是暴露给宿主机或外部网络的明确决定。
5. 运行、观测与排障
服务应把结构化日志写到标准输出和标准错误,让平台统一采集:
docker ps
docker logs --follow api
docker inspect api
排障时先确认容器是否运行、退出码是什么、日志是否显示配置或依赖连接失败,再检查端口、网络和资源限制。docker exec 适用于运行中容器的临时诊断:
docker exec -it api /bin/sh
但不要假设生产镜像一定带 shell。精简镜像和 distroless 镜像通常没有 shell,这是正常的安全取舍;应通过日志、指标、临时诊断容器或平台调试能力排障,而不是为了 exec 把调试工具长期放进业务镜像。
应用还应正确处理 SIGTERM:停止接收新请求、在截止时间内完成正在处理的请求、关闭连接后退出。容器编排平台通常先发送 SIGTERM,超时后才强制终止。
6. 镜像安全与可维护性
以下实践比“镜像越小越好”更重要:
- 使用受维护的基础镜像,并在发布时记录具体版本或 digest。
- 多阶段构建,只复制运行所需文件。
- 使用非 root 用户运行,避免挂载 Docker socket。
- 不在镜像层、环境文件或构建日志中留下密钥。
- 在 CI 中执行依赖和镜像漏洞扫描,并设定修复时限。
- 为镜像建立可追溯关系:源码提交、构建记录、镜像 digest 和部署版本能够互相对应。
ADD 支持额外的自动解压和远程 URL 行为。大多数复制场景应使用语义更明确的 COPY,避免构建过程出现隐式行为。
7. 上线前检查清单
- Dockerfile 能从干净环境构建,不依赖开发机本地文件。
- 镜像中不含密钥、
.env、构建缓存和不必要的调试工具。 - 进程以前台方式运行,日志输出到标准输出。
- 配置由环境或密钥系统注入,端口和依赖地址可配置。
- 重要数据不写入容器可写层,挂载策略已备份和演练。
- 应用能处理
SIGTERM并通过健康检查反映可用状态。 - 已验证资源限制、网络访问范围、镜像扫描和回滚方式。
8. 下一步
当单容器运行流程稳定后,再使用 Docker Compose 管理本地多服务依赖,或由 Kubernetes、Nomad 等平台负责生产编排。无论使用哪种平台,镜像的运行契约、状态边界、配置注入和可观测性原则不变。