镜像分层
在 Dockerfile 的构建过程中,每条指令都会基于上一步的结果创建一个新的镜像层,而不是为每一步单独生成一个完整的独立镜像。
镜像分层机制
分层构建原理:
- 每个 Dockerfile 指令(如
RUN,COPY,ADD等)都会在当前镜像层之上创建一个新的层 - 新层只包含该指令带来的变更
- 这些层是相互叠加的,最终组成完整的镜像
- 每个 Dockerfile 指令(如
具体构建过程:
FROM ubuntu:20.04 # 层1:基础镜像 RUN apt-get update # 层2:在层1基础上添加更新数据 RUN apt-get install -y curl # 层3:在层2基础上安装curl COPY app.py /app/ # 层4:在层3基础上添加文件 CMD ["python", "/app/app.py"] # 层5:在层4基础上设置启动命令构建时:
- 执行完
FROM后生成临时镜像A(层1) - 执行
RUN apt-get update后生成临时镜像B(层1+层2) - 依此类推,直到最终镜像
- 执行完
Union File System 的作用:
- Docker 使用联合文件系统(如 overlay2)将这些层叠加
- 容器运行时看到的是所有层的统一视图
- 上层会覆盖下层的同名文件
缓存机制:
- 如果 Dockerfile 的某一步及之前步骤没有变化,会直接使用缓存中的层
- 一旦某一步发生变化,该步骤及其后所有步骤都需要重新执行
查看镜像分层:
可以使用docker history <image>命令查看镜像的组成层次:docker history my-image:tag
为什么采用这种设计?
空间效率:
- 多个镜像可以共享相同的基础层
- 例如多个基于
ubuntu:20.04的镜像可以共享这个基础层
构建速度:
- 未改变的层可以直接使用缓存
- 只需重新构建发生变化的层及其后续层
最小化变更:
- 每个层只记录文件系统的变更集
- 便于追踪和管理镜像的变更历史
实际影响示例
假设有以下 Dockerfile:
FROM alpine
RUN apk add --no-cache python3 # 安装了Python(约40MB)
COPY small.txt /tmp/ # 添加1KB的小文件如果修改 small.txt 后重新构建:
FROM和RUN步骤会直接使用缓存- 只需重新执行
COPY和后续步骤
但如果调整 RUN 命令的顺序:
FROM alpine
COPY small.txt /tmp/ # 现在这行在前
RUN apk add --no-cache python3 # 这行在后则修改 small.txt 会导致 RUN 步骤也要重新执行,因为它的前置步骤发生了变化。
理解这种分层机制对于编写高效的 Dockerfile 非常重要,合理的指令顺序可以显著提高构建效率。
Dockerfile
Dockerfile 是一个文本文件,包含了一系列用于构建 Docker 镜像的指令。通过 Dockerfile,用户可以定义镜像的构建过程,包括基础镜像选择、文件添加、环境变量设置、运行命令等。
基本结构
一个典型的 Dockerfile 包含以下部分:
- 基础镜像:使用
FROM指令指定 - 维护者信息:使用
LABEL或已弃用的MAINTAINER指令 - 镜像构建指令:如
RUN,COPY,ADD,ENV等 - 容器启动指令:如
CMD,ENTRYPOINT,EXPOSE等
常用指令
FROM
指定基础镜像,必须是 Dockerfile 的第一条指令(除 ARG 外)。
FROM ubuntu:20.04
FROM python:3.8-slimLABEL
为镜像添加元数据,替代已弃用的 MAINTAINER。
LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="This is a custom Docker image"RUN
执行命令并创建新的镜像层,常用于安装软件包。
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*COPY 和 ADD
将文件从构建上下文复制到镜像中。
COPY ./app /app
COPY requirements.txt /tmp/
ADD https://example.com/file.tar.gz /tmp/ # ADD 可以解压和从URL获取区别:
COPY只支持本地文件复制ADD支持 URL 和自动解压功能
WORKDIR
设置工作目录,相当于 cd 命令。
WORKDIR /appENV
ENV 是 Dockerfile 中的一个重要指令,用于设置环境变量。这些变量可以在构建阶段和容器运行时使用。
基本语法
ENV <key> <value>
ENV <key>=<value> ...使用方式
单个变量设置:
ENV MY_NAME John Doe多个变量设置(Docker 1.4+):
ENV MY_NAME="John Doe" MY_DOG=Rex MY_CAT=fluffy
特点与用途
- 持久性: 设置的环境变量会持久存在于从该镜像创建的所有容器中
- 构建阶段使用: 可以在后续的 Dockerfile 指令中使用 (
RUN,CMD等) - 运行时可用: 容器运行时可以通过
echo $VAR访问 - 覆盖性: 可以在
docker run时通过-e参数覆盖
示例
FROM ubuntu:latest
ENV APP_VERSION=1.0.0 \
NODE_ENV=production \
PATH=/usr/local/app/bin:$PATH
RUN echo "Building version ${APP_VERSION}" && \
echo "Environment is ${NODE_ENV}"
CMD ["bash"]EXPOSE
声明容器运行时监听的端口(仅有声明作用)。
EXPOSE 80
EXPOSE 443CMD和ENTRYPOINT
CMD
作用:提供容器启动时的默认命令和参数。
三种形式:
CMD ["executable","param1","param2"](exec 形式,推荐)CMD ["param1","param2"](作为 ENTRYPOINT 的默认参数)CMD command param1 param2(shell 形式)
特点:
- 可以被
docker run后的命令完全覆盖 - 如果 Dockerfile 中有多个 CMD,只有最后一个生效
- 通常用于为 ENTRYPOINT 提供默认参数
ENTRYPOINT
作用:配置容器作为一个可执行程序运行。
两种形式:
ENTRYPOINT ["executable", "param1", "param2"](exec 形式,推荐)ENTRYPOINT command param1 param2(shell 形式)
特点:
docker run的参数会追加到 ENTRYPOINT 命令后- 需要使用
--entrypoint标志才能覆盖 - 使容器表现得像一个可执行程序
组合使用
CMD 和 ENTRYPOINT 通常一起使用,形成"命令+默认参数"的模式:
ENTRYPOINT ["nginx", "-g"]
CMD ["daemon off;"]这样:
- 默认运行
nginx -g "daemon off;" - 可以通过
docker run my-nginx -g "debug level;"覆盖 CMD 参数
示例
单独使用 CMD
FROM ubuntu
CMD ["echo", "Hello World"]运行 docker run <image> 输出 "Hello World"
运行 docker run <image> echo "Goodbye" 会覆盖 CMD,输出 "Goodbye"
单独使用 ENTRYPOINT
FROM ubuntu
ENTRYPOINT ["echo", "Hello"]运行 docker run <image> 输出 "Hello"
运行 docker run <image> World 输出 "Hello World"
组合使用
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["Hello World"]运行 docker run <image> 输出 "Hello World"
运行 docker run <image> Goodbye 输出 "Goodbye"
最佳实践
- 使用 exec 形式(JSON 数组格式),避免 shell 处理
- ENTRYPOINT 用于定义主命令,CMD 用于定义默认参数
- 需要可交互的容器(如 bash)通常只用 CMD
- 工具类容器通常使用 ENTRYPOINT 使其表现得像独立程序
ARG
定义构建时的变量,构建后不再存在。
ARG VERSION=latest
FROM busybox:$VERSIONVOLUME
VOLUME 是 Dockerfile 中的一个重要指令,用于在容器中创建挂载点(mount point),以便与宿主机或其他容器共享数据。
VOLUME ["/data"]
# 或者
VOLUME /data
# 或者指定多个卷
VOLUME ["/data1", "/data2", "/data3"]功能说明
创建匿名卷:
VOLUME指令会在容器运行时自动创建一个匿名卷(没有名称的卷)- 即使没有显式使用
-v或--mount选项挂载,也会创建
数据持久化:
- 确保数据不会随着容器的删除而丢失
- 即使容器被删除,卷中的数据仍然保留
共享数据:
- 可以作为多个容器共享数据的接口
与 docker run -v 的区别
| 特性 | Dockerfile 中的 VOLUME | docker run -v |
|---|---|---|
| 卷类型 | 匿名卷 | 可以是匿名卷或命名卷 |
| 挂载时机 | 构建时声明 | 运行时指定 |
| 主机目录 | 不能指定 | 可以指定主机目录 |
| 灵活性 | 较低 | 较高 |
USER
指定运行时的用户。
USER nobodyDockerfile 最佳实践
- 使用官方镜像:尽量使用官方维护的基础镜像
多阶段构建:减少最终镜像大小
FROM node:14 as builder WORKDIR /app COPY . . RUN npm install && npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html- 合并 RUN 命令:减少镜像层数
- 合理使用 .dockerignore:排除不必要的文件
- 最小化镜像:只包含必要的组件
- 固定版本:避免使用
latest标签 - 安全性:避免以 root 用户运行应用
实战
使用docker build
Dockerfile
无注释
FROM golang:alpine AS builder
LABEL stage=gobuilder
ENV CGO_ENABLED=0
ENV GOPROXY=https://goproxy.cn,direct
RUN apk update --no-cache && apk add --no-cache tzdata
WORKDIR /build
ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/main ./main.go
FROM alpine
RUN apk update --no-cache && apk add --no-cache ca-certificates
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ=Asia/Shanghai
WORKDIR /app
COPY --from=builder /app/main /app/main
CMD ["./main"]带注释
# 第一阶段开始
FROM golang:alpine AS builder
# 添加 stage=gobuilder 标签,用于标识构建阶段(虽然多阶段构建中标签非必需,但可用于调试)。
LABEL stage=gobuilder
# 禁用 CGO,确保编译出的二进制是静态链接的(避免依赖外部库,适合 scratch 镜像)。
ENV CGO_ENABLED=0
# 设置国内代理(goproxy.cn),加速 Go 模块下载。
ENV GOPROXY=https://goproxy.cn,direct
# #安装 tzdata 包,用于后续处理时区(虽然 Alpine 已很小,但 tzdata 是必要的时区数据库)。
RUN apk update --no-cache && apk add --no-cache tzdata
# 设置工作目录
WORKDIR /build
# 复制 go.mod 和 go.sum(优先复制以利用 Docker 缓存层)。
ADD go.mod .
ADD go.sum .
# 下载依赖
RUN go mod download
# 复制所有本地代码到容器。
COPY . .
# -ldflags="-s -w":移除调试信息,减小二进制体积
# 输出二进制到 /app/main,入口文件为 ./api/tc.go。
RUN go build -ldflags="-s -w" -o /app/main ./main.go
# 第二阶段开始
# 使用 alpine 作为基础镜像
FROM alpine
# 安装了 ca-certificates,这样使用 TLS证书就没问题了
RUN apk update --no-cache && apk add --no-cache ca-certificates
# 复制上海时区文件。
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
# 设置 TZ 环境变量为 Asia/Shanghai。
ENV TZ=Asia/Shanghai
# 设置工作目录
WORKDIR /app
# 从 builder 阶段复制文件
COPY --from=builder /app/main /app/main
# 运行可执行文件
CMD ["./main"]build
docker build --rm --platform linux/amd64 -t hello:v1 .使用makefile
IMAGE_NAME=hello
TAG=v1
CONTAINER_NAME=hello
# 构建
build:
docker build --rm --platform linux/amd64 -t $(IMAGE_NAME):$(TAG) .
# 运行
run:
docker run --name $(CONTAINER_NAME) $(IMAGE_NAME):$(TAG)
# 停止&删除容器
stop:
docker stop $(IMAGE_NAME) || true
docker rm $(IMAGE_NAME) || true
# 清理镜像
clean:
docker rmi $(IMAGE_NAME):$(TAG) || true