Dockerfile 编写与镜像构建
2026-05-09
运维
00
请注意,本文编写于 76 天前,最后修改于 38 天前,其中某些信息可能已经过时。

目录

Dockerfile 编写与镜像构建
Dockerfile 是什么?
Dockerfile 指令速查
CMD vs ENTRYPOINT
CMD —— 默认命令,可被覆盖
ENTRYPOINT —— 入口命令,不易被覆盖
最佳实践
构建上下文与 .dockerignore
构建上下文
.dockerignore
镜像分层原理(深入)
缓存机制
实战示例
示例 1:Python Flask 应用
完整项目结构
requirements.txt
app.py
Dockerfile
实现步骤
示例 2:Node.js 应用
完整项目结构
package.json
server.js
Dockerfile
实现步骤
示例 3:Go 应用(多阶段构建)
完整项目结构
go.mod
main.go
Dockerfile(多阶段构建)
实现步骤
构建命令
Dockerfile 最佳实践
1. 使用精简基础镜像
2. 合并 RUN 指令减少层数
3. 使用非 root 用户
4. 添加 HEALTHCHECK
5. 利用 .dockerignore
6. 避免敏感信息硬编码
7. 清理构建缓存
实操练习 Lab 03:构建你的第一个镜像
练习目标
步骤 1:准备项目目录
步骤 2:编写配置文件
1. requirements.txt
2. app.py
3. .dockerignore
4. Dockerfile
步骤 3:构建镜像
步骤 4:运行容器
步骤 5:验证应用
步骤 6:清理资源
练习拓展
常见问题排查

Dockerfile 编写与镜像构建

Dockerfile 是什么?

Dockerfile 是一个文本文件,包含一系列指令,Docker 根据这些指令自动构建镜像。可以理解为 "镜像的菜谱"—— 告诉 Docker 一步步如何做出一个镜像。


Dockerfile 指令速查

指令用途示例
FROM指定基础镜像(必须是第一条指令)FROM python:3.11-slim
WORKDIR设置工作目录WORKDIR /app
COPY复制文件到镜像COPY . /app
ADD复制文件(支持 URL 和自动解压)ADD app.tar.gz /app
RUN构建时执行命令RUN pip install -r requirements.txt
CMD容器启动时的默认命令CMD ["python", "app.py"]
ENTRYPOINT容器启动时的入口命令ENTRYPOINT ["python"]
ENV设置环境变量ENV APP_ENV=production
EXPOSE声明容器监听的端口(文档用途)EXPOSE 8080
VOLUME声明数据卷挂载点VOLUME ["/data"]
ARG构建时的参数ARG VERSION=1.0
LABEL元数据标签LABEL maintainer="darren"
USER指定运行用户USER appuser
HEALTHCHECK健康检查HEALTHCHECK CMD curl -f http://localhost/

CMD vs ENTRYPOINT

这是初学者最容易混淆的两个指令:

CMD —— 默认命令,可被覆盖

展开代码
FROM ubuntu CMD ["echo", "Hello World"]
展开代码
docker run myimage # 输出: Hello World docker run myimage echo Hi # 输出: Hi(CMD 被覆盖)

ENTRYPOINT —— 入口命令,不易被覆盖

展开代码
FROM ubuntu ENTRYPOINT ["echo"] CMD ["Hello World"]
展开代码
docker run myimage # 输出: Hello World(ENTRYPOINT + CMD) docker run myimage Hi # 输出: Hi(ENTRYPOINT + 覆盖的 CMD)

最佳实践

  • ENTRYPOINT: 定义容器的 "身份"(它是什么)
  • CMD: 定义默认参数(可以被用户覆盖)
展开代码
ENTRYPOINT ["python"] CMD ["app.py"] # docker run myimage → python app.py # docker run myimage test.py → python test.py

构建上下文与 .dockerignore

构建上下文

执行 docker build 时,Docker 会将指定目录的所有文件发送给 Docker Daemon:

展开代码
docker build -t myapp . # ^ 这个点就是构建上下文(当前目录)

.dockerignore

.gitignore 类似,告诉 Docker 哪些文件不需要发送到构建上下文。以下是完整的配置示例及每个忽略项的作用说明:

展开代码
# .dockerignore # 1. Git 版本控制目录:包含仓库元数据,镜像构建无需依赖,排除可减少上下文体积 .git # 2. Node.js 依赖目录:通常体积大且可通过 npm install 重新安装,排除后避免重复传输 node_modules # 3. Python 编译缓存目录:运行时自动生成,无需带入镜像 __pycache__ # 4. Python 编译后的字节码文件:同上,排除可减少上下文大小 *.pyc # 5. 环境变量文件:包含敏感信息(如密钥)或本地配置,不应进入镜像 .env # 6. Python 虚拟环境目录:本地开发依赖,镜像内可重新创建 .venv # 7. 项目说明文档:仅用于阅读,与镜像运行无关 README.md # 8. Docker Compose 配置文件:用于容器编排,非镜像构建必需 docker-compose*.yml # 可选扩展:根据项目类型补充 # log/ # 日志目录:本地日志无需带入镜像 # build/ # 编译产物目录:如前端打包后的dist可按需复制,而非整个build # *.log # 日志文件:避免本地日志污染镜像 # .DS_Store # MacOS 系统文件:无业务意义 # .vscode/ # 编辑器配置文件:仅本地开发使用

为什么重要:如果项目有大量不相关的文件(比如 node_modules),不使用 .dockerignore 会导致构建上下文传输缓慢,甚至可能将敏感文件意外带入镜像,同时增加镜像体积。


镜像分层原理(深入)

Dockerfile 中的每条指令都会创建一个新的镜像层:

展开代码
FROM python:3.11-slim # Layer 1: 基础镜像(约 120MB) WORKDIR /app # Layer 2: 设置工作目录(几乎 0) COPY requirements.txt . # Layer 3: 复制依赖文件(几 KB) RUN pip install -r requirements.txt # Layer 4: 安装依赖(可能几十 MB) COPY . . # Layer 5: 复制应用代码 CMD ["python", "app.py"] # Layer 6: 元数据(0 size)

缓存机制

Docker 构建时会利用缓存加速:

  • 如果某一层的指令和上下文没有变化,直接使用缓存
  • 一旦某一层缓存失效,它之后的所有层都会重新构建

所以:把变化频率低的指令放前面,变化频率高的放后面

展开代码
# ✗ 不好:每次代码改变都要重新安装依赖 COPY . . RUN pip install -r requirements.txt # ✓ 好:只有 requirements.txt 变了才重新安装依赖 COPY requirements.txt . RUN pip install -r requirements.txt COPY . .

实战示例

示例 1:Python Flask 应用

完整项目结构

展开代码
flask-app/ ├── .dockerignore # 参考上文配置 ├── Dockerfile ├── app.py # 应用代码 └── requirements.txt # 依赖清单

requirements.txt

展开代码
flask==2.3.3 gunicorn==21.2.0 # 生产环境 WSGI 服务器

app.py

展开代码
from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return "Hello Docker! 🐳" @app.route('/health') # 健康检查接口 def health(): return {"status": "ok"}, 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

Dockerfile

展开代码
# 使用精简的基础镜像 FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 复制依赖文件并安装(利用缓存) COPY requirements.txt . # --no-cache-dir:避免pip缓存,减少镜像体积 RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY . . # 声明端口(文档用途,实际映射需靠docker run -p) EXPOSE 5000 # 健康检查配置 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:5000/health || exit 1 # 生产环境推荐使用gunicorn,而非内置服务器 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

实现步骤

展开代码
# 1. 进入项目目录 cd flask-app # 2. 构建 Docker 镜像 docker build -t flask-app:v1 . # 3. 启动容器(后台运行,端口映射 5000:5000) docker run -d --name flask-container -p 5000:5000 flask-app:v1 # 4. 查看容器运行状态 docker ps # 5. 访问应用测试 curl http://localhost:5000 curl http://localhost:5000/health # 6. 查看容器日志 docker logs flask-container # 7. 停止并删除容器(结束使用) docker stop flask-container docker rm flask-container

示例 2:Node.js 应用

完整项目结构

展开代码
node-app/ ├── .dockerignore # 参考上文配置 ├── Dockerfile ├── server.js # 应用代码 ├── package.json # 依赖清单 └── package-lock.json # 依赖锁定文件

package.json

展开代码
{ "name": "docker-node-app", "version": "1.0.0", "dependencies": { "express": "^4.18.2" }, "scripts": { "start": "node server.js" } }

server.js

展开代码
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello Docker Node.js! 🚀'); }); app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); app.listen(port, '0.0.0.0', () => { console.log(`App running on port ${port}`); });

Dockerfile

展开代码
# 使用alpine精简镜像,体积更小 FROM node:20-alpine # 设置工作目录 WORKDIR /app # 复制依赖清单(利用缓存) COPY package*.json ./ # npm ci:严格按照package-lock.json安装,比npm install更稳定 RUN npm ci --only=production # 复制应用代码 COPY . . # 声明端口 EXPOSE 3000 # 健康检查 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1 # 启动应用 CMD ["npm", "start"]

实现步骤

展开代码
# 1. 进入项目目录 cd node-app # 2. 构建镜像 docker build -t node-app:v1 . # 3. 启动容器(后台运行,端口映射 3000:3000) docker run -d --name node-container -p 3000:3000 node-app:v1 # 4. 查看运行状态 docker ps # 5. 访问测试 curl http://localhost:3000 curl http://localhost:3000/health # 6. 查看日志 docker logs node-container # 7. 停止并删除容器 docker stop node-container docker rm node-container

示例 3:Go 应用(多阶段构建)

完整项目结构

展开代码
go-app/ ├── .dockerignore # 参考上文配置 ├── Dockerfile ├── go.mod # Go 模块配置 ├── go.sum # 依赖校验文件 └── main.go # 应用代码

go.mod

展开代码
module docker-go-app go 1.22 require github.com/gin-gonic/gin v1.9.1

main.go

展开代码
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Hello Docker Go! 🐹") }) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) r.Run(":8080") }

Dockerfile(多阶段构建)

展开代码
# 第一阶段:构建阶段(使用完整Go镜像) FROM golang:1.22-alpine AS builder # 设置工作目录 WORKDIR /app # 复制Go模块文件(利用缓存) COPY go.mod go.sum ./ RUN go mod download # 复制应用代码 COPY . . # 编译Go程序:CGO_ENABLED=0 禁用CGO,生成静态二进制文件 RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server . # 第二阶段:运行阶段(使用极简alpine镜像) FROM alpine:3.19 # 安装curl用于健康检查(alpine默认无curl) RUN apk --no-cache add curl # 从构建阶段复制编译后的二进制文件 COPY --from=builder /app/server /server # 声明端口 EXPOSE 8080 # 健康检查 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # 启动应用 CMD ["/server"]

实现步骤

展开代码
# 1. 进入项目目录 cd go-app # 2. 构建镜像(多阶段自动完成) docker build -t go-app:v1 . # 3. 启动容器(后台运行,端口映射 8080:8080) docker run -d --name go-container -p 8080:8080 go-app:v1 # 4. 查看运行状态 docker ps # 5. 访问测试 curl http://localhost:8080 curl http://localhost:8080/health # 6. 查看日志 docker logs go-container # 7. 停止并删除容器 docker stop go-container docker rm go-container

多阶段构建优势:最终镜像仅包含编译后的二进制文件,体积从几百 MB 降至几十 MB,减少攻击面,提升安全性。


构建命令

展开代码
# 基本构建(指定镜像名) docker build -t myapp . # 指定 tag 版本 docker build -t myapp:v1.0 . # 指定自定义Dockerfile路径 docker build -f Dockerfile.prod -t myapp:prod . # 不使用缓存构建(强制重新构建所有层) docker build --no-cache -t myapp . # 传递构建参数(对应Dockerfile中的ARG) docker build --build-arg VERSION=2.0 -t myapp . # 查看构建的镜像 docker images myapp # 构建后运行容器 docker run -d -p 5000:5000 --name myapp-container myapp # 查看容器日志 docker logs myapp-container # 检查健康状态 docker inspect --format '{{.State.Health.Status}}' myapp-container

Dockerfile 最佳实践

1. 使用精简基础镜像

展开代码
# ✗ 完整镜像,约 900MB FROM python:3.11 # ✓ slim 镜像,约 120MB(平衡体积和兼容性) FROM python:3.11-slim # ✓✓ alpine 镜像,约 50MB(但可能有兼容性问题,如缺少系统库) FROM python:3.11-alpine

2. 合并 RUN 指令减少层数

展开代码
# ✗ 三层(每层都会增加镜像体积) RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # ✓ 一层(用\换行,&&串联命令) RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* # 清理apt缓存,减少镜像体积

3. 使用非 root 用户

展开代码
# 创建无密码的普通用户 RUN adduser --disabled-password --gecos '' appuser # 切换到普通用户运行容器(降低安全风险) USER appuser

4. 添加 HEALTHCHECK

展开代码
# 配置健康检查:每30秒检查一次,超时5秒,重试3次失败则标记为不健康 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1

5. 利用 .dockerignore

始终创建 .dockerignore 文件排除不必要的文件(详见上文)。

6. 避免敏感信息硬编码

展开代码
# ✗ 不安全:密钥硬编码 ENV DB_PASSWORD=123456 # ✓ 推荐:通过构建参数或运行时环境变量传入 ARG DB_PASSWORD ENV DB_PASSWORD=$DB_PASSWORD # 运行时:docker run -e DB_PASSWORD=123456 myapp

7. 清理构建缓存

展开代码
# Python:禁用pip缓存 RUN pip install --no-cache-dir -r requirements.txt # Node.js:使用npm ci并清理缓存 RUN npm ci --only=production && npm cache clean --force # Ubuntu/Debian:清理apt缓存 RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

实操练习 Lab 03:构建你的第一个镜像

练习目标

  1. 编写完整的 Dockerfile 构建 Python Flask 应用
  2. 配置 .dockerignore 优化构建上下文
  3. 构建镜像并运行容器
  4. 验证健康检查和端口映射

步骤 1:准备项目目录

展开代码
# 创建项目目录 mkdir flask-docker-lab && cd flask-docker-lab # 创建空文件 touch Dockerfile .dockerignore app.py requirements.txt

步骤 2:编写配置文件

1. requirements.txt

展开代码
flask==2.3.3 gunicorn==21.2.0

2. app.py

展开代码
from flask import Flask, jsonify app = Flask(__name__) # 首页接口 @app.route('/') def index(): return "<h1>My First Docker App</h1><p>Built with Dockerfile!</p>" # 健康检查接口 @app.route('/health') def health_check(): return jsonify({ "status": "healthy", "app": "flask-docker-lab", "version": "1.0" }), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

3. .dockerignore

展开代码
.git __pycache__ *.pyc .env .venv README.md docker-compose*.yml *.log

4. Dockerfile

展开代码
# 基础镜像 FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 创建非root用户 RUN adduser --disabled-password --gecos '' appuser # 复制依赖文件并安装 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY . . # 更改目录权限(确保普通用户可访问) RUN chown -R appuser:appuser /app # 切换到普通用户 USER appuser # 声明端口 EXPOSE 5000 # 健康检查 HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD curl -f http://localhost:5000/health || exit 1 # 启动命令 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

步骤 3:构建镜像

展开代码
# 构建镜像,命名为flask-lab:v1 docker build -t flask-lab:v1 . # 查看镜像 docker images flask-lab:v1

步骤 4:运行容器

展开代码
# 后台运行容器,映射端口5000:5000 docker run -d -p 5000:5000 --name flask-lab-container flask-lab:v1 # 查看容器状态 docker ps # 查看健康状态(等待10秒后检查) docker inspect --format '{{.State.Health.Status}}' flask-lab-container

步骤 5:验证应用

展开代码
# 访问首页 curl http://localhost:5000 # 访问健康检查接口 curl http://localhost:5000/health

步骤 6:清理资源

展开代码
# 停止容器 docker stop flask-lab-container # 删除容器 docker rm flask-lab-container # 删除镜像(可选) docker rmi flask-lab:v1

练习拓展

  1. 尝试修改 app.py 代码,重新构建镜像,观察缓存是否生效
  2. 尝试添加环境变量(如 ENV APP_VERSION=1.0),在 app.py 中读取并返回
  3. 尝试使用 docker build --no-cache 强制重新构建,对比构建时间
  4. 尝试将基础镜像换成 alpine 版本,解决可能的依赖问题(如缺少 curl)

常见问题排查

  1. 构建上下文过大:检查 .dockerignore 是否配置正确,排除 node_modules、.venv 等大目录
  2. 缓存失效:确认指令顺序是否合理,变化频繁的指令(如 COPY . .)放在最后
  3. 权限问题:使用非 root 用户时,确保目录 / 文件权限正确(chown/chmod)
  4. 健康检查失败:确认接口路径正确,容器内已安装 curl/wget 等工具
  5. 端口映射失败:确认 EXPOSE 端口与 run -p 端口一致,容器内应用绑定 0.0.0.0 而非 127.0.0.1

本文作者:zzz

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!