编写 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 起步,逐步套用这些实践即可。