Dockerfile 语法

一、Dockerfile 核心指令

1. FROM(基础镜像,必须第一行

指定基础镜像,所有指令基于此镜像运行。

# 格式
FROM <镜像名>[:标签] [AS 阶段名]

# 示例
FROM ubuntu:22.04
FROM openjdk:17-jdk-slim
FROM nginx:1.24-alpine
FROM maven:3.8-openjdk-17 AS builder   # 多阶段构建命名

最佳实践
- 固定版本标签,不要用 latest(版本不可控,生产禁忌)
- 镜像选型:slim/alpine 体积更小,优先生产使用
- 多阶段构建时,用 AS 给阶段命名,便于 COPY --from=阶段名 引用


2. LABEL(镜像元数据)

添加描述、作者、版本等信息,替代老旧 MAINTAINER

LABEL maintainer="xxx <xxx@xxx.com>"
LABEL version="1.0" 
LABEL description="生产业务应用"
LABEL org.opencontainers.image.created="2024-01-01"  # OCI 标准标签

最佳实践:使用 OCI 标准标签便于镜像扫描工具识别。


3. WORKDIR(工作目录)

设定容器内默认工作路径,后续 RUN/CMD/COPY 均在此目录执行;目录不存在自动创建。

WORKDIR /app

最佳实践
- 不要频繁使用 cd 切换目录,统一用 WORKDIR
- 建议每阶段独立设置 WORKDIR


4. COPY(复制文件/目录)

构建上下文的文件复制到容器内,只支持本地文件,不支持 URL、解压。

# 格式:COPY [--chown=用户:组] 源路径 目标路径
COPY ./app.jar /app/
COPY --chown=root:root ./conf /app/conf/

# 通配符
COPY *.sh /usr/local/bin/

# 复制时保留文件权限
COPY --chown=1000:1000 ./data /app/data/

5. ADD(高级复制)

功能包含 COPY,额外支持:URL 远程文件下载、自动解压 tar 压缩包

# 本地 tar 包自动解压
ADD app.tar.gz /app/

# 远程 URL 下载文件
ADD https://xxx.com/file.zip /tmp/

规范:普通文件复制优先用 COPY;仅解压/下载场景使用 ADD


6. RUN(构建阶段执行命令)

镜像构建过程中执行命令,每一条 RUN 都会生成一层镜像

# 单条命令
RUN apt update

# 多条命令合并(生产必用,减少镜像层数)
RUN apt update \
    && apt install -y curl vim \
    && rm -rf /var/lib/apt/lists/*

# Ubuntu 特定:清理所有缓存
RUN apt update \
    && apt install -y --no-install-recommends python3 \
    && apt clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

最佳实践
- && 拼接命令,结尾清理缓存
- Ubuntu 系列:安装软件后务必删除 apt 缓存
- 使用 --no-install-recommends 减少不必要的依赖
- 避免在 RUN 中频繁 cd,用 WORKDIR 替代


7. ENV(环境变量)

定义环境变量,构建阶段 + 容器运行阶段均生效。

# 单个变量
ENV APP_PORT=8080

# 多个变量
ENV JAVA_HOME=/usr/local/jdk \
    APP_NAME=demo-app \
    TZ=Asia/Shanghai

容器内可通过 $变量名 引用,也可 docker run -e 覆盖。

最佳实践:时区、语言等系统环境变量放在 ENV 中统一配置。


8. ARG(构建参数)

构建阶段生效,容器运行后失效,用于传参构建镜像。

# 定义参数,可指定默认值
ARG VERSION=1.0
ARG BUILD_TIME

# 在构建中使用
LABEL version=${VERSION}

# 构建时传参覆盖
# docker build --build-arg VERSION=2.0 -t app .

ARG vs ENV 对比

特性 ARG ENV
生效阶段 仅构建阶段 构建 + 运行阶段
是否保留在镜像中 不保留 保留
构建时覆盖 --build-arg 不支持
运行时覆盖 不支持 -e 覆盖
适用场景 版本号、构建参数 应用配置、运行参数

9. EXPOSE(声明端口)

声明容器对外端口,仅文档说明,不会自动映射端口
端口映射仍需在 docker run -p 完成。

EXPOSE 8080 22
# 指定协议
EXPOSE 80/tcp 53/udp

10. VOLUME(数据卷)

声明匿名挂载点,容器运行时自动挂载宿主机目录,持久化数据

VOLUME ["/app/data","/app/logs"]

适用场景:日志、数据库数据、持久化文件,避免容器删除数据丢失。

注意VOLUME 声明的目录无法在后续 RUN 中修改,建议在 Dockerfile 末尾声明。


11. USER(切换运行用户,安全规范

指定后续指令、容器启动进程的运行用户,默认 root
生产禁止容器以 root 运行,降低逃逸风险

# 先创建普通用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# 切换用户
USER appuser

# 或直接指定 uid/gid
USER 1000:1000

安全最佳实践
- 始终在 CMD/ENTRYPOINT 前切换非 root 用户
- 使用 --chown 确保文件属主正确
- 不要在 USER 后再切换回 root


12. CMD(容器启动命令)

容器运行阶段默认启动命令,docker run 后追加命令会覆盖 CMD

三种写法:

# 写法1:数组格式(推荐,不会启动 shell)
CMD ["java","-jar","app.jar"]

# 写法2:shell 格式(自动调用 /bin/sh -c)
CMD java -jar app.jar

# 写法3:作为 ENTRYPOINT 的默认参数
CMD ["--env","prod"]

一个 Dockerfile 仅最后一条 CMD 生效


13. ENTRYPOINT(入口点)

容器主启动程序,和 CMD 配合使用:

组合 效果
ENTRYPOINT 固定 容器只能运行该程序
ENTRYPOINT + CMD 默认参数 docker run 可追加参数覆盖 CMD
ENTRYPOINT,只有 CMD docker run 可完全替换命令
# 固定启动脚本,CMD 传默认参数
ENTRYPOINT ["/bin/bash","/start.sh"]
CMD ["--env","prod"]

# 运行时可覆盖 CMD
docker run app --env dev   # 实际执行:/bin/bash /start.sh --env dev

最佳实践:ENTRYPOINT 使用数组格式(exec form),确保进程接收信号。


14. HEALTHCHECK(健康检查)

检测容器进程是否存活,替代简单端口探测,Docker 内置健康状态。

# 格式:HEALTHCHECK [选项] CMD 检查命令
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=60s \
  CMD curl -f http://127.0.0.1:8080/actuator/health || exit 1

参数说明

参数 默认值 说明
--interval 30s 检查间隔
--timeout 30s 单次检查超时
--retries 3 连续失败次数触发 unhealthy
--start-period 0s 启动缓冲期(期内失败不计入)

适用场景:Java、Python、Web 服务等需要业务级健康检测的服务。

查看状态docker ps 可看到 (healthy/unhealthy) 状态。


15. ONBUILD(触发器)

当前镜像被作为其他镜像的基础镜像时,自动执行指令,多用于基础公共镜像。

# 在基础镜像中声明
ONBUILD COPY . /app/src
ONBUILD RUN make build

# 当其他镜像 FROM 该基础镜像时,这些 ONBUILD 指令自动执行

ONBUILD 会延迟执行,难以调试,生产环境谨慎使用。


16. SHELL(切换默认 shell)

修改默认 shell,影响 RUNCMDENTRYPOINT 的 shell 格式执行。

# 切换为 bash(默认是 /bin/sh -c)
SHELL ["/bin/bash", "-c"]

# 之后所有 shell 格式指令都使用 bash
RUN echo "Hello from bash"

二、.dockerignore(减少上下文体积,加速构建)

在项目根目录创建 .dockerignore 文件,排除不必要的文件进入构建上下文。

# .dockerignore 示例
# 版本控制
.git/
.gitignore

# 构建产物(这些会在容器内重新生成)
target/
*.jar
*.war

# 本地 IDE
.idea/
.vscode/
*.iml

# 日志
logs/
*.log

# 系统文件
.DS_Store
Thumbs.db

# 敏感文件
*.pem
*.key
.env

三、Dockerfile 生产环境最佳实践

序号 规范 说明
1 精简基础镜像 优先 alpine / slim,减小攻击面与体积
2 合并 RUN 指令 多条命令用 && 拼接,清理缓存,减少镜像层数
3 合理分层(缓存优化) 变动少的放前面,变动频繁的放后面
4 禁止使用 latest 标签 所有镜像固定版本,保证环境一致性
5 非 root 运行容器 创建普通用户,通过 USER 切换
6 区分 COPY / ADD 普通文件用 COPY,解压/下载才用 ADD
7 添加健康检查 线上业务容器必须配置 HEALTHCHECK
8 日志、数据目录做 VOLUME 防止容器删除数据丢失
9 不要在容器内持久化配置 配置文件通过挂载传入
10 使用多阶段构建 编译环境与运行环境分离
11 配置 .dockerignore 排除无关文件,加速构建
12 使用明确的 ARG+ENV 构建参数和运行环境变量分离

四、构建缓存原理(理解分层)

Docker 构建缓存规则:

Step 1: FROM ubuntu:22.04
        └── 缓存命中(基础镜像)

Step 2: RUN apt update && apt install -y curl
        └── 指令与之前完全一致 → 命中缓存

Step 3: COPY ./app.jar /app/
        └── 文件内容变化 → 缓存失效

Step 4: RUN java -version
        └── 因为 Step 3 缓存失效,Step 4 也重新执行

缓存策略

# 错误:代码变动会破坏依赖缓存
COPY . /app/
RUN npm install

#  正确:先复制依赖文件,利用缓存
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/

五、通用模板

5.1 模板1:Ubuntu 基础环境(通用系统镜像)

FROM ubuntu:22.04-slim

LABEL maintainer="admin" version="1.0" desc="Ubuntu 基础运行环境"

# 禁止交互式弹窗
ENV DEBIAN_FRONTEND=noninteractive \
    TZ=Asia/Shanghai

# 安装基础工具 + 清理缓存
RUN apt update \
    && apt install -y --no-install-recommends curl wget vim net-tools \
    && apt clean \
    && rm -rf /var/lib/apt/lists/* /tmp/*

# 创建普通用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app

# 声明端口
EXPOSE 80 8080

# 数据卷
VOLUME ["/app/logs","/app/data"]

# 切换普通用户
USER appuser

# 默认启动命令
CMD ["/bin/bash"]

5.2 模板2:Nginx 前端静态站点(SPA 项目)

FROM nginx:1.24-alpine

LABEL maintainer="admin" version="1.0" desc="Vue/React 静态站点"

# 删除默认配置
RUN rm -rf /etc/nginx/conf.d/default.conf

# 复制自定义 nginx 配置
COPY ./nginx/nginx.conf /etc/nginx/
COPY ./nginx/default.conf /etc/nginx/conf.d/

# 复制前端打包产物
COPY ./dist /usr/share/nginx/html

WORKDIR /usr/share/nginx/html

EXPOSE 80

# 健康检查
HEALTHCHECK --interval=20s --timeout=3s --retries=2 \
  CMD wget -q -O - http://127.0.0.1/ || exit 1

# 启动 Nginx(前台运行)
CMD ["nginx", "-g", "daemon off;"]

5.3 模板3:Java SpringBoot 项目(多阶段构建,生产推荐)

# 阶段1:编译构建
FROM maven:3.8-openjdk-17 AS builder

WORKDIR /build

# 先复制依赖文件(利用缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源码并打包
COPY src ./src
RUN mvn clean package -Dmaven.test.skip=true

# 阶段2:运行环境(最终镜像)
FROM openjdk:17-jre-slim

LABEL maintainer="admin" version="1.0" desc="SpringBoot 应用"

# 创建运行用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app

# 从构建阶段复制产物
COPY --from=builder /build/target/*.jar app.jar
COPY --from=builder --chown=appuser:appgroup /build/target/*.jar app.jar

ENV JAVA_OPT="-Xms512m -Xmx1024m" \
    TZ=Asia/Shanghai

EXPOSE 8080

VOLUME ["/app/logs"]

HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=60s \
  CMD curl -f http://127.0.0.1:8080/actuator/health || exit 1

# 切换非 root 用户
USER appuser

ENTRYPOINT ["sh", "-c", "java $JAVA_OPT -jar app.jar"]

5.4 模板4:Python 应用

FROM python:3.10-slim

LABEL maintainer="admin"

# 创建非 root 用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app

# 先复制依赖文件(缓存优化)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制业务代码
COPY --chown=appuser:appgroup . .

EXPOSE 5000

VOLUME ["/app/logs"]

HEALTHCHECK --interval=25s --timeout=4s \
  CMD python -c "import socket;s=socket.socket();s.connect(('127.0.0.1',5000))" || exit 1

USER appuser

CMD ["python", "app.py"]

5.5 模板5:Node.js 应用

FROM node:18-alpine AS builder

WORKDIR /app

# 复制依赖文件
COPY package*.json ./
RUN npm ci --only=production

# 复制源码并构建
COPY . .
RUN npm run build

# 生产镜像
FROM node:18-alpine

LABEL maintainer="admin"

RUN addgroup -g 1000 -S appgroup && adduser -u 1000 -S appuser

WORKDIR /app

COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000

USER appuser

CMD ["node", "dist/index.js"]

六、配套构建 & 运行命令

1. 构建镜像

# 基础构建
docker build -t app-demo:1.0 .

# 传参构建
docker build --build-arg VERSION=2.0 -t app-demo:2.0 .

# 不使用缓存构建(强制重新构建所有层)
docker build --no-cache -t app-demo:1.0 .

# 指定 Dockerfile 路径
docker build -f ./docker/Dockerfile -t app-demo:1.0 .

# 使用构建缓存加速(从仓库拉取缓存层)
docker build --cache-from app-demo:latest -t app-demo:1.0 .

2. 运行容器

# Java 应用标准运行命令
docker run -d \
  --name demo-service \
  --restart always \
  -p 8080:8080 \
  -v /host/config:/app/config \
  -v /host/logs:/app/logs \
  --memory 2g \
  --cpus 1 \
  app-demo:1.0

# 调试模式(进入容器)
docker run -it --rm app-demo:1.0 /bin/bash

七、常见坑与排错

问题 原因 解决方案
文件找不到 路径不在构建上下文内 用绝对路径,确认文件在上下文中
镜像层数过多 每个 RUN 生成一层 合并 RUN,用 && 连接
容器启动后立刻退出 进程以守护模式运行,或程序报错 前台运行程序,docker logs 查看日志
端口无法访问 EXPOSE 只声明不映射 必须加 -p 映射端口
权限 403 错误 运行用户无文件权限 --chown 指定属主
构建缓存不生效 变动文件位置靠前 把变动少的指令放前面
apt 安装交互卡住 有交互式弹窗 DEBIAN_FRONTEND=noninteractive
alpine 镜像缺工具 使用 apk 而非 apt apk add --no-cache curl
容器内时区不对 未配置时区 ENV TZ=Asia/Shanghai
Java 进程收不到 SIGTERM 使用了 shell 格式 使用 exec 格式:CMD ["java","-jar"]

八、多阶段构建扩展(复制指定阶段产物)

# 场景:从另一个镜像直接复制文件(不依赖构建)

# 从官方镜像复制证书
FROM alpine:latest AS certs
RUN apk add --no-cache ca-certificates

# 最终镜像只包含证书
FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 场景:从不同镜像复制多个来源
FROM node:18 AS frontend
# ... 构建前端

FROM maven:3.8 AS backend
# ... 构建后端

FROM nginx:alpine
COPY --from=frontend /app/dist /usr/share/nginx/html
COPY --from=backend /app/target/*.jar /app/

九、指令速查表

指令 作用
FROM 指定基础镜像
LABEL 镜像元数据
WORKDIR 设置工作目录
COPY 复制文件
ADD 高级复制(解压/下载)
RUN 构建时执行命令
ENV 设置环境变量
ARG 构建参数
EXPOSE 声明端口
VOLUME 声明数据卷
USER 切换运行用户
CMD 默认启动命令
ENTRYPOINT 入口点
HEALTHCHECK 健康检查
ONBUILD 触发器(谨慎使用)
SHELL 切换默认 shell

十、总结

Dockerfile 三原则
1. 分层优化:合并 RUN,变动少的放前面,利用构建缓存
2. 安全加固:固定版本标签、非 root 用户运行、健康检查
3. 体积控制:选择 alpine/slim 基础镜像,多阶段构建分离编译环境

十一、补充

COPY 完整参数

参数 说明 示例
--chown 设置文件属主(用户:用户组) COPY --chown=appuser:appgroup ./app /app
--from 多阶段构建中指定复制来源 COPY --from=builder /build/app.jar /app
--chmod 设置文件权限(数字格式) COPY --chmod=755 ./script.sh /usr/local/bin/
--link 使用硬链接而非复制(减少层大小) COPY --link ./app /app

使用场景

# 多阶段构建:从 builder 阶段复制 jar 包
COPY --from=builder /build/target/*.jar app.jar

# 复制文件并指定属主(配合非 root 用户)
COPY --chown=appuser:appgroup ./config /app/config

# 复制脚本并赋予执行权限
COPY --chmod=755 ./entrypoint.sh /entrypoint.sh

RUN 完整参数(需启用 BuildKit)

参数 说明 示例
--mount=type=cache 挂载缓存目录(跨构建复用) RUN --mount=type=cache,target=/root/.m2 mvn package
--mount=type=bind 挂载只读文件 RUN --mount=type=bind,from=base,source=/etc/hosts,target=/etc/hosts
--mount=type=tmpfs 挂载内存文件系统 RUN --mount=type=tmpfs,target=/tmp
--network=host 使用宿主机网络 RUN --network=host apt update

使用场景

# 启用 BuildKit:DOCKER_BUILDKIT=1 docker build .

# Maven 构建缓存(避免每次都下载依赖)
RUN --mount=type=cache,target=/root/.m2 mvn package -DskipTests

# npm 缓存
RUN --mount=type=cache,target=/root/.npm npm install

# apt 缓存
RUN --mount=type=cache,target=/var/cache/apt apt update && apt install -y curl

ENTRYPOINT 与 CMD 组合完整说明

写法 格式 是否可被 docker run 覆盖 适用场景
ENTRYPOINT ["exec"] + CMD ["args"] exec CMD 可覆盖 推荐:固定程序 + 可变参数
ENTRYPOINT ["exec"] exec 不能被覆盖 固定命令的容器
CMD ["exec", "args"] exec 可覆盖 简单应用,不需要固定命令
ENTRYPOINT exec + CMD args shell CMD 可覆盖 需要 shell 解析(如变量展开)
ENTRYPOINT exec shell 不能覆盖 同上,但无参数

推荐

# Java 应用标准写法
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]
# 运行时可覆盖:docker run myapp --spring.profiles.active=prod

# Nginx 标准写法
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# 无 CMD,因为 nginx 不需要额外参数

场景决策

COPY vs ADD

场景 推荐 原因
复制普通文件/目录 COPY 语义明确,不会自动解压
复制本地 tar.gz 并解压 ADD 自动解压
下载远程文件 ADDRUN curl ADD 支持 URL,但 curl 更可控
复制文件到非 root 用户目录 COPY --chown 直接设置属主

原则:能用 COPY 就不用 ADDADD 的自动解压和远程下载行为可能带来意外结果。

ENTRYPOINT vs CMD

场景 推荐
容器是固定服务(如 Java 应用、Nginx) ENTRYPOINT 固定程序,CMD 放默认参数
容器是通用环境(如 Ubuntu、Alpine) 只用 CMD,方便用户执行任意命令
需要在启动前做变量替换 使用 shell 格式 ENTRYPOINT /bin/sh -c
需要接收信号(SIGTERM 等) 使用 exec 格式 ENTRYPOINT ["java", "-jar"]

Alpine vs Slim vs Full

基础镜像 大小 适用场景
alpine ~5MB 静态编译(Go、Rust)、Node.js、Python 纯代码
slim ~30-80MB Java(JRE)、Python 依赖 C 扩展、需要 glibc
full ~100-500MB 开发/调试环境、需要大量工具

--no-install-recommends 什么时候用

# Ubuntu/Debian 基础镜像
RUN apt update && apt install -y --no-install-recommends curl \
    && apt clean && rm -rf /var/lib/apt/lists/*

# Alpine 不需要(apk 默认不装推荐包)
RUN apk add --no-cache curl

作用:不安装推荐的依赖包,可减少 20-50% 的镜像体积。

踩坑记录

踩坑 1:WORKDIR 和 RUN cd 混用,文件总是找不到

现象

FROM ubuntu:22.04
RUN cd /opt
RUN mkdir app
RUN cd app
RUN touch hello.txt

构建后进入容器,发现 /opt/app/hello.txt 不存在。

原因

Dockerfile 中每一行 RUN 都是独立 shell 会话cd 只影响当前行,下一行就回到默认目录了。

正确做法

# 方案 A:用 WORKDIR 切换目录(推荐)
WORKDIR /opt/app
RUN touch hello.txt

# 方案 B:用 && 在同一层执行
RUN cd /opt/app && touch hello.txt

# 方案 C:使用绝对路径
RUN touch /opt/app/hello.txt

教训WORKDIR 是 Dockerfile 的“持久化 cd”,RUN cd 只是临时生效。除非用 && 串联,否则不要用 cd

延伸:WORKDIR 的“持久化”到底有多持久?

FROM ubuntu:22.04
WORKDIR /opt/app
RUN pwd                          # 输出 /opt/app

WORKDIR /opt/app/config
RUN pwd                          # 输出 /opt/app/config

COPY ./entrypoint.sh .           # 复制到 /opt/app/config/entrypoint.sh

WORKDIR 影响所有后续指令(RUNCOPYCMDENTRYPOINT)的默认路径。

如果目录不存在怎么办?

WORKDIR 会自动创建不存在的目录,不需要提前 mkdir

# 不需要
# RUN mkdir -p /opt/app/data

WORKDIR /opt/app/data            # 自动创建

原则

场景 推荐做法
切换目录 永远用 WORKDIR
在某个目录下执行一组命令 WORKDIR + 多条指令
单条命令换个目录执行 RUN cd /xxx && 命令
不知道当前在哪个目录 RUN pwd 打印出来看

踩坑 2:CMD 用了 shell 格式,容器收不到 SIGTERM

现象

CMD java -jar app.jar

执行 docker stop 后,容器要等 10 秒才退出,应用没有优雅关闭。

原因

shell 格式的 CMD 会启动 /bin/sh -c "java -jar app.jar",而 java 进程是 /bin/sh 的子进程。docker stop 发送 SIGTERM 给 PID 1(/bin/sh),但 /bin/sh 不转发信号给 java

正确做法

# 使用 exec 格式(推荐)
CMD ["java", "-jar", "app.jar"]

# 或使用 exec 格式的 ENTRYPOINT
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]

验证

# 检查容器的 PID 1 是否是应用进程
docker exec myapp ps aux

# 如果 PID 1 是 sh,说明信号转发有问题

什么时候可以用 shell 格式

  • 需要做变量替换(如 ${JAVA_OPTS}
  • 容器只是临时调试用途
  • 已经用 exec 包裹:CMD exec java -jar app.jar

踩坑 3:COPY . /app 把 .git 和 node_modules 也打进去了

现象:镜像体积异常大,或构建时复制了不该复制的文件。

解决方案:使用 .dockerignore

# .dockerignore
.git/
.gitignore
node_modules/
*.log
.env
.idea/
.vscode/

踩坑 4:Alpine 镜像跑 Java 报错 "Error loading shared library"

现象

FROM alpine:3.19
COPY --from=openjdk:17-jre /usr/local/openjdk-17 /usr/local/openjdk-17
ENV JAVA_HOME=/usr/local/openjdk-17
ENV PATH=$PATH:$JAVA_HOME/bin

运行时报:Error loading shared library libc.musl-x86_64.so.1

原因

OpenJDK 官方镜像基于 glibc,Alpine 使用 musl libc,不兼容。

解决方案

# 方案 A:使用 Alpine 专用的 JDK(推荐)
FROM eclipse-temurin:17-jre-alpine

# 方案 B:在 Alpine 中安装 glibc 兼容层(不推荐,复杂)
RUN apk add --no-cache gcompat

踩坑 5:apt install 交互式卡住

现象:构建时卡在 Configuring tzdataGeographic area

原因:某些包(如 tzdata)有交互式配置界面,在非交互式环境中会卡住。

解决方案

# 在 RUN 前设置环境变量
ENV DEBIAN_FRONTEND=noninteractive

RUN apt update && apt install -y tzdata curl \
    && apt clean \
    && rm -rf /var/lib/apt/lists/*

# 安装后恢复(可选)
ENV DEBIAN_FRONTEND=

快速检查清单

写完 Dockerfile 后,逐项检查:

  • 基础镜像固定了版本(不用 latest
  • 使用了 slim/alpine 最小化镜像
  • 多条 RUN 命令用 && 合并了
  • apt 缓存已清理(rm -rf /var/lib/apt/lists/*
  • 设置了 DEBIAN_FRONTEND=noninteractive(Ubuntu/Debian)
  • 使用了 COPY 而非 ADD(除非需要解压)
  • 使用了 WORKDIR 而非 RUN cd
  • CMD 使用了 exec 格式(["cmd", "arg"]
  • 创建了非 root 用户并切换到 USER
  • 配置文件通过 VOLUME 或环境变量外部化
  • 添加了 .dockerignore 排除无关文件
  • COPY 顺序优化了缓存(依赖文件先复制)
  • 添加了 HEALTHCHECK(生产环境)