文章
Docker 学习笔记:容器化与 Java 应用部署
牛耕田
暂存笔记,持续补充中。容器化是现代部署的起点,「在我电脑上能跑」的终结者。
一、核心概念:镜像、容器、仓库
| 概念 | 类比 | 说明 |
|---|---|---|
| 镜像 Image | 类 / 光盘 | 只读模板,分层存储 |
| 容器 Container | 对象 / 运行中的程序 | 镜像的运行实例,可读写层 |
| 仓库 Registry | Maven 中央仓库 | 存放镜像,如 Docker Hub、Harbor |
| Dockerfile | 源码 | 构建镜像的「配方」 |
| Volume | 移动硬盘 | 数据持久化 |
| Network | 局域网 | 容器间通信 |
镜像分层:每条 Dockerfile 指令生成一层,层可被多个镜像共享,这是 Docker 省空间的关键。
┌─────────────────┐ ← 可写层(容器运行时新增/修改)
├─────────────────┤ ← COPY app.jar
├─────────────────┤ ← RUN mvn package
├─────────────────┤ ← COPY pom.xml
├─────────────────┤ ← FROM eclipse-temurin:21-jre
└─────────────────┘
层缓存:某一层变化,其之后的所有层都会失效重建。因此变化频率低的指令放前面(如先
COPY pom.xml再COPY src)。
二、Docker vs 虚拟机
| 维度 | 虚拟机 | Docker |
|---|---|---|
| 隔离级别 | 硬件虚拟化,独立内核 | 进程级隔离,共享宿主内核 |
| 启动速度 | 分钟级 | 秒级 / 毫秒级 |
| 资源占用 | 每台 GB 级 | MB 级 |
| 镜像大小 | GB 级 | MB ~ 几百 MB |
| 隔离性 | 强 | 较弱(共享内核) |
原理:Docker 依赖 Linux 的 Namespace(隔离视图:PID、NET、MNT、UTS、IPC、USER)+ Cgroups(限制资源:CPU、内存)+ UnionFS(分层文件系统)。
三、Dockerfile 指令速查
| 指令 | 作用 | 备注 |
|---|---|---|
FROM |
指定基础镜像 | 必须是第一条 |
WORKDIR |
设置工作目录 | 优先用它,别用 RUN cd |
COPY |
复制本地文件进镜像 | 推荐,比 ADD 语义清晰 |
ADD |
复制 + 自动解压 + 支持 URL | 非必要不用 |
RUN |
构建时执行命令 | 多层合并可减小体积 |
ENV |
设置环境变量 | 构建和运行时都生效 |
ARG |
构建参数 | 仅构建期有效 |
EXPOSE |
声明端口 | 仅文档作用,不实际映射 |
CMD |
容器启动默认命令 | 可被 docker run 覆盖,只生效最后一条 |
ENTRYPOINT |
容器入口 | 不易被覆盖,常配 CMD 传参 |
VOLUME |
声明匿名卷 | 数据持久化 |
USER |
指定运行用户 | 安全实践:不要用 root |
HEALTHCHECK |
健康检查 | 配合编排系统 |
CMD vs 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 --server.port=9090 → java -jar /app.jar --server.port=9090
ENTRYPOINT/CMD一定用 exec 形式(JSON 数组):
CMD ["java","-jar","app.jar"]→ 进程 PID 为 1,能正确接收SIGTERM优雅停机。CMD java -jar app.jar→ shell 形式,会包一层/bin/sh -c,信号转发给 shell 而非 Java 进程,导致docker stop无法优雅关闭。
四、Java 应用 Dockerfile 最佳实践
方案 A:多阶段构建(推荐)
单阶段会把 Maven、源码、依赖缓存全打进镜像,动辄 1GB+。多阶段构建只把产物复制进运行镜像:
# ---------- 构建阶段 ----------
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build
# 先只复制 pom,利用层缓存避免每次改代码都重新拉依赖
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
# ---------- 运行阶段 ----------
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# 时区(日志时间对得上)
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建非 root 用户
RUN groupadd -r app && useradd -r -g app app
COPY --from=builder --chown=app:app /build/target/*.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", \
"-XX:MaxRAMPercentage=75.0", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "/app/app.jar"]
方案 B:分层 JAR(进一步优化启动与拉取速度)
Spring Boot 3 支持把依赖分层,代码改动时只重建最后一层:
FROM eclipse-temurin:21-jre AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
关键点:依赖层变化少,业务代码层变化多。分层后,改一行代码只需重新推送最小的
application层,镜像拉取从几百 MB 降到几 MB。
五、.dockerignore
必须配置,否则 node_modules、.git、target 等会被打进构建上下文,拖慢构建:
.git
.gitignore
.idea
*.iml
target/
node_modules/
dist/
*.log
Dockerfile*
docker-compose*
README.md
六、Docker Compose 编排
单机多容器编排的标准工具,用 YAML 声明式描述「服务 + 网络 + 卷」。
services:
mysql:
image: mysql:8.0
container_name: blog-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: blog
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
ports:
- "6379:6379"
app:
build:
context: .
dockerfile: Dockerfile
container_name: blog-app
depends_on:
mysql:
condition: service_healthy # 等 MySQL 真正就绪再启动
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/blog?useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- "8080:8080"
restart: unless-stopped
volumes:
mysql-data:
redis-data:
要点:
- 服务名即主机名:app 连 MySQL 用
mysql:3306,不是localhost(每个容器有独立网络命名空间)。 depends_on用condition: service_healthy,否则 MySQL 还没初始化完 app 就启动会报连接失败。restart: unless-stopped保证宿主机重启后自动拉起。- 密码用
${VAR}从.env读取,.env加入.gitignore。
常用命令:
docker compose up -d # 后台启动
docker compose ps # 查看状态
docker compose logs -f app # 跟踪日志
docker compose restart app # 重启单个服务
docker compose down # 停止并移除容器(卷保留)
docker compose down -v # 连卷一起删(危险!数据丢失)
docker compose up -d --build # 重新构建并启动
七、数据持久化与管理
# 具名卷(推荐,Docker 管理,性能好)
docker run -v mysql-data:/var/lib/mysql mysql:8
# 绑定挂载(宿主机路径,便于直接改文件)
docker run -v /data/mysql:/var/lib/mysql mysql:8
# 只读挂载
docker run -v /data/config:/app/config:ro myapp
容器无状态原则:容器内不存重要数据,全部挂卷或存外部。这样才能随时删除重建。
八、镜像瘦身技巧
| 手段 | 效果 |
|---|---|
| 多阶段构建 | 去掉构建工具链,最有效 |
| 换 slim / alpine 基础镜像 | 数百 MB → 几十 MB |
RUN 命令合并 + 清理缓存 |
减少层数与垃圾文件 |
.dockerignore |
减小构建上下文 |
| 用 JRE 而非 JDK | JRE 更小(除非需要 jcmd/jmap) |
| 分层 JAR | 提升拉取速度(体积不减) |
⚠️ alpine 的坑:使用 musl libc,某些依赖 glibc 的库(如部分 Netty 本地库、字体库)会出问题。Java 应用优先用
eclipse-temurin:21-jre-jammy(Ubuntu 系)而非 alpine,稳定优先。
九、常用运维命令
# 镜像
docker images # 列出镜像
docker build -t myapp:1.0 . # 构建
docker rmi myapp:1.0 # 删除镜像
docker image prune -a # 清理无引用镜像
# 容器
docker ps # 运行中的容器
docker ps -a # 包括已停止
docker run -d -p 8080:8080 --name app myapp:1.0
docker exec -it app /bin/sh # 进容器调试
docker logs -f --tail 100 app # 看日志
docker stats # 实时资源占用
docker stop app # 优雅停止(发 SIGTERM,10s 超时后 SIGKILL)
docker rm app # 删除容器
# 清理(谨慎)
docker system df # 查看磁盘占用
docker system prune -a # 清理所有未使用资源
排查「容器起不来」的标准套路:
docker ps -a # 1. 看状态是不是 Exited
docker logs <container> # 2. 看日志报什么错(90% 的答案在这)
docker inspect <container> # 3. 看配置、网络、退出码
docker exec -it <container> sh # 4. 进容器手动验证(能进说明没崩)
十、生产实践要点
- 不要用
latest标签,用具体版本号或 Git commit hash,保证可追溯、可回滚。 EXPOSE只是文档,端口映射靠-p或 compose 的ports。- 日志走 stdout/stderr,不要往容器内写文件(容器删除即丢)。用
docker logs或接入 ELK/Loki。 - JVM 感知容器内存限制:JDK 10+ 默认开启
UseContainerSupport,-XX:MaxRAMPercentage=75比硬编码-Xmx更适合容器。 - 健康检查必须配,否则编排系统无法判断容器是否真的可用。
- 镜像安全扫描:
docker scout cves myapp:1.0或 Trivy,定期修复基础镜像漏洞。 .env与密钥永不入镜像,通过环境变量或 Secret 注入。
十一、小结
- 镜像分层 + 构建缓存 是理解 Dockerfile 优化的钥匙:不变的放前面。
- 多阶段构建 是 Java 镜像瘦身的头号手段;分层 JAR 是拉取提速的利器。
- exec 形式写 ENTRYPOINT,否则优雅停机失效。
- Compose 用服务名当主机名,
depends_on要配service_healthy。 - 容器无状态,数据一律挂卷;日志走 stdout。
- 生产纪律:版本标签明确、非 root 运行、健康检查齐全、密钥不落盘。