镜像分层

在 Dockerfile 的构建过程中,每条指令都会基于上一步的结果创建一个新的镜像层,而不是为每一步单独生成一个完整的独立镜像。

镜像分层机制

  1. 分层构建原理

    • 每个 Dockerfile 指令(如 RUN, COPY, ADD 等)都会在当前镜像层之上创建一个新的层
    • 新层只包含该指令带来的变更
    • 这些层是相互叠加的,最终组成完整的镜像
  2. 具体构建过程

    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)
    • 依此类推,直到最终镜像
  3. Union File System 的作用

    • Docker 使用联合文件系统(如 overlay2)将这些层叠加
    • 容器运行时看到的是所有层的统一视图
    • 上层会覆盖下层的同名文件
  4. 缓存机制

    • 如果 Dockerfile 的某一步及之前步骤没有变化,会直接使用缓存中的层
    • 一旦某一步发生变化,该步骤及其后所有步骤都需要重新执行
  5. 查看镜像分层
    可以使用 docker history <image> 命令查看镜像的组成层次:

    docker history my-image:tag

为什么采用这种设计?

  1. 空间效率

    • 多个镜像可以共享相同的基础层
    • 例如多个基于 ubuntu:20.04 的镜像可以共享这个基础层
  2. 构建速度

    • 未改变的层可以直接使用缓存
    • 只需重新构建发生变化的层及其后续层
  3. 最小化变更

    • 每个层只记录文件系统的变更集
    • 便于追踪和管理镜像的变更历史

实际影响示例

假设有以下 Dockerfile:

FROM alpine
RUN apk add --no-cache python3  # 安装了Python(约40MB)
COPY small.txt /tmp/            # 添加1KB的小文件

如果修改 small.txt 后重新构建:

  • FROMRUN 步骤会直接使用缓存
  • 只需重新执行 COPY 和后续步骤

但如果调整 RUN 命令的顺序:

FROM alpine
COPY small.txt /tmp/            # 现在这行在前
RUN apk add --no-cache python3  # 这行在后

则修改 small.txt 会导致 RUN 步骤也要重新执行,因为它的前置步骤发生了变化。

理解这种分层机制对于编写高效的 Dockerfile 非常重要,合理的指令顺序可以显著提高构建效率。

Dockerfile

Dockerfile 是一个文本文件,包含了一系列用于构建 Docker 镜像的指令。通过 Dockerfile,用户可以定义镜像的构建过程,包括基础镜像选择、文件添加、环境变量设置、运行命令等。

基本结构

一个典型的 Dockerfile 包含以下部分:

  1. 基础镜像:使用 FROM 指令指定
  2. 维护者信息:使用 LABEL 或已弃用的 MAINTAINER 指令
  3. 镜像构建指令:如 RUN, COPY, ADD, ENV
  4. 容器启动指令:如 CMD, ENTRYPOINT, EXPOSE

常用指令

FROM

指定基础镜像,必须是 Dockerfile 的第一条指令(除 ARG 外)。

FROM ubuntu:20.04
FROM python:3.8-slim

LABEL

为镜像添加元数据,替代已弃用的 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 /app

ENV

ENV 是 Dockerfile 中的一个重要指令,用于设置环境变量。这些变量可以在构建阶段和容器运行时使用。

基本语法

ENV <key> <value>
ENV <key>=<value> ...

使用方式

  1. 单个变量设置:

    ENV MY_NAME John Doe
  2. 多个变量设置(Docker 1.4+):

    ENV MY_NAME="John Doe" MY_DOG=Rex MY_CAT=fluffy

特点与用途

  1. 持久性: 设置的环境变量会持久存在于从该镜像创建的所有容器中
  2. 构建阶段使用: 可以在后续的 Dockerfile 指令中使用 (RUN, CMD 等)
  3. 运行时可用: 容器运行时可以通过 echo $VAR 访问
  4. 覆盖性: 可以在 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 443

CMD和ENTRYPOINT

CMD

作用:提供容器启动时的默认命令和参数。

三种形式

  1. CMD ["executable","param1","param2"] (exec 形式,推荐)
  2. CMD ["param1","param2"] (作为 ENTRYPOINT 的默认参数)
  3. CMD command param1 param2 (shell 形式)

特点

  • 可以被 docker run 后的命令完全覆盖
  • 如果 Dockerfile 中有多个 CMD,只有最后一个生效
  • 通常用于为 ENTRYPOINT 提供默认参数

ENTRYPOINT

作用:配置容器作为一个可执行程序运行。

两种形式

  1. ENTRYPOINT ["executable", "param1", "param2"] (exec 形式,推荐)
  2. 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"

最佳实践

  1. 使用 exec 形式(JSON 数组格式),避免 shell 处理
  2. ENTRYPOINT 用于定义主命令,CMD 用于定义默认参数
  3. 需要可交互的容器(如 bash)通常只用 CMD
  4. 工具类容器通常使用 ENTRYPOINT 使其表现得像独立程序

ARG

定义构建时的变量,构建后不再存在。

ARG VERSION=latest
FROM busybox:$VERSION

VOLUME

VOLUME 是 Dockerfile 中的一个重要指令,用于在容器中创建挂载点(mount point),以便与宿主机或其他容器共享数据。

VOLUME ["/data"]
# 或者
VOLUME /data
# 或者指定多个卷
VOLUME ["/data1", "/data2", "/data3"]

功能说明

  1. 创建匿名卷

    • VOLUME 指令会在容器运行时自动创建一个匿名卷(没有名称的卷)
    • 即使没有显式使用 -v--mount 选项挂载,也会创建
  2. 数据持久化

    • 确保数据不会随着容器的删除而丢失
    • 即使容器被删除,卷中的数据仍然保留
  3. 共享数据

    • 可以作为多个容器共享数据的接口

docker run -v 的区别

特性Dockerfile 中的 VOLUMEdocker run -v
卷类型匿名卷可以是匿名卷或命名卷
挂载时机构建时声明运行时指定
主机目录不能指定可以指定主机目录
灵活性较低较高

USER

指定运行时的用户。

USER nobody

Dockerfile 最佳实践

  1. 使用官方镜像:尽量使用官方维护的基础镜像
  2. 多阶段构建:减少最终镜像大小

    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
  3. 合并 RUN 命令:减少镜像层数
  4. 合理使用 .dockerignore:排除不必要的文件
  5. 最小化镜像:只包含必要的组件
  6. 固定版本:避免使用 latest 标签
  7. 安全性:避免以 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
最后修改:2025 年 06 月 29 日
如果觉得我的文章对你有用,请随意赞赏