编写 Dockerfile 与镜像最佳实践

用一份 Dockerfile 把应用打包成可复现的镜像,并通过分层、缓存与多阶段构建把体积和风险降到最低。

Dockerfile 是一份纯文本的「构建脚本」,docker build 会逐行读取并生成镜像。掌握常用指令和几条最佳实践,你就能在自己的服务器或 VPS 上稳定地打包和运行应用。本文以 Ubuntu/Debian 环境为主。

常用指令速览

  • FROM:指定基础镜像,必须是第一条指令,例如 FROM debian:12-slim。
  • WORKDIR:设置后续指令的工作目录,不存在会自动创建。
  • COPY:把构建上下文里的文件复制进镜像。
  • RUN:在构建阶段执行命令(如安装依赖),每条 RUN 会生成一个新层。
  • ENV:设置环境变量,运行时依然生效。
  • EXPOSE:声明容器监听的端口,主要是文档作用。
  • CMD:容器启动时的默认命令,可被 docker run 覆盖。
  • ENTRYPOINT:固定的入口程序,常与 CMD 配合,CMD 作为默认参数。

构建镜像并打标签:

docker build -t myapp:1.0 .

末尾的 . 是构建上下文(当前目录);-t 给镜像命名和标签。

最佳实践

选小的基础镜像

优先用 -slim 或 alpine 变体。基础镜像越小,拉取越快、攻击面越小。注意 Alpine 用 musl libc,某些程序需改用 Debian slim。

合并 RUN 减少层数

每条 RUN 都是一层,层越多镜像越臃肿。把关联命令用 && 串起来,并在同一层清理缓存:

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*

利用构建缓存顺序

Docker 按指令逐层缓存,某层变了它之后的层全部失效。把「变化少」的步骤放前面:先复制依赖清单并安装,再复制源代码。这样改代码时不必每次重装依赖。

使用 .dockerignore

在项目根目录建 .dockerignore,排除 nodemodules、.git、日志等,既缩小上下文、加快构建,也避免把敏感文件打进镜像:

.git
node_modules
*.log

多阶段构建减小体积

用一个阶段编译,再把产物复制到干净的运行镜像,编译器和源码都不会进最终镜像:

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM debian:12-slim
COPY --from=build /app /app

用非 root 用户运行

默认以 root 运行有安全风险。创建普通用户并切换:

RUN useradd -m appuser
USER appuser

一个完整示例

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

FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
RUN useradd -m appuser
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

先安装依赖再复制源码,充分利用缓存;多阶段只保留运行所需产物;最后以 appuser 非 root 身份启动。

小结

Dockerfile 的核心是几条基础指令加上「小、少、有序」三个原则:选小基础镜像、合并 RUN 减少层、按变化频率排序以命中缓存,再配合 .dockerignore、多阶段构建和非 root 用户,就能在你的服务器或 VPS 上产出体积小、构建快、更安全的镜像。从一份能跑的 Dockerfile 起步,逐步套用这些实践即可。