一周搞懂 Git + Docker + K8s + CI/CD:从零到自动化部署的踩坑实录
写代码只是第一步。代码写完之后怎么管、怎么打包、怎么部署、怎么自动上线——这才是企业真正在用的技术栈。这一周我把整个DevOps工具链从头到尾走了一遍,从Git分支管理到Docker容器化,再到K8s编排和GitHub Actions自动化。这篇文章记录我这一周学到的所有东西,附带踩坑经验和个人感悟。
为什么要学这些?
学完Spring Boot + Redis + 多线程之后,我遇到了一个很尴尬的问题:我的项目只能在我的电脑上跑。换个同学想看一下,得帮他装JDK、装MySQL、配Redis、改配置文件……搞了半天还没跑起来。
更离谱的是,我改了一版代码觉得没问题就push到main分支了,结果单元测试是挂的,把整个仓库的CI搞红了。
这两件事让我意识到:会写代码只是程序员的基本功,能把代码管好、部署好、持续交付才是真正的工作能力。 所以我花了一周时间集中攻克Git、Docker、K8s和CI/CD这四块内容。
下面按时间线分享。
一、Git:不只是 add/commit/push
之前我用Git就三板斧:git add .、git commit -m "xxx"、git push。学了之后才发现,企业里用Git远不止这么简单。
1.1 Git Flow:五个分支各司其职
Git Flow是一套分支管理模型,核心思想是不同用途的代码放在不同分支上:
master(主分支) ← 永远是线上稳定版本,不能直接在master上开发
↑
release(发布分支) ← 准备上线的版本,做最后的测试和bug修复
↑
develop(开发分支) ← 所有feature合并到这里,日常开发的主战场
↑
feature(功能分支) ← 每个新功能一个分支,做完merge回develop
hotfix(热修复分支) ← 线上紧急bug,从master切出来,修完同时merge到master和develop
我之前都是直接在master上写代码,push完事。这样做的问题显而易见——万一写了个bug直接就上生产了,连个缓冲都没有。有了Git Flow,feature分支开发完要先merge到develop做测试,再从develop切release做预发布,最后才到master。层层把关,出问题的概率小很多。
1.2 merge vs rebase:面试必问
这两个命令都能把分支合到一起,但历史长得不一样:
# merge:保留完整历史,会多一个合并提交
git checkout develop
git merge feature-xxx
# rebase:把提交"搬"到目标分支最新提交的后面,历史是直线
git checkout feature-xxx
git rebase develop
merge 像是两条河流汇合,保留所有交汇痕迹。历史图上有个"菱形"。
rebase 像是把你的提交"摘"下来,重新"嫁接"到目标分支的最新位置。历史图是一条干净的直线。
我的使用原则:个人分支用rebase保持历史干净,团队协作用merge保留真相。 rebase会改写commit hash,如果别人也在你的分支上开发,rebase会导致历史冲突。
1.3 stash:救急神器
正在feature分支写代码,写到一半,突然要切到hotfix修bug。但代码写到一半没法commit(commit了就是半成品)。怎么办?
# 暂存当前修改
git stash
# 切到hotfix分支干活
git checkout hotfix-xxx
# ...修bug、commit、push...
# 切回feature分支
git checkout feature-xxx
# 恢复暂存的修改
git stash pop
stash就像把桌上的文件一股脑塞进抽屉,桌面干净了。回来之后从抽屉里拿出来继续干。简单粗暴但很实用。
1.4 Conventional Commits:提交信息规范化
以前我写commit message都是"修复bug"、“改了个东西”、“update”……自己回头看根本不知道改了什么。
Conventional Commits是一套提交信息规范:
feat: 新增用户注册接口
fix: 修复登录时密码校验逻辑错误
refactor: 重构订单服务的超时重试机制
docs: 更新API文档中的参数说明
test: 补充UserService的单元测试
chore: 升级Spring Boot版本到3.2.1
格式是 类型(可选范围): 简短描述。好处是:看commit列表一眼就知道哪些是新功能、哪些是bug修复、哪些是重构。配合 git log --oneline 输出非常清爽。
二、Docker:终结"在我电脑上能跑"
Git管好了代码,下一个问题是环境一致性。Docker就是干这个的。
2.1 Docker到底是什么
Docker官网的定义是"容器化平台",但我觉得最直观的比喻是集装箱。
国际贸易以前运货,电视、衣服、食品混着装,每个港口搬运方式不一样。后来用标准集装箱——不管装什么,外面都一样,轮船火车卡车都能运。
Docker对软件做了同样的事。你写的代码、JDK、MySQL、Redis、配置文件……全部打包进一个"容器"。这个容器在任何装了Docker的机器上都以完全相同的方式运行。
2.2 Docker vs 虚拟机
面试必问的一个点。我总结的比喻是独栋别墅 vs 公寓楼:
虚拟机像独栋——每栋有独立的地基和水电(完整OS内核),资源开销大(GB级)、启动慢(分钟级)。Docker像公寓——所有房间共用地基和水电(共享宿主机内核),但每个房间内部独立隔离,资源开销小(MB级)、启动快(秒级)。
| 对比 | 虚拟机 | Docker容器 |
|---|---|---|
| 内核 | 各自完整内核 | 共享宿主机内核 |
| 启动 | 分钟级 | 秒级 |
| 空间 | GB级 | MB级 |
| 隔离 | 强 | 较弱(进程级) |
2.3 四大核心概念
这四个概念是Docker的基础,我用自己的话串一下:
Image(镜像) = 菜谱。只读的模板,包含运行应用所需的一切。你不能改菜谱上已经印好的内容,但你可以按菜谱做出一道菜。
Container(容器) = 炒出来的菜。镜像的运行实例,可以启动、停止、删除。同一份菜谱可以炒出很多盘(同一镜像可以启动多个容器)。
Registry(仓库) = 应用商店。Docker Hub是最大的公共仓库,几乎所有软件都有官方镜像。docker pull mysql:8.0 就是从仓库下载镜像。
Volume(数据卷) = U盘。容器删了里面的文件全没了,Volume把数据挂载到宿主机上,容器重建数据不丢。任何有状态服务(数据库)都必须用Volume。
Docker Hub → docker pull → Image → docker run → Container
↕
Volume(持久化)
2.4 docker run 参数详解
这个命令是日常用得最多的,每个参数都得搞懂:
docker run -d --name my-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v mysql-data:/var/lib/mysql mysql:8.0
# ↑ ↑ ↑ ↑ ↑ ↑
# | | | | | 镜像名:版本
# | | | | 数据卷挂载
# | | | 环境变量
# | | 端口映射(宿主机:容器)
# | 容器名
# 后台运行
端口映射特别容易搞混。-p 3306:3306 是 宿主机端口:容器内端口。宿主机就是你自己电脑。Spring Boot连 localhost:3306,Docker自动转发到容器里的MySQL。
2.5 数据持久化验证
这一步我亲手做了之后才真正理解Volume的意义:
# 插入数据
docker exec -it my-mysql mysql -uroot -p123456 -e "INSERT INTO bookstore.book (title) VALUES ('test');"
# 删掉容器
docker stop my-mysql && docker rm my-mysql
# 用同一个Volume重建容器
docker run -d --name my-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v mysql-data:/var/lib/mysql mysql:8.0
# 查询数据——还在!
docker exec -it my-mysql mysql -uroot -p123456 -e "SELECT * FROM bookstore.book;"
容器删了又重建,数据一条没丢。这就是Volume的作用。
2.6 容器间通信的小坑
我在让Spring Boot连Docker里的MySQL时踩了一个坑:容器里的 localhost 指向容器自己,不是宿主机。
所以如果Spring Boot也在Docker里跑,application.yml 里写 localhost:3306 是连不上MySQL容器的。
解决方案:Docker自定义网络。
docker network create my-network
docker network connect my-network my-mysql
docker network connect my-network my-redis
同一网络内,容器可以用容器名互相访问。jdbc:mysql://my-mysql:3306 就行了。
三、Dockerfile:把自己的项目装进镜像
跑MySQL和Redis用的是别人做好的镜像,但我自己写的Spring Boot项目怎么打包成镜像? 答案就是Dockerfile。
3.1 最小可用的Dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
就五行,逐行解释:
FROM — 选地基。基于Java 17 JRE镜像。用JRE而不是JDK,因为项目已经打成jar包了,运行时不需要编译工具。JRE镜像比JDK小一半(200MB vs 400MB),传输快、启动快、攻击面小。
WORKDIR — 选工作台。后续操作都以 /app 为基准目录。
COPY — 搬材料。把Maven打好的jar包复制进容器,重命名为app.jar。
EXPOSE — 声明端口。注意这只是个"文档声明",真正的端口映射靠 docker run -p。
ENTRYPOINT — 开机自启。容器启动时自动执行 java -jar app.jar。
3.2 ENTRYPOINT vs CMD
面试常考。简单说:ENTRYPOINT不容易被覆盖(需要 --entrypoint 参数),CMD容易被覆盖(docker run后面跟的命令会替换CMD)。Spring Boot推荐只用ENTRYPOINT,因为启动命令不需要运行时替换。
3.3 镜像分层与缓存(面试高频)
Docker镜像是一层一层叠加的,每条Dockerfile指令产生一层。构建时Docker逐层检查缓存:没变的层直接用缓存跳过,某层变了后面所有层都得重建。
所以指令顺序很重要:
# ❌ 反例:变化的文件放前面,导致后面缓存全失效
COPY target/app.jar app.jar # jar包每次变 → 缓存失效
RUN apt-get update && apt-get install -y curl # 这层也得重建
# ✅ 正例:不变的放前面,变化的放后面
RUN apt-get update && apt-get install -y curl # 不变 → 用缓存
COPY target/app.jar app.jar # 变了 → 只从这层开始重建
3.4 多阶段构建
这是个进阶技巧,解决了"构建环境和运行环境分离"的问题:
# 第一阶段:编译(用包含Maven的大镜像)
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行(只用小的JRE镜像)
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
第一阶段编译出jar包,第二阶段只把jar包"拷贝"到小的JRE镜像里。最终镜像不包含Maven、JDK和源代码,干干净净。好处是宿主机不需要装Maven,docker build 一步到位。
3.5 .dockerignore 别忘了
不写 .dockerignore,Docker构建时会把 .git(几百MB提交历史)、node_modules、IDE配置等全发过去,构建又慢又大。
.git
.idea
.vscode
node_modules
target/classes
*.log
.DS_Store
四、Docker Compose:一键编排多个容器
4.1 为什么需要Compose
手动 docker run 三个服务,每个都要记一堆参数——端口、网络、环境变量、启动顺序。三个服务就写三条长命令,项目有十个服务呢?
Docker Compose就是把所有容器的配置写进一个YAML文件,一条命令全部启动。
4.2 完整的 docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: my-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: "bookstore"
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
restart: unless-stopped
redis:
image: redis:7
container_name: my-redis
ports:
- "6379:6379"
networks:
- app-network
restart: unless-stopped
bookstore:
build: .
container_name: my-bookstore
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: "jdbc:mysql://mysql:3306/bookstore?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"
SPRING_DATA_REDIS_HOST: "redis"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
restart: unless-stopped
volumes:
mysql-data:
networks:
app-network:
几个关键点:
image vs build:别人的软件(MySQL、Redis)用 image 直接拉镜像;自己的项目用 build: . 现场构建。
depends_on 的坑:depends_on 只保证容器启动顺序,不保证服务就绪。MySQL容器启动了但可能还在初始化,bookstore这时候去连会报Connection refused。解决方案:给MySQL配healthcheck,depends_on用 condition: service_healthy。这是面试高频坑。
环境变量覆盖:注意 SPRING_DATASOURCE_URL 里的host写的是 mysql(容器名),不是localhost。在Compose定义的同一networks下,服务之间直接用服务名互相访问,DNS自动解析。用环境变量而不是改 application.yml 的好处是:本地开发时 application.yml 保持localhost配置不变,部署到Docker时通过环境变量切换。
Volume声明:文件底部 volumes: mysql-data: 声明数据卷,mysql服务里引用 mysql-data:/var/lib/mysql。docker-compose down 只删容器不删卷(数据不丢),docker-compose down -v 连卷一起删(数据全没)。
.env文件:密码等敏感配置放 .env 文件,compose里用 ${MYSQL_ROOT_PASSWORD} 引用。.env 加到 .gitignore 不提交到Git。
4.3 一条命令启动全部
docker-compose up -d
# 自动:拉镜像 → 构建 → 创建网络 → 创建数据卷 → 按顺序启动容器
docker-compose ps # 查看状态
docker-compose logs -f # 看日志
docker-compose down # 停掉(数据保留)
我验证了数据持久化:docker-compose down → docker-compose up -d → 查MySQL数据,还在。Volume确实管用。
五、Kubernetes:当一台机器不够用时
Docker Compose只能在一台机器上管理容器。如果你的项目需要多台服务器、自动扩缩容、滚动更新不中断服务——就需要K8s了。
5.1 K8s的定位
我的理解是:K8s是容器的"超级管理员"。 你告诉它"我要3个bookstore的Pod随时在线",它就会自己维护这个状态——一个挂了立刻补一个新的,流量大了自动扩容,代码更新了逐个替换不中断服务。
这种模式叫声明式API:你声明"期望状态"(我要3个Pod),K8s自动确保现实等于期望。面试很爱考这个概念。
5.2 五大核心资源
Pod — K8s管理的最小单元。一个Pod就像一个工位,通常坐一个容器(偶尔坐两三个紧密合作的)。你几乎不会直接创建Pod,而是通过Deployment管理。
Deployment — Pod的团队管理器。控制副本数(replicas)、自愈(挂了自动补)、滚动更新(逐个替换不中断)。这是K8s最核心的资源。
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookstore-deployment
spec:
replicas: 3 # 始终保持3个Pod在线
selector:
matchLabels:
app: bookstore
template:
metadata:
labels:
app: bookstore
spec:
containers:
- name: bookstore
image: bookstore:1.0
ports:
- containerPort: 8080
Service — 固定入口。Pod的IP会变(重建就变),Service提供固定地址,自动负载均衡到后面的Pod。类似公司前台——不管后面员工怎么换,前台电话永远不变。
| 类型 | 谁能访问 | 场景 |
|---|---|---|
| ClusterIP | 集群内部 | 微服务间内部通信 |
| NodePort | 外部通过节点IP:端口 | 开发测试 |
| LoadBalancer | 通过云厂商负载均衡器 | 生产环境 |
ConfigMap — 配置中心。存放配置键值对,Pod可以引用。和Docker Compose的environment类似,但更灵活(可以按Namespace隔离不同环境的配置)。
Namespace — 逻辑隔离。不同Namespace的资源互不影响。开发阶段用default就够了。
5.3 体验K8s自愈能力
这是我亲手做的最酷的一个实验:
# 确认2个Pod在运行
kubectl get pods
# bookstore-deployment-xxx-aaa 1/1 Running
# bookstore-deployment-xxx-bbb 1/1 Running
# 手动杀掉一个
kubectl delete pod bookstore-deployment-xxx-aaa
# 立刻再看
kubectl get pods
# bookstore-deployment-xxx-bbb 1/1 Running
# bookstore-deployment-xxx-ccc 0/1 ContainerCreating ← K8s秒级补了一个新的!
你杀了一个Pod,K8s立刻自动补上。因为 replicas: 2 是声明式配置,K8s拼命维护"2个Pod在线"这个状态。
六、CI/CD:让流水线替你干活
6.1 CI和CD是什么
CI(持续集成) = 代码一提交,自动编译+自动测试。有问题立刻标红,阻止合并到主分支。
CD(持续部署) = 测试通过后,自动部署到服务器。
以前我的流程是:写完代码 → 手动push → 手动ssh到服务器 → 手动拉代码 → 手动打包 → 手动启动。每一步都是手动的,忘了哪步就可能翻车。有了CI/CD,push代码之后的一切都是自动的。
6.2 GitHub Actions实战
GitHub Actions是GitHub自带的CI/CD服务,不需要装任何东西。在仓库里放一个配置文件就行:
# .github/workflows/ci.yml
name: Java CI
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Compile
run: mvn compile
- name: Run tests
run: mvn test
- name: Package
run: mvn package -DskipTests
核心语法:on 定义触发条件(push到哪些分支),jobs 定义任务(在什么虚拟机上跑),steps 定义具体步骤(每步要么用 uses 引用现成Action,要么用 run 执行命令)。
push之后打开GitHub仓库的Actions页面,看到绿色勾就是全部通过,红色叉就是某步失败了——点进去看日志就能定位问题。
✅ Java CI
├── ✅ Checkout code 5s
├── ✅ Set up JDK 17 15s
├── ✅ Compile 30s
├── ✅ Run tests 45s
└── ✅ Package 20s
我加了一个构建状态徽章到README顶部:[✅ build passing]。面试官一看就知道项目有CI,好感度直接+1。
七、整体串联:这一周我学到了什么
把这一周的东西串起来,其实是一条完整的工具链:
Git(管代码)
→ Docker(打包环境)
→ Docker Compose(编排多容器)
→ K8s(管理大规模容器)
→ CI/CD(自动化全流程)
每一层解决一个问题:Git解决"代码版本管理";Docker解决"环境一致性";Docker Compose解决"多容器协同";K8s解决"大规模容器编排和自愈";CI/CD解决"从代码到上线的自动化"。
个人感悟
这一周最大的感触是:工具的价值不在于它有多复杂,而在于它解决了什么真实痛点。
Docker的伟大之处不是它技术多牛,而是它终结了"在我电脑上能跑"这个困扰所有程序员的问题。Git Flow的价值不是分支多好看,而是让团队协作时代码不再互相打架。CI的价值不是多了条流水线,而是让你敢于频繁提交——因为你知道,有bug会被自动拦下来。
之前我觉得"运维"是别人的事,写好自己的代码就行了。但现在我明白了,在微服务和云原生的时代,开发和运维的边界越来越模糊。一个后端工程师不懂Docker、不懂CI/CD,就像一个厨师不知道怎么用烤箱——你菜谱写得再好,东西做不出来也白搭。
还有一个小感悟:面试问这些不是为了考你会不会敲命令,而是看你理解不理解背后的思想。 面试官问你"K8s的声明式API是什么",不是让你背定义,而是看你有没有真正用过、踩过坑、理解了它为什么这么设计。所以我这一周每个概念都亲手做了一遍,不是看看文档就过去了。
下周开始进入微服务和中间件的学习,会继续记录。如果这篇文章对你有帮助,点个赞再走吧。
更多推荐



所有评论(0)