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,影响 RUN、CMD、ENTRYPOINT 的 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 | 自动解压 |
| 下载远程文件 | ADD 或 RUN curl | ADD 支持 URL,但 curl 更可控 |
| 复制文件到非 root 用户目录 | COPY --chown | 直接设置属主 |
原则:能用
COPY就不用ADD,ADD的自动解压和远程下载行为可能带来意外结果。
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 影响所有后续指令(RUN、COPY、CMD、ENTRYPOINT)的默认路径。
如果目录不存在怎么办?
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 tzdata 或 Geographic 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(生产环境)