Docker
这一课你将能做到
- 说清楚镜像、容器、仓库三者关系,理解容器和虚拟机的本质区别
- 为一个 Spring Boot 应用写出多阶段构建的 Dockerfile,构建出 <200MB 的生产镜像
- 用 Docker Compose 一键拉起「Spring Boot + MySQL + Redis + Nginx」完整环境
为什么要学
你肯定听过这句话:「我本地跑得好好的,到你机器上就不行」。原因是环境差异——JDK 版本、系统库、配置文件、环境变量,任何一项不同都可能导致行为不一致。Docker 的解决思路极其暴力:把应用和它需要的整个运行环境(操作系统层以上的所有东西)打包成一个镜像,这个镜像在哪里跑都一模一样。
人话:Docker 之于应用,就像集装箱之于货物。以前码头工人要懂每种货物怎么搬(每种应用怎么装环境),现在全部装进标准集装箱(镜像),码头(服务器)只需要会操作集装箱(跑容器)就行。这也是为什么 Docker 的 logo 是一头驮着集装箱的鲸鱼。
对本站技术栈而言:以前部署要在服务器上装 JDK、MySQL、Redis、Nginx,配版本、调依赖,一台新机器要折腾半天;用 Docker 后,docker compose up -d 一条命令,3 分钟完整环境就绪。
先建立图像
容器 vs 虚拟机:
虚拟机(VM) 容器(Container)
┌──────────────┐ ┌──────────────┐
│ 应用 A │ │ 应用 A │
├──────────────┤ ├──────────────┤
│ 完整客户机OS │ 重(GB级) │ 应用+依赖 │ 轻(MB级)
├──────────────┤ 启动分钟级 ├──────────────┤ 启动秒级
│ Hypervisor │ │ Docker Engine │
├──────────────┤ ├──────────────┤
│ 宿主机 OS │ │ 宿主机 OS │ 共享内核
└──────────────┘ └──────────────┘
镜像 → 容器 → 仓库的关系:
Dockerfile(配方)──build──▶ 镜像 Image(模具)──run──▶ 容器 Container(实例)
│
push │ pull
▼
仓库 Registry(Docker Hub / 阿里云 ACR)
跟着做
步骤 1:安装 Docker 并跑通 hello world
# Ubuntu 安装(一行官方脚本)
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
# 把当前用户加入 docker 组,免 sudo(重新登录生效)
usermod -aG docker $USER
# 验证
docker run hello-world
docker version
docker info
步骤 2:核心命令肌肉记忆
# 镜像操作
docker pull nginx:1.25 # 拉镜像
docker images # 看本地镜像
docker rmi nginx:1.25 # 删镜像
# 容器生命周期
docker run -d --name web -p 80:80 nginx:1.25 # 后台跑,端口映射
docker ps # 运行中的容器
docker ps -a # 所有容器(含已停止)
docker logs -f web # 实时日志
docker exec -it web bash # 进入容器
docker stop web && docker rm web
# 排查三件套
docker inspect web # 容器全部配置
docker stats # 资源占用实时面板
docker system df # 磁盘占用
步骤 3:为 Spring Boot 写多阶段 Dockerfile
在 Spring Boot 项目根目录创建 Dockerfile:
# 阶段 1:构建(用大镜像,编译完就扔)
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /build
COPY pom.xml .
# 单独拷贝 pom 先下依赖,利用 Docker 层缓存:pom 没变就不用重下
RUN mvn dependency:go-offline -q
COPY src ./src
RUN mvn package -DskipTests -q
# 阶段 2:运行(用最小 JRE 镜像)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 创建非 root 用户(安全基线)
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=build /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
docker build -t myapp:1.0.0 .
docker run -d --name myapp -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
myapp:1.0.0
curl http://localhost:8080/actuator/health
检查镜像大小:docker images myapp —— 多阶段 + alpine 应该在 150~200MB,直接用 maven 镜像跑会超过 600MB。
步骤 4:Docker Compose 编排完整技术栈
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydb?useSSL=false
SPRING_DATASOURCE_USERNAME: myuser
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} # 从 .env 文件读,不硬编码
SPRING_DATA_REDIS_HOST: redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: mydb
MYSQL_USER: myuser
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
volumes:
- mysql-data:/var/lib/mysql # 数据持久化,容器删了数据还在
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p$MYSQL_PASSWORD"]
interval: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
restart: unless-stopped
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
restart: unless-stopped
volumes:
mysql-data:
redis-data:
配套 .env(记得加进 .gitignore!):
DB_PASSWORD=Str0ng!Pass
REDIS_PASSWORD=An0ther!Pass
docker compose up -d # 一键拉起全部
docker compose ps # 看各服务状态
docker compose logs -f app # 看应用日志
docker compose down # 全部停止并删除(数据卷保留)
步骤 5:理解容器网络
Compose 自动创建一个网络,服务之间用服务名互相访问:Spring Boot 连数据库写 mysql:3306 而不是 IP。这是容器编排的核心便利之一。
docker network ls
docker compose exec app ping mysql # 在 app 容器里能 ping 通 mysql 服务名
讲清楚要点
- 镜像分层与缓存:Dockerfile 每条指令生成一层,层有缓存——内容没变的层直接复用。所以把变化少的指令(pom.xml、依赖下载)放前面,代码 COPY 放最后,构建速度差 10 倍。
- 容器是进程不是虚拟机:容器里跑的 java 进程直接出现在宿主机
ps aux 里(PID 不同而已)。容器删了里面数据就没,持久数据必须挂卷(volume)。
- 容器内必须有 1 号前台进程:容器的主进程退出 = 容器退出。
CMD ["java", "-jar", ...] 保持前台运行;不要用 CMD java -jar app.jar & 这种把主进程放后台的写法。
- 数据卷 vs 绑定挂载:volume(
mysql-data:/var/lib/mysql)由 Docker 管理,生产推荐;bind mount(./nginx.conf:/etc/nginx/nginx.conf)直接映射宿主机文件,适合配置文件。
- 多阶段构建是 Java 镜像标配:编译需要 Maven + JDK(600MB+),运行只需要 JRE(~80MB)。分两个阶段,最终镜像只含运行时,又小又安全。
你可能会懵的地方
-
容器和镜像到底什么关系?
类与对象的关系:镜像是类(只读模板),容器是对象(运行实例)。一个镜像可以跑 10 个容器,就像一个人设图纸可以盖 10 栋楼。
-
容器删了数据是不是就没了?
容器内的可写层会随容器删除而消失。但挂载的 volume 独立于容器生命周期,docker compose down 默认不删 volume,除非你加 -v。所以 MySQL 数据放 volume 里是安全的。
-
为什么我的 Java 容器被 OOM Kill 了?
老版本 JVM 不认识容器内存限制,按宿主机内存算堆大小。JDK 17 默认开启容器感知,-XX:MaxRAMPercentage=75 表示堆最大用容器内存的 75%(留 25% 给堆外)。Docker 侧用 --memory 1g 限制容器。
-
镜像 tag 不写就是 latest,有什么坑?
latest 只是默认 tag 名,不代表「最新版本」,更不自动更新。生产环境必须用明确版本号(myapp:1.2.3),否则你无法确认线上跑的到底是哪个版本,回滚也无从谈起。
-
docker-compose 和 Docker Compose 有什么区别?
docker-compose(带横杠)是老的 Python 独立工具,docker compose(空格)是 Docker 官方内置插件(Go 重写)。新版本都用后者,命令基本兼容。
错法对照
| 错法 | 问题 | 正解 |
|---|
Dockerfile 用 latest 基础镜像 | 基础镜像更新导致构建行为突变 | 锁定版本:mysql:8.0、nginx:1.25-alpine |
| 容器内用 root 跑应用 | 容器逃逸风险被放大 | Dockerfile 里 USER app |
| 密码写死在 compose 文件 | 进 Git 泄露 | .env 文件 + .gitignore |
| MySQL 不挂 volume | 容器重建数据全丢 | volumes: - mysql-data:/var/lib/mysql |
| 一个容器里塞 Nginx+Java+MySQL | 违背单一职责,无法独立伸缩 | 一个容器一个进程,用 Compose 编排 |
卡点急救
- 容器启动立刻退出:
docker logs <容器> 看报错。最常见原因:主进程启动失败(端口冲突、配置缺失、依赖服务没就绪)。Compose 里用 depends_on + healthcheck 解决启动顺序。
- 镜像越来越大,磁盘告警:
docker system df 看占用 → docker system prune -a 清理无用镜像/停止的容器/悬空层(注意会删未使用镜像)。日常养成 docker compose down 而不是只 stop 的习惯。
- 容器内连不上 MySQL:确认走的是 Compose 网络(服务名解析)而不是 localhost——容器内的 localhost 是容器自己,不是宿主机。Java 配置里写
jdbc:mysql://mysql:3306(服务名),不是 localhost:3306。
面试常问
-
Docker 和虚拟机的区别?
虚拟机通过 Hypervisor 虚拟硬件,跑完整客户机 OS,隔离强但重(GB 级、分钟级启动);容器共享宿主机内核,通过 namespace 做隔离(进程/网络/文件系统各有独立视图)、cgroup 做资源限制,轻量(MB 级、秒级启动)。隔离性 VM 更强,密度和速度容器完胜。K8s 时代也有两者结合的方案(Kata Containers)。
-
Dockerfile 里 COPY 和 ADD、CMD 和 ENTRYPOINT 的区别?
COPY 就是拷贝文件,ADD 额外支持 URL 下载和 tar 自动解压(但官方建议优先 COPY,行为更可预测)。CMD 提供默认命令可被 docker run 后的参数覆盖,ENTRYPOINT 是固定入口,CMD 内容变成 ENTRYPOINT 的参数。常见模式:ENTRYPOINT ["java", "-jar", "app.jar"] + CMD ["--spring.profiles.active=prod"]。
-
如何排查容器内的网络问题?
思路:宿主机视角 docker inspect 看网络配置和端口映射 → docker exec -it 容器 sh 进容器 → 容器内用 ss/ping/curl 测连通性 → 跨容器通信用服务名。容器镜像太精简没工具时,可以用 nicolaka/netshoot 这种排障专用镜像共享网络命名空间诊断。
自测三问
- 为什么 Dockerfile 要把
COPY pom.xml 和依赖下载放在 COPY src 之前?
docker compose down 之后 MySQL 数据还在吗?为什么?
- 容器之间互相访问应该用什么地址?为什么在容器里访问
localhost:3306 连不上宿主机 MySQL?
小结一句话
Docker 把「应用 + 环境」封装成标准镜像,让「一次构建,到处运行」成为现实,是现代 DevOps 的基石。
延伸