0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

使用Dockerfile构建镜像的详细步骤

马哥Linux运维 来源:马哥Linux运维 2026-02-26 09:43 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

一、概述

1.1 背景介绍

Dockerfile写得好不好,直接影响三件事:镜像大小、构建速度、运行安全性。我见过太多团队的Dockerfile是"能跑就行"的水平——基础镜像用ubuntu:latest,一个RUN装几十个包不清理缓存,最终镜像1.2GB,构建一次15分钟,里面还带着gcc和make这些生产环境根本不需要的东西。

一个优化过的Dockerfile能把镜像从1.2GB压缩到80MB,构建时间从15分钟降到2分钟(利用缓存后30秒),同时减少90%的安全漏洞面。这不是理论数字,是我在实际项目中反复验证过的。

Dockerfile本质上是一系列指令的集合,Docker按顺序执行每条指令,每条指令生成一个镜像层(Layer)。理解分层机制是写好Dockerfile的基础——层可以被缓存和复用,合理的指令顺序能大幅提升构建速度;但层太多会增加镜像体积和拉取时间。

1.2 技术特点

分层缓存:每条指令生成一层,未变更的层直接使用缓存,构建速度从分钟级降到秒级

多阶段构建:编译环境和运行环境分离,最终镜像只包含运行时必需的文件,体积减少70%-90%

BuildKit引擎:Docker 18.09引入的新构建引擎,支持并行构建、缓存挂载、Secret挂载,构建速度提升2-3倍

可重复构建:同一个Dockerfile在任何机器上构建出相同的镜像,消除"我机器上能构建"的问题

安全扫描集成:构建时可以集成Trivy等扫描工具,在CI阶段拦截有漏洞的镜像

1.3 适用场景

Java/Go/Node.js/Python等各语言应用的容器化打包

CI/CD流水线中的自动化镜像构建

基础镜像定制(在官方镜像基础上添加公司内部工具和配置)

开发环境标准化(统一开发工具链版本)

多架构镜像构建(同时支持amd64和arm64)

1.4 环境要求

组件 版本要求 说明
Docker Engine 23.0+(推荐24.0+) 需要BuildKit支持
BuildKit 内置于Docker 23.0+ 默认启用,旧版本需手动开启
操作系统 Linux/macOS/Windows 构建环境不限,生产镜像建议基于Linux
磁盘空间 20GB+可用空间 构建缓存和中间层需要空间
内存 4GB+(编译型语言建议8GB+) Go/Java编译消耗内存较大

二、详细步骤

2.1 准备工作

2.1.1 确认BuildKit已启用

# 检查Docker版本
docker version

# 检查BuildKit是否启用(Docker 23.0+默认启用)
docker buildx version

# 如果是旧版本Docker,手动启用BuildKit
exportDOCKER_BUILDKIT=1

# 或者在daemon.json中永久启用
# "features": { "buildkit": true }

# 验证BuildKit工作正常
docker build --progress=plain -ttest-buildkit -f- . <<'EOF'
FROM alpine:3.19
RUN echo "BuildKit is working"
EOF

2.1.2 准备.dockerignore文件

.dockerignore的作用和.gitignore类似,排除不需要发送到构建上下文的文件。构建上下文越小,构建越快。我见过因为没有.dockerignore,把node_modules(500MB)和.git目录(200MB)都发送到构建上下文,导致每次构建光传输上下文就要30秒。

# 文件路径:项目根目录/.dockerignore
.git
.gitignore
.dockerignore
Dockerfile
docker-compose*.yml
README.md
LICENSE
docs/
tests/
*.md
*.log
*.tmp
*.swp

# Node.js项目
node_modules/
npm-debug.log
.npm/

# Java项目
target/
*.jar
*.class
.gradle/
build/

# Python项目
__pycache__/
*.pyc
.venv/
venv/
*.egg-info/

# IDE文件
.idea/
.vscode/
*.iml

# 操作系统文件
.DS_Store
Thumbs.db

2.2 核心配置

2.2.1 基础镜像选择

基础镜像的选择直接决定了最终镜像的大小和安全性。

#  错误示范:用ubuntu作为基础镜像,体积77MB,包含大量不需要的包
FROMubuntu:22.04

#  错误示范:用latest标签,每次构建可能拉到不同版本
FROMnode:latest

#  正确:用alpine变体,体积只有5MB
FROMnode:20.11-alpine3.19

#  正确:用distroless镜像,只包含运行时,没有shell和包管理器
FROMgcr.io/distroless/java17-debian12

#  正确:用slim变体,比完整版小但比alpine兼容性好
FROMpython:3.12-slim-bookworm

各基础镜像大小对比

基础镜像 大小 适用场景
ubuntu:22.04 77MB 需要apt安装大量系统包的场景
debian:bookworm-slim 74MB 需要glibc但想控制体积
alpine:3.19 7MB 追求极致小体积,注意musl libc兼容性
distroless 2-20MB 生产环境最安全,没有shell无法exec进入
scratch 0MB 静态编译的Go程序

注意:alpine使用musl libc而不是glibc,部分C语言编写的程序可能有兼容性问题。典型案例:Python的某些C扩展在alpine上编译失败或运行时段错误。遇到这种情况换slim变体。

2.2.2 指令顺序优化(利用构建缓存)

Docker构建缓存的规则:从第一条变更的指令开始,后续所有层的缓存全部失效。所以要把变化频率低的指令放前面,变化频率高的放后面。

#  错误示范:COPY . 放在安装依赖之前
# 任何源码文件变更都会导致依赖重新安装
FROMnode:20.11-alpine3.19
WORKDIR/app
COPY. .
RUNnpm ci --production
EXPOSE3000
CMD["node","server.js"]

#  正确:先复制依赖文件,安装依赖,再复制源码
# 只有package.json变更才会重新安装依赖
FROMnode:20.11-alpine3.19
WORKDIR/app
COPYpackage.json package-lock.json ./
RUNnpm ci --production
COPY. .
EXPOSE3000
CMD["node","server.js"]

缓存利用的最佳顺序

FROM(基础镜像,几乎不变)

安装系统依赖(apt/apk install,偶尔变)

复制依赖描述文件(package.json/pom.xml/go.mod)

安装应用依赖(npm ci/mvn install/go mod download)

复制源代码(每次提交都变)

构建应用

配置运行参数(CMD/ENTRYPOINT)

2.2.3 RUN指令优化

#  错误示范:每个命令一个RUN,产生多个层,且没有清理缓存
FROMubuntu:22.04
RUNapt update
RUNapt install -y curl
RUNapt install -y wget
RUNapt install -y vim

#  错误示范:安装了不需要的推荐包,没有清理apt缓存
FROMubuntu:22.04
RUNapt update && apt install -y curl wget

#  正确:合并RUN,使用--no-install-recommends,清理缓存
FROMubuntu:22.04
RUNapt-get update && 
  apt-get install -y --no-install-recommends 
    curl 
    wget 
    ca-certificates 
  && rm -rf /var/lib/apt/lists/*
# Alpine镜像的正确写法
FROMalpine:3.19
RUNapk add --no-cache 
  curl 
  tzdata 
  && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 
  &&echo"Asia/Shanghai"> /etc/timezone 
  && apk del tzdata

关键点

--no-install-recommends:不安装推荐包,能减少30%-50%的安装体积

rm -rf /var/lib/apt/lists/*:清理apt缓存,节省约30MB

apk add --no-cache:alpine的等价写法,不缓存索引文件

安装和清理必须在同一个RUN中,否则清理操作只是在新层中标记删除,不会减小镜像体积

2.2.4 COPY和ADD的区别

# COPY:简单复制文件,推荐使用
COPYapp.jar /app/
COPY--chown=app:app config/ /app/config/

# ADD:有额外功能,但不推荐日常使用
# ADD会自动解压tar文件
ADDarchive.tar.gz /app/

# ADD可以从URL下载文件(但不推荐,用curl更可控)
# ADD https://example.com/file.tar.gz /app/

#  推荐:用curl下载,可以在同一层中下载、解压、清理
RUNcurl -fsSL https://example.com/file.tar.gz -o /tmp/file.tar.gz && 
  tar xzf /tmp/file.tar.gz -C /app/ && 
  rm /tmp/file.tar.gz

原则:除非需要自动解压tar文件,否则一律用COPY。COPY的行为更明确,不会有意外的自动解压。

2.2.5 多阶段构建

多阶段构建是Dockerfile优化的核心技术。编译环境可能需要JDK、Maven、gcc等工具(几百MB),但运行时只需要JRE或一个二进制文件。

# Go应用的多阶段构建
# 阶段1:编译(使用完整的Go SDK,约800MB)
FROMgolang:1.22-alpine AS builder
WORKDIR/build
COPYgo.mod go.sum ./
RUNgo mod download
COPY. .
RUNCGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w"-o /app/server ./cmd/server

# 阶段2:运行(使用scratch,0MB基础镜像)
FROMscratch
COPY--from=builder /app/server /server
COPY--from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE8080
ENTRYPOINT["/server"]
# 最终镜像大小:约10-20MB(只有一个静态二进制文件+CA证书)
# Java应用的多阶段构建
# 阶段1:编译
FROMmaven:3.9-eclipse-temurin-17AS builder
WORKDIR/build
COPYpom.xml .
RUNmvn dependency:go-offline -B
COPYsrc ./src
RUNmvn package -DskipTests -B

# 阶段2:运行
FROMeclipse-temurin:17-jre-alpine
RUNaddgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
WORKDIR/app
COPY--from=builder --chown=app:app /build/target/*.jar app.jar
USERapp
EXPOSE8080
HEALTHCHECK--interval=30s --timeout=5s --start-period=60s --retries=3 
  CMD wget -qO- http://localhost:8080/actuator/health ||exit1
ENTRYPOINT["java","-XX:MaxRAMPercentage=75.0","-jar","app.jar"]
# 编译阶段镜像约800MB,最终运行镜像约180MB

2.3 启动和验证

2.3.1 构建镜像

# 基本构建
docker build -t myapp:1.0.0 .

# 指定Dockerfile路径
docker build -t myapp:1.0.0 -f deploy/Dockerfile .

# 使用BuildKit并显示详细输出
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp:1.0.0 .

# 构建时传入参数
docker build --build-arg APP_VERSION=1.0.0 --build-arg BUILD_ENV=prod -t myapp:1.0.0 .

# 不使用缓存构建(排查缓存问题时用)
docker build --no-cache -t myapp:1.0.0 .

# 多平台构建(同时构建amd64和arm64)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0.0 --push .

2.3.2 验证镜像

# 查看镜像大小
docker images myapp:1.0.0

# 查看镜像分层(每层大小和指令)
dockerhistorymyapp:1.0.0

# 查看镜像详细信息
docker inspect myapp:1.0.0

# 用dive工具分析镜像层(推荐)
# 安装:https://github.com/wagoodman/dive
dive myapp:1.0.0

# 安全扫描
docker scout cves myapp:1.0.0
# 或使用Trivy
trivy image myapp:1.0.0

# 运行测试
docker run --rm myapp:1.0.0 --version
docker run --rm -p 8080:8080 myapp:1.0.0
curl http://localhost:8080/health

三、示例代码和配置

3.1 完整配置示例

3.1.1 Node.js应用Dockerfile(生产级)

# 文件路径:Dockerfile
# Node.js生产环境Dockerfile - 多阶段构建

# 阶段1:安装依赖
FROMnode:20.11-alpine3.19AS deps
WORKDIR/app
COPYpackage.json package-lock.json ./
RUNnpm ci --production --ignore-scripts && 
  npm cache clean --force

# 阶段2:构建(如果有TypeScript编译或前端构建)
FROMnode:20.11-alpine3.19AS builder
WORKDIR/app
COPYpackage.json package-lock.json ./
RUNnpm ci --ignore-scripts
COPY. .
RUNnpm run build

# 阶段3:运行
FROMnode:20.11-alpine3.19AS runner
LABELmaintainer="ops@example.com"
LABELversion="1.0.0"

# 安装tini作为PID 1进程,正确处理信号和僵尸进程
RUNapk add --no-cache tini tzdata && 
  cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && 
 echo"Asia/Shanghai"> /etc/timezone && 
  apk del tzdata

# 创建非root用户
RUNaddgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app

WORKDIR/app

# 只复制生产依赖和构建产物
COPY--from=deps --chown=app:app /app/node_modules ./node_modules
COPY--from=builder --chown=app:app /app/dist ./dist
COPY--chown=app:app package.json ./

USERapp

ENVNODE_ENV=production
ENVPORT=3000

EXPOSE3000

HEALTHCHECK--interval=30s --timeout=5s --start-period=10s --retries=3 
  CMD wget -qO- http://localhost:3000/health ||exit1

ENTRYPOINT["/sbin/tini","--"]
CMD["node","dist/server.js"]

说明

三阶段构建:deps阶段只装生产依赖,builder阶段编译TypeScript,runner阶段只复制需要的文件

tini作为PID 1:Node.js不擅长处理信号和僵尸进程回收,tini只有几十KB,专门干这个事

npm ci而不是npm install:ci严格按照lock文件安装,保证可重复构建

3.1.2 Python应用Dockerfile(生产级)

# 文件路径:Dockerfile
# Python生产环境Dockerfile - 多阶段构建

# 阶段1:构建wheel包
FROMpython:3.12-slim-bookworm AS builder

RUNapt-get update && 
  apt-get install -y --no-install-recommends gcc libpq-dev && 
  rm -rf /var/lib/apt/lists/*

WORKDIR/build
COPYrequirements.txt .
RUNpip install --no-cache-dir --prefix=/install -r requirements.txt

# 阶段2:运行
FROMpython:3.12-slim-bookworm AS runner

# 安装运行时依赖(不需要gcc)
RUNapt-get update && 
  apt-get install -y --no-install-recommends 
    libpq5 
    curl 
    tini 
  && rm -rf /var/lib/apt/lists/*

# 创建非root用户
RUNgroupadd -g 1000 app && useradd -u 1000 -g app -s /bin/bash -m app

# 从builder阶段复制已安装的Python包
COPY--from=builder /install /usr/local

WORKDIR/app
COPY--chown=app:app . .

USERapp

ENVPYTHONUNBUFFERED=1
ENVPYTHONDONTWRITEBYTECODE=1

EXPOSE8000

HEALTHCHECK--interval=30s --timeout=5s --start-period=15s --retries=3 
  CMD curl -f http://localhost:8000/health ||exit1

ENTRYPOINT["tini","--"]
CMD["gunicorn","app.wsgi:application", 
  "--bind","0.0.0.0:8000", 
  "--workers","4", 
  "--worker-class","gvicorn.workers.UvicornWorker", 
  "--timeout","120", 
  "--access-logfile","-", 
  "--error-logfile","-"]

说明

PYTHONUNBUFFERED=1:禁用Python输出缓冲,确保日志实时输出到docker logs

PYTHONDONTWRITEBYTECODE=1:不生成.pyc文件,减少容器层大小

--prefix=/install:pip安装到独立目录,方便多阶段构建复制

gunicorn的worker数一般设为2 * CPU核心数 + 1,容器限制2核就设5个worker

3.1.3 CI/CD构建脚本

#!/bin/bash
# 文件名:build.sh
# CI/CD流水线中的镜像构建脚本

set-euo pipefail

# 变量
APP_NAME="myapp"
REGISTRY="registry.example.com"
GIT_COMMIT=$(git rev-parse --short HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
VERSION=${CI_COMMIT_TAG:-${GIT_BRANCH}-${GIT_COMMIT}}

IMAGE_NAME="${REGISTRY}/${APP_NAME}"
IMAGE_TAG="${IMAGE_NAME}:${VERSION}"
IMAGE_LATEST="${IMAGE_NAME}:latest"

echo"Building${IMAGE_TAG}"

# 构建镜像
docker build 
  --build-arg BUILD_TIME="${BUILD_TIME}"
  --build-arg GIT_COMMIT="${GIT_COMMIT}"
  --build-arg VERSION="${VERSION}"
  --label"org.opencontainers.image.created=${BUILD_TIME}"
  --label"org.opencontainers.image.revision=${GIT_COMMIT}"
  --label"org.opencontainers.image.version=${VERSION}"
  -t"${IMAGE_TAG}"
  -t"${IMAGE_LATEST}"
  .

# 安全扫描
echo"Scanning image for vulnerabilities..."
trivy image --exit-code 1 --severity HIGH,CRITICAL"${IMAGE_TAG}"
if[ $? -ne 0 ];then
 echo"ERROR: High/Critical vulnerabilities found, blocking push"
 exit1
fi

# 推送镜像
docker push"${IMAGE_TAG}"
docker push"${IMAGE_LATEST}"

echo"Successfully built and pushed${IMAGE_TAG}"

3.2 实际应用案例

案例一:镜像瘦身实战——从1.2GB到45MB

场景描述:一个Go微服务项目,原始Dockerfile直接在golang镜像中编译和运行,镜像1.2GB。通过多阶段构建+scratch基础镜像,压缩到45MB。

优化前的Dockerfile

# 优化前:1.2GB
FROMgolang:1.22
WORKDIR/app
COPY. .
RUNgo build -o server ./cmd/server
EXPOSE8080
CMD["./server"]

优化后的Dockerfile

# 优化后:45MB
FROMgolang:1.22-alpine AS builder
RUNapk add --no-cache ca-certificates git

WORKDIR/build

# 先下载依赖(利用缓存)
COPYgo.mod go.sum ./
RUNgo mod download

# 编译
COPY. .
RUNCGO_ENABLED=0 GOOS=linux GOARCH=amd64 
  go build -ldflags="-s -w -X main.version=1.0.0"
  -o /app/server ./cmd/server

# 用UPX进一步压缩二进制文件(可选,压缩率约60%)
RUNapk add --no-cache upx && upx --best /app/server

# 运行阶段
FROMscratch
COPY--from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY--from=builder /app/server /server

EXPOSE8080
ENTRYPOINT["/server"]

优化效果对比

优化前:
REPOSITORY  TAG   SIZE
myapp    v1   1.2GB
构建时间:3分12秒

优化后:
REPOSITORY  TAG   SIZE
myapp    v2   45MB
构建时间:1分05秒(有缓存时:8秒)

关键优化点

-ldflags="-s -w":去掉调试信息和符号表,二进制文件减小约30%

CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件,可以在scratch上运行

UPX压缩:二进制文件从50MB压缩到20MB,启动时有约100ms的解压开销,生产环境可以不用

scratch基础镜像:0字节,没有shell、没有包管理器、没有任何多余的东西

案例二:BuildKit缓存挂载加速构建

场景描述:Java项目每次构建都要下载Maven依赖,耗时5-8分钟。使用BuildKit的缓存挂载功能,依赖缓存在构建主机上,重复构建时间从8分钟降到40秒。

# syntax=docker/dockerfile:1
# 注意第一行的syntax指令,启用BuildKit扩展语法

FROMmaven:3.9-eclipse-temurin-17AS builder
WORKDIR/build

COPYpom.xml .
# --mount=type=cache 将Maven本地仓库缓存到构建主机
# 即使镜像层缓存失效,Maven依赖缓存仍然有效
RUN--mount=type=cache,target=/root/.m2/repository 
  mvn dependency:go-offline -B

COPYsrc ./src
RUN--mount=type=cache,target=/root/.m2/repository 
  mvn package -DskipTests -B

FROMeclipse-temurin:17-jre-alpine
RUNaddgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
WORKDIR/app
COPY--from=builder --chown=app:app /build/target/*.jar app.jar
USERapp
EXPOSE8080
ENTRYPOINT["java","-XX:MaxRAMPercentage=75.0","-jar","app.jar"]
# 构建命令(BuildKit默认启用)
docker build -t myapp:1.0.0 .

# 第一次构建:下载所有依赖,约8分钟
# 第二次构建(修改了源码):依赖从缓存读取,约40秒
# 第三次构建(修改了pom.xml):只下载新增的依赖,约1分钟

BuildKit缓存挂载类型

type=cache:持久化缓存目录,跨构建保留。适合包管理器缓存(Maven、npm、pip)

type=secret:挂载密钥文件,不会写入镜像层。适合私有仓库认证

type=ssh:转发SSH agent,用于拉取私有Git仓库

# Secret挂载示例:拉取私有npm包
RUN--mount=type=secret,id=npmrc,target=/root/.npmrc 
  npm ci --production

# 构建时传入secret
# docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp:1.0.0 .

# SSH挂载示例:拉取私有Git仓库
RUN--mount=type=ssh 
  gitclonegit@github.com:company/private-lib.git

# 构建时转发SSH
# docker build --ssh default -t myapp:1.0.0 .

四、最佳实践和注意事项

4.1 最佳实践

4.1.1 性能优化

合理利用构建缓存:把变化频率低的指令放前面(系统依赖安装),变化频率高的放后面(源码复制)。一个典型的Node.js项目,合理利用缓存后构建时间从3分钟降到15秒(只有源码变更时):

# 依赖文件单独复制,变更频率低
COPYpackage.json package-lock.json ./
RUNnpm ci --production
# 源码最后复制,变更频率高
COPY. .

使用BuildKit并行构建:多阶段构建中,没有依赖关系的阶段会自动并行执行。把独立的构建任务拆成不同阶段:

# 这两个阶段会并行执行
FROMnode:20-alpine AS frontend-builder
COPYfrontend/ .
RUNnpm run build

FROMgolang:1.22-alpine AS backend-builder
COPYbackend/ .
RUNgo build -o server

# 最终阶段合并
FROMalpine:3.19
COPY--from=frontend-builder /app/dist /www
COPY--from=backend-builder /app/server /server

减少镜像层数:合并相关的RUN指令。Docker限制最多127层,虽然一般不会超,但层数越少拉取越快。每一层都有元数据开销,合并后镜像通常小5%-10%。

4.1.2 安全加固

不在镜像中存储密钥:构建参数(ARG)和环境变量(ENV)都会被记录在镜像层中,docker history可以看到。密钥用BuildKit的secret挂载:

#  错误:密钥会留在镜像历史中
ARGNPM_TOKEN
RUNecho"//registry.npmjs.org/:_authToken=${NPM_TOKEN}"> .npmrc && 
  npm ci && rm .npmrc

#  正确:secret不会写入镜像层
RUN--mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

使用固定版本的基础镜像:不要用latest,不要用只有主版本号的tag(如node:20)。用完整的版本号+变体(如node:20.11.1-alpine3.19),确保每次构建基础镜像一致:

#  不确定性高
FROMpython:3
FROMnode:latest

#  版本锁定
FROMpython:3.12.1-slim-bookworm
FROMnode:20.11.1-alpine3.19

镜像安全扫描集成到CI:每次构建后自动扫描,HIGH和CRITICAL级别漏洞阻断发布:

# Trivy扫描,发现高危漏洞返回非0退出码
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:1.0.0

4.1.3 高可用配置

镜像仓库高可用:生产环境用Harbor搭建私有仓库,配置主从复制。构建机推送到主仓库,各机房从本地仓库拉取,避免跨机房拉取镜像的网络延迟

构建缓存持久化:CI/CD环境中,构建缓存默认在构建机本地。用docker buildx的远程缓存功能,把缓存存到仓库:

docker buildx build 
  --cache-fromtype=registry,ref=registry.example.com/myapp:buildcache 
  --cache-totype=registry,ref=registry.example.com/myapp:buildcache,mode=max 
  -t myapp:1.0.0 .

多架构支持:生产环境可能有x86和ARM混合部署,用buildx构建多架构镜像,一个tag同时支持amd64和arm64

4.2 注意事项

4.2.1 配置注意事项

警告:Dockerfile中的每个RUN、COPY、ADD指令都会创建新的镜像层。删除文件的操作如果不在同一层中执行,不会减小镜像体积——文件在上一层已经存在,新层只是标记删除。

注意ENTRYPOINT和CMD的区别:ENTRYPOINT定义容器的主进程,CMD提供默认参数。docker run后面的参数会覆盖CMD但不会覆盖ENTRYPOINT:

# ENTRYPOINT + CMD组合
ENTRYPOINT["java","-jar","app.jar"]
CMD["--spring.profiles.active=prod"]
# docker run myapp 会执行:java -jar app.jar --spring.profiles.active=prod
# docker run myapp --spring.profiles.active=dev 会执行:java -jar app.jar --spring.profiles.active=dev

注意shell形式和exec形式的区别:exec形式(JSON数组)直接执行命令,shell形式会通过/bin/sh -c执行。shell形式的进程不是PID 1,收不到SIGTERM信号:

#  shell形式:sh是PID 1,java是子进程,收不到SIGTERM
ENTRYPOINTjava -jar app.jar

#  exec形式:java是PID 1,能正确接收信号
ENTRYPOINT["java","-jar","app.jar"]

注意ARG的作用域:ARG在FROM之前定义的只能在FROM中使用,FROM之后需要重新声明:

ARGBASE_IMAGE=alpine:3.19
FROM${BASE_IMAGE}
# 这里ARG BASE_IMAGE已经失效,需要重新声明
ARGAPP_VERSION
RUNecho${APP_VERSION}

4.2.2 常见错误

错误现象 原因分析 解决方案
镜像体积异常大 没有清理包管理器缓存,或者删除操作不在同一层 安装和清理放在同一个RUN中
构建缓存总是失效 COPY . . 放在安装依赖之前,任何文件变更都导致缓存失效 先COPY依赖文件,安装依赖,再COPY源码
容器启动后立即退出 CMD/ENTRYPOINT写成了shell形式,前台进程变成后台 用exec形式,确保主进程在前台运行
构建时网络超时 构建环境无法访问外网或镜像源 配置镜像源加速,或用--network=host构建
权限拒绝错误 USER指令切换了用户但文件属主还是root COPY --chown=user:group 或 RUN chown
alpine上程序段错误 musl libc和glibc不兼容 换成slim变体或用静态编译

4.2.3 兼容性问题

版本兼容:BuildKit的--mount语法需要Docker 18.09+,# syntax=docker/dockerfile:1指令需要BuildKit启用。旧版Docker不支持这些特性

平台兼容:多架构构建需要QEMU模拟器支持非本机架构。在x86机器上构建arm64镜像,编译速度会慢5-10倍

基础镜像兼容:alpine 3.19使用musl libc 1.2.4,部分依赖glibc的二进制文件无法运行。Node.js和Go的alpine变体没问题,Python和Java的某些native扩展可能有问题

五、故障排查和监控

5.1 故障排查

5.1.1 日志查看

# 查看构建详细日志
docker build --progress=plain -t myapp:1.0.0 . 2>&1 | tee build.log

# 查看构建历史(每层的指令和大小)
dockerhistorymyapp:1.0.0

# 查看镜像元数据
docker inspect myapp:1.0.0

# 查看构建缓存使用情况
docker buildx du

# 查看BuildKit构建日志
sudo journalctl -u docker.service | grep buildkit

5.1.2 常见问题排查

问题一:构建缓存不生效

# 检查构建上下文是否有变化
# .dockerignore没有排除的文件变更会导致COPY指令缓存失效
docker build --progress=plain -t myapp:1.0.0 . 2>&1 | grep -E"CACHED|RUN|COPY"

# 查看哪一步开始缓存失效
# 输出中从"CACHED"变成非CACHED的那一步就是缓存失效点

# 常见原因:
# 1. COPY . . 之前的文件有变更(检查.dockerignore)
# 2. ARG值变了(ARG变更会导致后续所有层缓存失效)
# 3. 基础镜像更新了(FROM的镜像有新版本)

解决方案

完善.dockerignore,排除不需要的文件

把COPY拆分,先复制依赖文件,再复制源码

基础镜像用完整版本号锁定

问题二:构建过程中网络超时

# 诊断:检查构建环境网络
docker run --rm alpine ping -c 3 registry.npmjs.org
docker run --rm alpine wget -qO- https://registry.npmjs.org/ | head -1

# 使用宿主机网络构建(绕过Docker网络)
docker build --network=host -t myapp:1.0.0 .

# 配置构建时的代理
docker build 
  --build-arg HTTP_PROXY=http://proxy.example.com:8080 
  --build-arg HTTPS_PROXY=http://proxy.example.com:8080 
  --build-arg NO_PROXY=localhost,127.0.0.1,.example.com 
  -t myapp:1.0.0 .

解决方案:配置镜像源加速(npm用淘宝源,pip用清华源,Maven用阿里云源),或者在Dockerfile中设置代理环境变量。

问题三:镜像体积异常大

症状:镜像大小远超预期,比如一个Go应用镜像超过500MB

排查

# 用dive分析每一层的内容和大小
dive myapp:1.0.0

# 查看每层大小
dockerhistory--no-trunc myapp:1.0.0

# 检查是否有不必要的文件
docker run --rm myapp:1.0.0 du -sh /* 2>/dev/null | sort -rh
docker run --rm myapp:1.0.0 find / -size +10M -typef 2>/dev/null

解决

检查是否用了多阶段构建,编译工具不应该出现在最终镜像

检查RUN指令是否在同一层中清理了缓存

检查是否复制了不需要的文件(完善.dockerignore)

5.1.3 调试模式

# 在构建失败的层启动一个临时容器进行调试
# 方法1:用最后一个成功的层启动容器
docker build -t myapp:debug . 2>&1
# 找到最后成功的层ID,然后
docker run --rm -it  /bin/sh

# 方法2:在Dockerfile中插入调试指令
# 在失败的RUN之前加一个RUN ls -la /app/ 查看文件状态

# 方法3:用BuildKit的调试功能
BUILDKIT_PROGRESS=plain docker build -t myapp:1.0.0 . 2>&1 | tee build.log

# 方法4:交互式调试(Docker Desktop 4.27+)
docker debug myapp:1.0.0

5.2 性能监控

5.2.1 关键指标监控

# 监控构建时间
time docker build -t myapp:1.0.0 .

# 监控镜像大小趋势
docker images --format"{{.Repository}}:{{.Tag}} {{.Size}}"| sort

# 监控构建缓存大小
docker buildx du
docker system df

# 监控构建机磁盘使用
df -h /var/lib/docker

5.2.2 监控指标说明

指标名称 正常范围 告警阈值 说明
镜像构建时间 <5分钟 >10分钟 超过10分钟检查缓存是否失效
最终镜像大小 <200MB >500MB 超过500MB检查是否有多余文件
构建缓存大小 <20GB >50GB 定期清理构建缓存
镜像层数 <20层 >40层 层数过多影响拉取速度
安全漏洞数(HIGH+) 0 >0 高危漏洞必须修复
构建成功率 >95% <90% 低于90%检查构建环境稳定性

5.2.3 CI/CD构建监控配置

# GitLab CI中的构建监控示例:.gitlab-ci.yml
build:
stage:build
script:
 -BUILD_START=$(date+%s)
 -dockerbuild-t${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}.
 -BUILD_END=$(date+%s)
 -BUILD_TIME=$((BUILD_END-BUILD_START))
 -echo"Build time: ${BUILD_TIME}s"
 # 推送构建指标到Prometheus Pushgateway
 -|
   cat <
# Prometheus告警规则:dockerfile-build-alerts.yml
groups:
-name:docker_build_alerts
 rules:
  -alert:DockerBuildSlow
   expr:docker_build_duration_seconds>600
   for:0m
   labels:
    severity:warning
   annotations:
    summary:"项目{{ $labels.instance }}构建时间过长"
    description:"构建耗时{{ $value }}秒,超过10分钟阈值"

  -alert:DockerImageTooLarge
   expr:docker_image_size_bytes>524288000
   for:0m
   labels:
    severity:warning
   annotations:
    summary:"项目{{ $labels.instance }}镜像体积过大"
    description:"镜像大小{{ $value | humanize }},超过500MB"

  -alert:BuildCacheUsageHigh
   expr:docker_builder_cache_bytes/docker_builder_cache_limit_bytes>0.85
   for:5m
   labels:
    severity:warning
   annotations:
    summary:"构建缓存使用率过高"
    description:"缓存使用率{{ $value | humanizePercentage }}"

5.3 备份与恢复

5.3.1 备份策略

#!/bin/bash
# Dockerfile和构建配置备份脚本
# 建议纳入Git版本管理,这里是额外的备份

BACKUP_DIR="/backup/dockerfile/$(date +%Y%m%d)"
mkdir -p${BACKUP_DIR}

# 备份所有项目的Dockerfile
find /data/projects -name"Dockerfile*"-execcp --parents {}${BACKUP_DIR}/ ;

# 备份.dockerignore
find /data/projects -name".dockerignore"-execcp --parents {}${BACKUP_DIR}/ ;

# 备份构建脚本
find /data/projects -name"build.sh"-execcp --parents {}${BACKUP_DIR}/ ;

# 导出构建缓存(可选,体积可能很大)
# docker buildx prune --keep-storage 10GB

echo"Backup completed:${BACKUP_DIR}"

5.3.2 恢复流程

恢复Dockerfile:从Git仓库或备份目录恢复

重建构建缓存:第一次构建会比较慢,后续构建会自动建立缓存

验证构建:docker build -t test:latest .确认构建正常

验证镜像:运行容器并执行健康检查

六、总结

6.1 技术要点回顾

基础镜像选择:alpine变体体积最小(5-7MB),slim变体兼容性最好,distroless最安全。根据应用语言和依赖选择合适的基础镜像

多阶段构建:编译环境和运行环境分离,Go应用可以从800MB压缩到20MB,Java应用从800MB压缩到180MB

构建缓存利用:指令顺序按变更频率从低到高排列,依赖安装和源码复制分开,缓存命中时构建时间从分钟级降到秒级

安全基线:非root用户运行、固定版本基础镜像、不在镜像中存储密钥、集成安全扫描

BuildKit特性:缓存挂载(--mount=type=cache)、密钥挂载(--mount=type=secret)、并行构建,是现代Dockerfile的标配

6.2 进阶学习方向

多架构构建:使用docker buildx构建同时支持amd64和arm64的镜像,适配混合架构部署

学习资源:Docker官方文档 Multi-platform images

实践建议:在CI/CD中配置多架构构建流水线

镜像供应链安全:镜像签名(Cosign/Notary)、SBOM生成、漏洞扫描集成

学习资源:Sigstore项目、Trivy文档

实践建议:在Harbor中启用镜像签名验证策略

构建性能优化:远程构建缓存、分布式构建、构建集群

学习资源:BuildKit GitHub仓库

实践建议:配置registry类型的远程缓存,多个CI Runner共享构建缓存

6.3 参考资料

Dockerfile reference- 官方指令参考

Best practices for writing Dockerfiles- 官方最佳实践

BuildKit- BuildKit源码和文档

dive- 镜像层分析工具

Trivy- 容器安全扫描工具

distroless- Google的最小化基础镜像

附录

A. 命令速查表

# 构建命令
docker build -t 名称:tag .          # 基本构建
docker build -f Dockerfile.prod -t 名称:tag . # 指定Dockerfile
docker build --no-cache -t 名称:tag .     # 不使用缓存
docker build --build-arg KEY=VALUE -t 名称:tag .# 传入构建参数
docker build --target stage-name -t 名称:tag . # 构建到指定阶段
docker buildx build --platform linux/amd64,linux/arm64 -t 名称:tag --push .# 多架构构建

# 镜像分析
dockerhistory镜像:tag            # 查看分层历史
docker inspect 镜像:tag            # 查看镜像元数据
docker images --filter"dangling=true"    # 查看dangling镜像
dive 镜像:tag                 # 交互式分析镜像层

# 缓存管理
docker builder prune             # 清理构建缓存
docker buildx du               # 查看缓存使用量
docker buildx prune --keep-storage 10GB    # 保留10GB缓存

# 安全扫描
trivy image 镜像:tag             # 扫描镜像漏洞
docker scout cves 镜像:tag          # Docker官方扫描

B. Dockerfile指令详解

指令 作用 示例 注意事项
FROM 指定基础镜像 FROM alpine:3.19 必须是第一条指令(ARG除外)
RUN 执行命令 RUN apt-get update 每条RUN创建一层,合并减少层数
COPY 复制文件 COPY src/ /app/src/ 推荐用COPY而不是ADD
ADD 复制文件(支持解压和URL) ADD app.tar.gz /app/ 仅在需要自动解压时使用
WORKDIR 设置工作目录 WORKDIR /app 不要用RUN cd,用WORKDIR
ENV 设置环境变量 ENV NODE_ENV=production 会写入镜像元数据,不要放密钥
ARG 构建时参数 ARG VERSION=1.0 只在构建时有效,运行时不存在
EXPOSE 声明端口 EXPOSE 8080 仅声明作用,不实际映射端口
USER 切换用户 USER app 之后的指令以该用户身份执行
ENTRYPOINT 容器入口点 ENTRYPOINT ["java","-jar","app.jar"] 用exec形式(JSON数组)
CMD 默认命令/参数 CMD ["--port","8080"] 可被docker run参数覆盖
HEALTHCHECK 健康检查 HEALTHCHECK CMD curl -f http://localhost/ 生产环境必须配置
LABEL 元数据标签 LABEL version="1.0" 用于镜像管理和追溯
VOLUME 声明卷 VOLUME /data 仅声明,实际挂载在run时指定
STOPSIGNAL 停止信号 STOPSIGNAL SIGTERM 默认SIGTERM,一般不需要改

C. 术语表

术语 英文 解释
构建上下文 Build Context docker build时发送给Docker daemon的文件集合,由.dockerignore控制范围
镜像层 Image Layer Dockerfile中每条指令生成的只读文件系统层,多层叠加组成完整镜像
多阶段构建 Multi-stage Build 一个Dockerfile中使用多个FROM,前面阶段的产物可以复制到后面阶段
BuildKit BuildKit Docker新一代构建引擎,支持并行构建、缓存挂载等高级特性
构建缓存 Build Cache Docker缓存已构建的层,未变更的层直接复用,加速构建
distroless Distroless Google维护的最小化容器镜像,只包含应用运行时,没有shell和包管理器
scratch Scratch Docker的空白基础镜像,0字节,用于静态编译的程序
dangling镜像 Dangling Image 没有tag的镜像,通常是被新构建覆盖的旧镜像
OCI Open Container Initiative 容器镜像和运行时的开放标准

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    88

    文章

    11807

    浏览量

    219512
  • python
    +关注

    关注

    58

    文章

    4882

    浏览量

    90290
  • 镜像
    +关注

    关注

    0

    文章

    181

    浏览量

    11699

原文标题:Dockerfile 最佳实践:构建高效、轻量、安全镜像的完整指南

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    构建ARM64版本nacos docker镜像

    在适配过程中有大量合作伙伴用到nacos且采用容器化部署,dockerhub未提供官方镜像,因此需要在鲲鹏服务器自定义构建构建前提:Docker已部署构建
    发表于 06-16 14:29

    一文详解DockerFile基础知识

    DockerFile用来构建docker的镜像文件,可以理解为命令参数脚本。构建步骤编写一个Docker
    发表于 09-15 15:54

    Dockerfile构建环境报错如何解决?

    我正在使用以下 Dockerfile 来设置构建环境:修改local.conf文件,执行bitbake命令后,收到如下错误:ERROR: Task (virtual:native:/home
    发表于 04-11 06:17

    全面详解Dockerfile文件

    Docker 可以通过读取 Dockerfile 中的指令自动构建镜像Dockerfile 是一个文本文档,其中包含了用户创建镜像的所有命
    的头像 发表于 09-22 15:38 2635次阅读

    镜像构建Dockerfile的介绍

    Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
    的头像 发表于 09-06 09:36 2075次阅读

    Dockerfile的最佳实践

    随着应用的容器化、上云后,将伴随着 Docker 镜像构建构建 Docker 镜像成为了最基本的一步,其中 Dockerfile 便是用
    的头像 发表于 01-20 10:59 1939次阅读
    <b class='flag-5'>Dockerfile</b>的最佳实践

    Docker入门指南之什么是Dockerfile

    发时所构建镜像或者通过Dockerfile文件构建一个新的镜像开始工作 * 对于运维人员:在部署时,可以实现应用的无缝移植
    的头像 发表于 02-06 15:25 1256次阅读
    Docker入门指南之什么是<b class='flag-5'>Dockerfile</b>

    新一代更强大的镜像构建工具Earthly

    在使用 Earthly 进行构建镜像时目前强依赖于 buildkit,Earthly 通过 buildkit 支持了一些 Dockerfile 的扩展语法,同时将 Dockerfile
    的头像 发表于 03-30 11:21 1648次阅读

    Dockerfile定义Docker镜像构建过程

    了解Dockerfile Dockerfile 是一个文本文件,用于定义 Docker 镜像构建过程。它以指令的形式描述了如何构建
    的头像 发表于 09-30 10:22 3479次阅读

    如何使用dockerfile创建镜像

    Docker是一个开源的平台,用于快速构建、打包、部署应用程序的容器化工具。而Dockerfile是一个文本文件,包含了一组可自动化构建Docker镜像的指令。本文将
    的头像 发表于 11-23 09:52 1678次阅读

    手动构建Docker镜像的方法

    不推荐使用docker commit命令,而应该使用更灵活、更强大的dockerfile构建docker镜像
    的头像 发表于 08-05 15:30 1776次阅读
    手动<b class='flag-5'>构建</b>Docker<b class='flag-5'>镜像</b>的方法

    提升DevOps效率,从基础到进阶的Dockerfile编写技巧

    目录 Dockerfile 基本结构 指令 创建镜像(centos版) 创建镜像(alpine版) 基本结构 Dockerfile 是一个文本格式的配置文件,用户可以使用
    的头像 发表于 11-26 09:44 1236次阅读
    提升DevOps效率,从基础到进阶的<b class='flag-5'>Dockerfile</b>编写技巧

    Dockerfile镜像制作与Docker-Compose容器编排

    Dockerfile镜像制作 docker/podman中, 镜像是容器的基础,每次执行docker run的时候都会指定哪个基本镜像作为容器运行的基础。我们之前的docker的操作都
    的头像 发表于 01-07 11:01 1465次阅读
    <b class='flag-5'>Dockerfile</b><b class='flag-5'>镜像</b>制作与Docker-Compose容器编排

    Docker-镜像的分层-busybox镜像制作

    目录 知识点1:镜像的分层 示例:进入 docker hub查看Jenkins的Dockerfile 知识点2:base镜像 知识点3:scratch镜像 scratch
    的头像 发表于 01-15 10:44 1482次阅读
    Docker-<b class='flag-5'>镜像</b>的分层-busybox<b class='flag-5'>镜像</b>制作

    基于Docker镜像逆向生成Dockerfile

    在本文中, 我们将通过理解Docker镜像如何存储数据, 以及如何使用工具查看镜像方方面面的信息来逆向工程一个Docker镜像; 以及如何使用Python的Docker API来构建
    的头像 发表于 03-10 09:45 1883次阅读
    基于Docker<b class='flag-5'>镜像</b>逆向生成<b class='flag-5'>Dockerfile</b>