Docker 容器化入门:告别"在我电脑上能跑"
一、那个让全公司程序员血压飙升的瞬间
你有没有经历过这种场景?
周五下午五点,你正美滋滋地收拾东西准备下班,测试小哥突然在群里@你:“哥,你昨天提测的功能又挂了。”
你一脸懵:“不可能啊,我本地跑得好好的!”
十分钟后,你蹲在测试机前面排查,发现是 JDK 版本不对——你本地用的是 JDK 17,测试环境还是 JDK 8。改完 JDK,又发现某个系统环境变量没配。好不容易环境变量搞定了,依赖库的版本又对不上……
等全部修完,窗外天都黑了,晚饭变夜宵。
说白了,这就是经典的"在我电脑上能跑"综合症。环境不一致,就像你搬家时只带了家具没带螺丝,到了新家发现怎么装都装不上。
那有没有一种办法,能把"我家"原封不动地搬到"测试家"、"生产家"呢?
有,这就是 Docker。
二、Docker 到底是啥?用"搬家"给你讲明白
我第一次接触 Docker 的时候,也被一堆概念绕晕了:镜像、容器、仓库……听着就头大。
后来我想了个办法,用"搬家"来类比,一下子就通透了。
镜像(Image)= 搬家前的打包清单
你要搬家,得先把家里的东西一样一样打包好,列个清单。这个清单加上打包好的箱子,就是镜像。它包含了你的应用代码、运行环境、依赖库、系统配置,一应俱全。
容器(Container)= 搬进去住的房子
镜像本身不会跑,就像打包好的箱子不会自己打开。你得找个房子,把箱子拆开,家具摆好,才能真正住进去。这个"住进去"的状态,就是容器。你可以用同一个镜像,在不同的服务器上启动无数个容器。
仓库(Registry)= 搬家公司/仓库
你打包好的东西,可以存到仓库里。下次要搬到别的地方,直接从仓库拉过来就行。Docker Hub 就是最大的公共仓库,你们公司也可以搭自己的私有仓库。
还有一个更形象的比喻是俄罗斯套娃:镜像是最外面那个完整的套娃,容器是你打开后里面那一层正在运行的状态。每个容器都是独立的,互不干扰。
三、手写 Dockerfile:从"能跑"到"跑得优雅"
光理解概念不够,咱们来动手写一个 Dockerfile。
假设你有一个 Spring Boot 项目,用 Maven 构建,打包后是个 JAR 文件。最朴素的写法可能是这样的:
# 阶段一:直接基于一个带 JDK 的完整系统镜像
FROM ubuntu:22.04
# 安装 JDK 和 Maven
RUN apt-get update && apt-get install -y openjdk-17-jdk maven
# 把代码拷进去
COPY . /app
WORKDIR /app
# 编译打包
RUN mvn clean package -DskipTests
# 运行
CMD ["java", "-jar", "target/myapp.jar"]
这段 Dockerfile 能跑吗?能。但问题大了去了:
- 镜像体积爆炸:基于 Ubuntu,还装了 JDK 和 Maven,动辄 1GB 起步。
- 构建效率低:每次改一行代码,Maven 依赖都要重新下载。
- 安全风险高:带着完整的编译工具跑到生产环境,没必要。
优化思路:分层构建 + 多阶段构建
Docker 的构建是分层缓存的。咱们要做的就是:让变化少的层在前面,变化多的层在后面,这样改动代码时就能复用缓存。
另外,多阶段构建的意思是:编译用一个镜像,运行用另一个更小的镜像,最后只把 JAR 文件带过去。
优化后的 Dockerfile 长这样:
# ========== 阶段一:编译 ==========
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
WORKDIR /app
# 关键:先把 pom.xml 拷进去,单独下载依赖
# 这样只要 pom.xml 不变,这层缓存就一直复用
COPY pom.xml .
RUN mvn dependency:go-offline
# 再把源码拷进去编译
COPY src ./src
RUN mvn clean package -DskipTests
# ========== 阶段二:运行 ==========
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 只把编译好的 JAR 从 builder 阶段拷过来
COPY --from=builder /app/target/*.jar app.jar
# 暴露端口
EXPOSE 8080
# 运行
ENTRYPOINT ["java", "-jar", "app.jar"]
关键点在哪?
maven:3.9-eclipse-temurin-17-alpine和eclipse-temurin:17-jre-alpine都是 Alpine 版本,体积比 Ubuntu 小得多。- 先拷
pom.xml单独下载依赖,利用了 Docker 分层缓存,后续改源码不用重新下依赖。 --from=builder实现了多阶段构建,最终镜像只包含 JRE 和 JAR,没有 Maven、没有源码,干净清爽。
效果对比(我实际测的数据):
| 方案 | 镜像体积 | 二次构建时间 |
|---|---|---|
| 单阶段 Ubuntu 方案 | ~1.2GB | 3-5 分钟 |
| 多阶段 Alpine 方案 | ~180MB | 20-30 秒 |
这差距,相当于从绿皮火车升级到了高铁。
四、Docker Compose:一键启动整个"小区"
单个容器搞定了,但真实项目很少只有一个服务。你的 Web 应用可能要连 Redis、MySQL、Nginx,手动一个个 docker run 太麻烦了。
Docker Compose 就是来解决这个问题的。你可以把它理解成小区物业管家:一份配置文件,一键启动整个小区的所有住户。
下面这份 docker-compose.yml,可以直接拿去用:
version: "3.8"
services:
# ===== Web 应用 =====
app:
build: .
container_name: my-app
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/mydb
- SPRING_REDIS_HOST=redis
depends_on:
- mysql
- redis
networks:
- my-network
# ===== MySQL =====
mysql:
image: mysql:8.0
container_name: my-mysql
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: mydb
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- my-network
# ===== Redis =====
redis:
image: redis:7-alpine
container_name: my-redis
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- my-network
# 数据卷:容器删了,数据还在
volumes:
mysql_data:
redis_data:
# 自定义网络:容器之间可以通过服务名互相访问
networks:
my-network:
driver: bridge
几个重点解释一下:
build: .表示在当前目录找 Dockerfile 构建应用镜像。depends_on只是控制启动顺序,不保证服务已就绪。生产环境建议加健康检查。volumes挂载数据卷,这是血泪教训——不挂 volume,容器一删,MySQL 数据全没了。networks让所有容器在同一个自定义网络里,它们可以通过服务名(如mysql、redis)直接互相访问,不用记 IP。
启动命令就一行:
docker-compose up -d
停止也是一行:
docker-compose down
要连数据一起清掉:
docker-compose down -v
五、几个不得不说的踩坑记录
Docker 用起来爽,但坑也不少。我挑两个印象最深的分享给你。
坑一:容器里时区不对,日志时间差了 8 小时
有一次排查线上问题,看日志发现时间全是 UTC,跟北京时间差了 8 小时。找半天才发现是 Alpine 镜像默认没有时区数据。
解决办法,在 Dockerfile 里加上:
# Alpine 镜像需要手动安装时区数据
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
如果是非 Alpine 镜像,可以直接挂载宿主机的时区文件:
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
坑二:数据没挂 volume,容器删了数据没了
这个坑我踩过两次,刻骨铭心。
第一次是本地搭测试环境,MySQL 容器跑得好好的,我手贱执行了 docker system prune -a,把所有没挂 volume 的容器数据一起清了。测试数据全没,哭都没地方哭。
第二次是生产环境迁移,同事直接 docker run 起了个新容器,没挂 volume。结果服务器重启后容器自动重建,数据库直接归零。
核心原则:有状态服务必须挂 volume。 MySQL、Redis、Elasticsearch,但凡存数据的,volume 就是你的救命稻草。
六、容器调试:几个常用的小技巧
容器跑起来了,但出问题怎么排查?分享几个我常用的命令:
# 看容器日志
docker logs -f 容器名
# 进容器内部看看
docker exec -it 容器名 /bin/sh
# 如果是基于 Ubuntu/Debian 的镜像,可以用 bash
docker exec -it 容器名 /bin/bash
# 查看容器资源占用
docker stats
# 查看容器网络信息
docker inspect 容器名 | grep IPAddress
进容器后,你可以像操作普通 Linux 一样排查:看进程、看文件、ping 其他容器。不过要注意,很多精简镜像(比如 Alpine、distroless)连 curl、ping 都没有,需要提前安装或者换镜像调试。
七、总结:Docker 到底改变了什么?
写到这里,咱们来回顾一下:
- 问题:环境不一致导致"在我电脑上能跑"成了程序员噩梦。
- 方案:Docker 用镜像把应用和环境一起打包,做到"一次构建,到处运行"。
- 实现:通过合理的 Dockerfile 分层和多阶段构建,把镜像体积从 GB 级压到 MB 级;用 Docker Compose 一键管理多容器环境。
- 验证:镜像体积和构建效率大幅提升,环境一致性得到保障。
Docker 不是什么高深莫测的黑科技,它本质上就是一个更优雅的打包和搬家工具。理解了这一点,你会发现它其实很好上手。
当然,Docker 只是容器化的起点。再往后还有 Kubernetes、Service Mesh、CI/CD 流水线……路还长着呢。
八、聊聊你的经历
你在工作中遇到过"在我电脑上能跑"的崩溃时刻吗?是用 Docker 解决的,还是踩过别的坑?
关于 Dockerfile 优化、Docker Compose 使用,或者容器化迁移,你有什么独门技巧?欢迎在评论区交流,咱们一起进步!

3022

被折叠的 条评论
为什么被折叠?



