文章

Docker 学习笔记:容器化与 Java 应用部署

更新于 2026/09/20牛耕田

暂存笔记,持续补充中。容器化是现代部署的起点,「在我电脑上能跑」的终结者。

一、核心概念:镜像、容器、仓库#

概念类比说明
镜像 Image类 / 光盘只读模板,分层存储
容器 Container对象 / 运行中的程序镜像的运行实例,可读写层
仓库 RegistryMaven 中央仓库存放镜像,如 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。

常用命令:

Terminal window
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 # 重新构建并启动

七、数据持久化与管理#

Terminal window
# 具名卷(推荐,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 而非 JDKJRE 更小(除非需要 jcmd/jmap)
分层 JAR提升拉取速度(体积不减)

⚠️ alpine 的坑:使用 musl libc,某些依赖 glibc 的库(如部分 Netty 本地库、字体库)会出问题。Java 应用优先用 eclipse-temurin:21-jre-jammy(Ubuntu 系)而非 alpine,稳定优先。

九、常用运维命令#

Terminal window
# 镜像
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 # 清理所有未使用资源

排查「容器起不来」的标准套路:

Terminal window
docker ps -a # 1. 看状态是不是 Exited
docker logs <container> # 2. 看日志报什么错(90% 的答案在这)
docker inspect <container> # 3. 看配置、网络、退出码
docker exec -it <container> sh # 4. 进容器手动验证(能进说明没崩)

十、生产实践要点#

  1. 不要用 latest 标签,用具体版本号或 Git commit hash,保证可追溯、可回滚。
  2. EXPOSE 只是文档,端口映射靠 -p 或 compose 的 ports。
  3. 日志走 stdout/stderr,不要往容器内写文件(容器删除即丢)。用 docker logs 或接入 ELK/Loki。
  4. JVM 感知容器内存限制:JDK 10+ 默认开启 UseContainerSupport,-XX:MaxRAMPercentage=75 比硬编码 -Xmx 更适合容器。
  5. 健康检查必须配,否则编排系统无法判断容器是否真的可用。
  6. 镜像安全扫描:docker scout cves myapp:1.0 或 Trivy,定期修复基础镜像漏洞。
  7. .env 与密钥永不入镜像,通过环境变量或 Secret 注入。

十一、小结#

  • 镜像分层 + 构建缓存 是理解 Dockerfile 优化的钥匙:不变的放前面。
  • 多阶段构建 是 Java 镜像瘦身的头号手段;分层 JAR 是拉取提速的利器。
  • exec 形式写 ENTRYPOINT,否则优雅停机失效。
  • Compose 用服务名当主机名,depends_on 要配 service_healthy。
  • 容器无状态,数据一律挂卷;日志走 stdout。
  • 生产纪律:版本标签明确、非 root 运行、健康检查齐全、密钥不落盘。
返回主页