Docker 基础与 Dockerfile
概述
Docker 是一个开源的容器化平台,能够将应用及其依赖打包到轻量级、可移植的容器中运行。本文档涵盖 Docker 核心概念、Dockerfile 指令详解、构建优化及生产实践。
一、Docker 核心概念
1.1 镜像(Image)
镜像是容器的只读模板,包含运行应用所需的文件系统、依赖库、环境变量和启动配置。镜像由多层只读层堆叠而成,每一层对应 Dockerfile 中的一条指令。
# 查看本地镜像
docker images
# 拉取远程镜像
docker pull nginx:1.25-alpine
# 删除镜像
docker rmi nginx:1.25-alpine
# 查看镜像构建历史
docker history nginx:1.25-alpine镜像命名规范:[仓库地址/]命名空间/镜像名[:标签],例如 registry.cn-hangzhou.aliyuncs.com/library/nginx:1.25。
1.2 容器(Container)
容器是镜像的运行实例,拥有独立的文件系统、网络栈和进程空间。容器在镜像层之上增加一个可写层(容器层),容器内发生的写入操作均记录在此层。
# 创建并启动容器
docker run -d --name my-nginx -p 8080:80 nginx:1.25-alpine
# 查看运行中的容器
docker ps
# 查看所有容器(含已停止)
docker ps -a
# 停止容器
docker stop my-nginx
# 启动已停止的容器
docker start my-nginx
# 进入容器交互终端
docker exec -it my-nginx sh
# 查看容器日志
docker logs -f my-nginx
# 删除容器
docker rm my-nginx1.3 仓库(Registry)
仓库用于存储和分发 Docker 镜像。Docker Hub 是官方公共仓库,企业也可以搭建私有仓库。
# 登录仓库
docker login registry.example.com
# 给本地镜像打标签
docker tag my-app:latest registry.example.com/my-app:v1.0
# 推送镜像到远程仓库
docker push registry.example.com/my-app:v1.0
# 从私有仓库拉取镜像
docker pull registry.example.com/my-app:v1.0常见仓库:
| 仓库 | 类型 | 说明 |
|---|---|---|
| Docker Hub | 公共 | 官方默认仓库,镜像最全 |
| Harbor | 企业级私有 | 集成安全扫描、RBAC 权限 |
| AWS ECR | 云托管 | 与 AWS 服务深度集成 |
| Alibaba ACR | 云托管 | 国内访问速度快 |
| Nexus | 私有 | 同时支持 Maven、npm 等 |
1.4 数据卷(Volume)
数据卷是 Docker 管理数据的机制,用于持久化容器数据和共享数据。数据卷独立于容器的生命周期,删除容器不会删除数据卷。
# 创建数据卷
docker volume create app-data
# 查看数据卷列表
docker volume ls
# 挂载数据卷到容器
docker run -d --name my-app -v app-data:/app/data my-app:latest
# 绑定挂载(将宿主机目录挂载到容器)
docker run -d --name my-app -v /host/path:/app/data my-app:latest
# 查看数据卷详情
docker volume inspect app-data
# 删除未被使用的数据卷
docker volume prune三种挂载方式对比:
| 方式 | 存储位置 | 适用场景 |
|---|---|---|
| Volume(数据卷) | /var/lib/docker/volumes/ | 持久化数据库数据、配置文件 |
| Bind Mount(绑定挂载) | 宿主机任意路径 | 开发热重载、日志输出 |
| tmpfs Mount(临时挂载) | 内存 | 敏感信息、缓存 |
1.5 网络模式(Network)
Docker 提供多种网络模式,控制容器间的通信和外部访问。
# 查看网络列表
docker network ls
# 创建自定义网络
docker network create --driver bridge my-network
# 指定网络运行容器
docker run -d --name app1 --network my-network my-app:latest
docker run -d --name app2 --network my-network my-app:latest
# 容器间通过服务名通信(app1 可直接 ping app2)
docker exec app1 ping app2五种网络模式:
| 模式 | 驱动 | 说明 | 适用场景 |
|---|---|---|---|
| Bridge | bridge | 默认模式,容器通过 Docker 网桥通信 | 单机多容器通信 |
| Host | host | 直接使用宿主机网络栈,无网络隔离 | 追求网络性能的场景 |
| None | null | 无网络,容器完全隔离 | 高安全场景 |
| Overlay | overlay | 跨主机容器通信 | Docker Swarm 集群 |
| Macvlan | macvlan | 容器拥有独立 MAC 地址 | 遗留应用迁移 |
二、Dockerfile 指令详解
2.1 FROM — 指定基础镜像
FROM 是 Dockerfile 的第一条有效指令,指定构建使用的基础镜像。
# 标准用法
FROM openjdk:17-jdk-slim
# 多阶段构建时命名构建阶段
FROM node:18-alpine AS builder
# 使用 ARG 变量动态指定基础镜像版本
ARG JAVA_VERSION=17
FROM openjdk:${JAVA_VERSION}-jdk-slim
# 使用 scratch 构建空基础镜像(静态编译应用)
FROM scratch最佳实践:
- 优先选择官方镜像,安全性更高
- 使用 Alpine 变体减小镜像体积
- 指定精确版本标签,避免使用
latest
2.2 RUN — 执行构建命令
RUN 在构建过程中执行命令,每条 RUN 指令创建一个新的镜像层。
# shell 格式(默认使用 /bin/sh -c)
RUN apt-get update && apt-get install -y curl
# exec 格式(不会调用 shell,推荐)
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]
# 合并多条命令减少层数(推荐做法)
RUN apt-get update \
&& apt-get install -y \
curl \
vim \
net-tools \
&& rm -rf /var/lib/apt/lists/*2.3 COPY — 复制文件
COPY 将构建上下文中的文件或目录复制到镜像中。
# 复制单个文件
COPY target/app.jar /app/app.jar
# 复制目录
COPY src/ /app/src/
# 带权限复制
COPY --chown=appuser:appgroup config/ /app/config/
# 多阶段构建中从上一阶段复制
COPY --from=builder /build/output.jar /app/app.jar
# 复制时保留文件属性
COPY --chmod=644 config.yml /app/config.yml2.4 ADD — 增强版复制
ADD 在 COPY 的基础上增加了自动解压和远程 URL 下载功能。
# 自动解压 tar 包
ADD jdk-17_linux-x64.tar.gz /usr/local/
# 从 URL 下载文件(不推荐,层缓存不稳定)
ADD https://example.com/file.tar.gz /tmp/
# 与 COPY 相同的用法
ADD config/ /app/config/ADD vs COPY 选择建议: 除非需要自动解压 tar 文件,否则始终使用 COPY。COPY 语义更清晰,不会引入意外行为。
2.5 CMD — 容器启动默认命令
CMD 指定容器启动时执行的命令,可以被 docker run 后面的参数覆盖。
# shell 格式
CMD java -jar /app/app.jar
# exec 格式(推荐,信号处理更友好)
CMD ["java", "-jar", "/app/app.jar"]
# 作为 ENTRYPOINT 的默认参数
CMD ["--server.port=8080"]2.6 ENTRYPOINT — 容器入口点
ENTRYPOINT 配置容器启动的主进程,容器将作为该进程运行。
# 固定入口点
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 配合 CMD 提供默认参数
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
CMD ["--spring.profiles.active=prod"]
# 使用 wrapper 脚本作为入口
ENTRYPOINT ["/app/entrypoint.sh"]2.7 ENV — 环境变量
ENV 设置容器内的环境变量,运行中的容器和后续指令均可使用。
# 设置单个环境变量
ENV JAVA_HOME=/usr/local/openjdk-17
# 设置多个环境变量
ENV APP_HOME=/app \
APP_PORT=8080 \
APP_PROFILE=prod
# 在 RUN 指令中使用环境变量
RUN echo "Java home is $JAVA_HOME"
# 运行时可通过 -e 覆盖
# docker run -e APP_PROFILE=dev my-app2.8 ARG — 构建参数
ARG 定义构建时可传递的参数,仅在构建过程中有效,不会保留在最终镜像中。
# 定义构建参数
ARG APP_VERSION=1.0.0
ARG BUILD_DATE
# 在构建中使用
LABEL version=${APP_VERSION}
LABEL build-date=${BUILD_DATE}
RUN echo "Building version: ${APP_VERSION}"
# 构建时传参
# docker build --build-arg APP_VERSION=2.0.0 --build-arg BUILD_DATE=2024-01-01 -t my-app .ENV vs ARG 区别:
| 特性 | ENV | ARG |
|---|---|---|
| 作用域 | 构建期 + 运行期 | 仅构建期 |
| 运行时访问 | 可访问(docker exec 可见) | 不可访问 |
| 默认值 | 可设置默认值 | 可设置默认值 |
| 覆盖方式 | -e 或 --env | --build-arg |
| 安全性 | 会保存到镜像中 | 不会保存到最终镜像 |
2.9 EXPOSE — 暴露端口
EXPOSE 声明容器运行时监听的端口,仅作为文档用途,不实际发布端口。
EXPOSE 8080
EXPOSE 443/tcp
EXPOSE 53/udp
# 实际发布端口需要在 docker run 时指定
# docker run -p 8080:8080 my-app2.10 WORKDIR — 工作目录
WORKDIR 设置当前工作目录,后续的 RUN、CMD、ENTRYPOINT、COPY、ADD 指令均在该目录下执行。
WORKDIR /app
# 等效于上面的三条 mkdir + cd
WORKDIR /app
WORKDIR src
WORKDIR main
# 最终工作目录为 /app/src/main2.11 USER — 指定运行用户
USER 指定容器运行时的用户,遵循最小权限原则,避免使用 root。
# 创建非 root 用户并切换
RUN addgroup -S appgroup \
&& adduser -S appuser -G appgroup
USER appuser:appgroup
# 切换到 root 安装依赖,再切回普通用户
USER root
RUN apk add --no-cache curl
USER appuser2.12 HEALTHCHECK — 健康检查
HEALTHCHECK 告诉 Docker 如何判断容器内的应用是否正常运行。
# 检查 HTTP 服务是否可用
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 禁用从基础镜像继承的健康检查
HEALTHCHECK NONE参数说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
--interval | 30s | 检查间隔时间 |
--timeout | 30s | 单次检查超时时间 |
--retries | 3 | 连续失败次数判定为不健康 |
--start-period | 0s | 启动宽限期,此期间失败不计入重试 |
三、CMD vs ENTRYPOINT 区别详解
3.1 核心区别
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
| 覆盖方式 | docker run 后的参数会完全替换 CMD | docker run 后的参数会追加到 ENTRYPOINT 之后 |
| 主进程定义 | 不定义入口,只提供默认命令 | 定义容器入口点 |
| 可覆盖性 | 易覆盖 | 默认不可覆盖(除非使用 --entrypoint) |
| 组合使用 | 可作为 ENTRYPOINT 的默认参数 | 与 CMD 配合实现灵活入口 |
3.2 组合使用场景
场景一:ENTRYPOINT 固定入口 + CMD 提供默认参数
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
CMD ["--spring.profiles.active=prod"]# 使用默认参数
docker run my-app
# 实际执行:java -jar /app/app.jar --spring.profiles.active=prod
# 覆盖参数
docker run my-app --spring.profiles.active=dev
# 实际执行:java -jar /app/app.jar --spring.profiles.active=dev场景二:只有 CMD
CMD ["java", "-jar", "/app/app.jar"]# 正常运行
docker run my-app
# 实际执行:java -jar /app/app.jar
# 参数完全覆盖
docker run my-app java -version
# 实际执行:java -version(原本的 jar 不会运行)场景三:只有 ENTRYPOINT
ENTRYPOINT ["ping"]# 参数追加
docker run my-ping google.com
# 实际执行:ping google.com
# 无法覆盖入口
docker run my-ping echo hello
# 实际执行:ping echo hello(仍然执行 ping)3.3 选择建议
| 场景 | 推荐方式 |
|---|---|
| 固定启动命令,偶尔改参数 | ENTRYPOINT + CMD(CMD 提供默认参数) |
| 镜像作为通用工具(如 curl、ping) | ENTRYPOINT 固定工具名,CMD 可省略 |
| 纯数据展示或一次性任务 | CMD 即可 |
| 需要接收动态参数的 wrapper 脚本 | ENTRYPOINT 指向脚本 |
四、多阶段构建
4.1 概念
多阶段构建(Multi-stage Build)允许在一个 Dockerfile 中使用多个 FROM 语句,每个 FROM 开始一个新的构建阶段。只有最后一个阶段的产物会保留到最终镜像中,中间阶段的产出可以通过 COPY --from 选择性复制。
核心优势:
- 最终镜像仅包含运行所需文件,不含构建工具链
- 不需要多个 Dockerfile 维护
- 减小镜像体积,降低攻击面
4.2 基本语法
# 第一阶段:构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ ./src/
RUN mvn package -DskipTests
# 第二阶段:运行阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]4.3 命名阶段与选择性构建
# 可以为阶段命名,方便引用
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY package.json .
RUN npm ci
COPY . .
RUN npm run build
FROM maven:3.9-eclipse-temurin-17 AS backend-builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ ./src/
RUN mvn package -DskipTests
# 最终阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=frontend-builder /app/dist/ /app/static/
COPY --from=backend-builder /app/target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]4.4 在特定阶段停止构建
# 只构建到指定阶段,用于调试
docker build --target builder -t my-app-builder .
# 使用 --target 可在 CI/CD 中分阶段构建
docker build --target test -t my-app-test .4.5 最佳实践
| 实践 | 说明 |
|---|---|
| 分离构建环境与运行环境 | 构建阶段用完整 SDK,运行阶段用精简 JRE |
| 利用构建缓存 | 先复制依赖描述文件,再复制源码 |
| 最小化复制内容 | 只复制构建产物,不复制中间文件 |
| 选择合适的运行镜像 | Alpine / slim / distroless |
五、构建优化
5.1 .dockerignore 文件
.dockerignore 用于排除构建上下文中不必要的文件,减小发送给 Docker daemon 的上下文体积,同时避免敏感文件被包含到镜像中。
# .dockerignore 示例
.git
.gitignore
node_modules
target/
*.log
.env
.env.*
.idea/
.vscode/
*.md
Dockerfile
.dockerignore为什么需要 .dockerignore: Docker build 时会将上下文目录打包发送给 daemon,忽略无关文件可以显著加快构建速度,并防止 .env 等敏感文件泄露。
5.2 层缓存利用
Docker 构建时对每条指令缓存一层,当指令和上下文均未变化时复用缓存。合理排列指令顺序可以最大化缓存命中率。
# 不利用缓存的写法(每次修改源码都需要重新下载依赖)
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY . . # 源码变化会导致 pom.xml 也变化
RUN mvn package
# 利用缓存的写法(依赖只下载一次)
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml . # 单独复制 pom.xml
RUN mvn dependency:go-offline # 下载依赖(仅 pom 变化时重新执行)
COPY src/ ./src/ # 复制源码
RUN mvn package -DskipTests # 编译打包缓存失效规则:
- 指令发生变化(如
RUN apt-get install vim->RUN apt-get install curl) COPY/ADD的文件内容发生变化- 基础镜像发生变化
- 使用了
--no-cache标志
5.3 Alpine 镜像选择
Alpine Linux 基于 musl libc 和 BusyBox,体积远小于标准发行版。
# 标准镜像 ~ 300MB
FROM openjdk:17-jdk
# Slim 镜像 ~ 200MB
FROM openjdk:17-jdk-slim
# Alpine 镜像 ~ 150MB
FROM openjdk:17-jdk-alpine
# Eclipse Temurin JRE(推荐 Java 应用)
FROM eclipse-temurin:17-jre-alpine # ~ 80MBAlpine 注意事项:
- 使用 musl libc 而非 glibc,某些原生库可能不兼容
- 包管理器为
apk,非apt或yum - 缺少一些常用命令(需手动安装)
- DNS 解析方式与 glibc 不同,偶尔有网络问题
Alpine vs Slim 选择:
| 对比项 | Alpine | Slim |
|---|---|---|
| 镜像体积 | 约 5MB(基础) | 约 30MB(基础) |
| 包管理器 | apk | apt |
| libc | musl | glibc |
| 兼容性 | 部分原生库可能不兼容 | 兼容性好 |
| 安全更新 | 更新较快 | 更新稳定 |
5.4 减少层数
每条 Dockerfile 指令都会创建一个新层,过多的层会增加镜像大小和构建时间。
# 不推荐:多条 RUN 指令,产生 3 层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 推荐:合并为一条 RUN 指令,只产生 1 层
RUN apt-get update \
&& apt-get install -y \
curl \
vim \
&& rm -rf /var/lib/apt/lists/*注意: Docker 不会为合并的命令减少磁盘占用,但会减少镜像元数据层数,拉取和推送时效率更高。
5.5 优化总览
# 完整的优化示例
FROM eclipse-temurin:17-jre-alpine AS base
# 1. 设置标签
LABEL maintainer="team@example.com" \
description="Optimized Spring Boot application"
# 2. 设置时区和安全配置
RUN apk add --no-cache tzdata curl \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata
# 3. 创建非 root 用户
RUN addgroup -S appgroup \
&& adduser -S appuser -G appgroup
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ ./src/
RUN mvn package -DskipTests
FROM base AS final
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
USER appuser:appgroup
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -sf http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--spring.profiles.active=prod"]六、完整示例:Java Spring Boot 应用多阶段构建
6.1 项目结构
my-spring-boot-app/
├── .dockerignore
├── Dockerfile
├── pom.xml
└── src/
└── main/
├── java/
└── resources/6.2 .dockerignore
.git
.gitignore
node_modules/
target/
*.log
.env
.env.*
.idea/
.vscode/
*.md
Dockerfile
.dockerignore6.3 Dockerfile
# ============================================================
# 第一阶段:Maven 构建
# ============================================================
FROM maven:3.9-eclipse-temurin-17 AS builder
# 设置工作目录
WORKDIR /build
# 优先复制 pom.xml 并下载依赖,利用缓存
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 复制源码并打包
COPY src/ ./src/
RUN mvn package -DskipTests -B
# ============================================================
# 第二阶段:产物提取(可选,用于更精细的控制)
# ============================================================
FROM eclipse-temurin:17-jre-alpine AS extractor
WORKDIR /app
# 从 builder 阶段复制构建产物
COPY --from=builder /build/target/*.jar app.jar
# 使用 Spring Boot 的 layertools 解压 jar,分离依赖和应用代码
RUN java -Djarmode=layertools -jar app.jar extract
# ============================================================
# 第三阶段:最终运行镜像
# ============================================================
FROM eclipse-temurin:17-jre-alpine
# 元数据
LABEL maintainer="team@example.com" \
description="Spring Boot 应用 - 多阶段构建示例"
# 安装运行时工具(仅用于健康检查)
RUN apk add --no-cache curl \
&& addgroup -S appgroup \
&& adduser -S appuser -G appgroup
# 设置工作目录
WORKDIR /app
# 从 extractor 阶段分层复制,最大化层缓存利用率
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
# 切换为非 root 用户
USER appuser:appgroup
# 声明端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -sf http://localhost:8080/actuator/health || exit 1
# 启动命令(CMD 提供默认参数,运行时可通过命令行覆盖)
ENTRYPOINT ["java", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", \
"app.jar"]
CMD ["--spring.profiles.active=prod"]6.4 构建与运行
# 构建镜像
docker build -t my-spring-app:1.0.0 .
# 查看镜像大小
docker images my-spring-app
# 运行容器
docker run -d \
--name my-app \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_URL=jdbc:mysql://host:3306/db \
-v app-logs:/app/logs \
--restart=always \
my-spring-app:1.0.0
# 验证健康检查
docker ps
docker inspect my-app --format='{{json .State.Health}}'
# 查看启动日志
docker logs -f my-app6.5 镜像尺寸对比
| 构建方式 | 基础镜像 | 最终大小 | 说明 |
|---|---|---|---|
| 单阶段(完整 JDK) | maven:3.9-eclipse-temurin-17 | ~400MB | 包含构建工具链,不推荐 |
| 单阶段(JRE) | eclipse-temurin:17-jre-alpine | ~180MB | 不含构建工具 |
| 多阶段(JRE + Alpine) | eclipse-temurin:17-jre-alpine | ~150MB | 推荐方式 |
| 多阶段(分层 Jar) | eclipse-temurin:17-jre-alpine | ~150MB(优化缓存) | 最佳实践 |
6.6 Docker Compose 配合使用
# docker-compose.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: my-spring-app:1.0.0
container_name: my-app
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://db:3306/my_db
- DB_USERNAME=app_user
- DB_PASSWORD=${DB_PASSWORD}
volumes:
- app-logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 60s
restart: always
depends_on:
db:
condition: service_healthy
networks:
- app-network
db:
image: mysql:8.0
container_name: my-db
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=my_db
- MYSQL_USER=app_user
- MYSQL_PASSWORD=${DB_PASSWORD}
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s
retries: 10
networks:
- app-network
volumes:
app-logs:
db-data:
networks:
app-network:
driver: bridge