一周搞懂 Git + Docker + K8s + CI/CD:从零到自动化部署的踩坑实录

发布时间:2026/8/4 12:17:12
一周搞懂 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 Flowfeature分支开发完要先merge到develop做测试再从develop切release做预发布最后才到master。层层把关出问题的概率小很多。1.2 merge vs rebase面试必问这两个命令都能把分支合到一起但历史长得不一样# merge保留完整历史会多一个合并提交gitcheckout developgitmerge feature-xxx# rebase把提交搬到目标分支最新提交的后面历史是直线gitcheckout feature-xxxgitrebase developmerge像是两条河流汇合保留所有交汇痕迹。历史图上有个菱形。rebase像是把你的提交摘下来重新嫁接到目标分支的最新位置。历史图是一条干净的直线。我的使用原则个人分支用rebase保持历史干净团队协作用merge保留真相。rebase会改写commit hash如果别人也在你的分支上开发rebase会导致历史冲突。1.3 stash救急神器正在feature分支写代码写到一半突然要切到hotfix修bug。但代码写到一半没法commitcommit了就是半成品。怎么办# 暂存当前修改gitstash# 切到hotfix分支干活gitcheckout hotfix-xxx# ...修bug、commit、push...# 切回feature分支gitcheckout feature-xxx# 恢复暂存的修改gitstash popstash就像把桌上的文件一股脑塞进抽屉桌面干净了。回来之后从抽屉里拿出来继续干。简单粗暴但很实用。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 参数详解这个命令是日常用得最多的每个参数都得搞懂dockerrun-d--namemy-mysql-p3306:3306-eMYSQL_ROOT_PASSWORD123456-vmysql-data:/var/lib/mysql mysql:8.0# ↑ ↑ ↑ ↑ ↑ ↑# | | | | | 镜像名:版本# | | | | 数据卷挂载# | | | 环境变量# | | 端口映射宿主机:容器# | 容器名# 后台运行端口映射特别容易搞混。-p 3306:3306是宿主机端口:容器内端口。宿主机就是你自己电脑。Spring Boot连localhost:3306Docker自动转发到容器里的MySQL。2.5 数据持久化验证这一步我亲手做了之后才真正理解Volume的意义# 插入数据dockerexec-itmy-mysql mysql-uroot-p123456-eINSERT INTO bookstore.book (title) VALUES (test);# 删掉容器dockerstop my-mysqldockerrmmy-mysql# 用同一个Volume重建容器dockerrun-d--namemy-mysql-p3306:3306-eMYSQL_ROOT_PASSWORD123456-vmysql-data:/var/lib/mysql mysql:8.0# 查询数据——还在dockerexec-itmy-mysql mysql-uroot-p123456-eSELECT * FROM bookstore.book;容器删了又重建数据一条没丢。这就是Volume的作用。2.6 容器间通信的小坑我在让Spring Boot连Docker里的MySQL时踩了一个坑容器里的localhost指向容器自己不是宿主机。所以如果Spring Boot也在Docker里跑application.yml里写localhost:3306是连不上MySQL容器的。解决方案Docker自定义网络。dockernetwork create my-networkdockernetwork connect my-network my-mysqldockernetwork connect my-network my-redis同一网络内容器可以用容器名互相访问。jdbc:mysql://my-mysql:3306就行了。三、Dockerfile把自己的项目装进镜像跑MySQL和Redis用的是别人做好的镜像但我自己写的Spring Boot项目怎么打包成镜像答案就是Dockerfile。3.1 最小可用的DockerfileFROM 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 --frombuilder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]第一阶段编译出jar包第二阶段只把jar包拷贝到小的JRE镜像里。最终镜像不包含Maven、JDK和源代码干干净净。好处是宿主机不需要装Mavendocker build一步到位。3.5 .dockerignore 别忘了不写.dockerignoreDocker构建时会把.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.ymlversion:3.8services:mysql:image:mysql:8.0container_name:my-mysqlports:-3306:3306environment:MYSQL_ROOT_PASSWORD:${MYSQL_ROOT_PASSWORD}MYSQL_DATABASE:bookstorevolumes:-mysql-data:/var/lib/mysqlnetworks:-app-networkrestart:unless-stoppedredis:image:redis:7container_name:my-redisports:-6379:6379networks:-app-networkrestart:unless-stoppedbookstore:build:.container_name:my-bookstoreports:-8080:8080environment:SPRING_DATASOURCE_URL:jdbc:mysql://mysql:3306/bookstore?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghaiSPRING_DATA_REDIS_HOST:redisdepends_on:mysql:condition:service_healthyredis:condition:service_startednetworks:-app-networkrestart:unless-stoppedvolumes:mysql-data:networks:app-network:几个关键点image vs build别人的软件MySQL、Redis用image直接拉镜像自己的项目用build: .现场构建。depends_on 的坑depends_on只保证容器启动顺序不保证服务就绪。MySQL容器启动了但可能还在初始化bookstore这时候去连会报Connection refused。解决方案给MySQL配healthcheckdepends_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-composeup-d# 自动拉镜像 → 构建 → 创建网络 → 创建数据卷 → 按顺序启动容器docker-composeps# 查看状态docker-composelogs-f# 看日志docker-composedown# 停掉数据保留我验证了数据持久化docker-compose down→docker-compose up -d→ 查MySQL数据还在。Volume确实管用。五、Kubernetes当一台机器不够用时Docker Compose只能在一台机器上管理容器。如果你的项目需要多台服务器、自动扩缩容、滚动更新不中断服务——就需要K8s了。5.1 K8s的定位我的理解是K8s是容器的超级管理员。你告诉它我要3个bookstore的Pod随时在线它就会自己维护这个状态——一个挂了立刻补一个新的流量大了自动扩容代码更新了逐个替换不中断服务。这种模式叫声明式API你声明期望状态我要3个PodK8s自动确保现实等于期望。面试很爱考这个概念。5.2 五大核心资源Pod— K8s管理的最小单元。一个Pod就像一个工位通常坐一个容器偶尔坐两三个紧密合作的。你几乎不会直接创建Pod而是通过Deployment管理。Deployment— Pod的团队管理器。控制副本数replicas、自愈挂了自动补、滚动更新逐个替换不中断。这是K8s最核心的资源。apiVersion:apps/v1kind:Deploymentmetadata:name:bookstore-deploymentspec:replicas:3# 始终保持3个Pod在线selector:matchLabels:app:bookstoretemplate:metadata:labels:app:bookstorespec:containers:-name:bookstoreimage:bookstore:1.0ports:-containerPort:8080Service— 固定入口。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秒级补了一个新的你杀了一个PodK8s立刻自动补上。因为replicas: 2是声明式配置K8s拼命维护2个Pod在线这个状态。六、CI/CD让流水线替你干活6.1 CI和CD是什么CI持续集成 代码一提交自动编译自动测试。有问题立刻标红阻止合并到主分支。CD持续部署 测试通过后自动部署到服务器。以前我的流程是写完代码 → 手动push → 手动ssh到服务器 → 手动拉代码 → 手动打包 → 手动启动。每一步都是手动的忘了哪步就可能翻车。有了CI/CDpush代码之后的一切都是自动的。6.2 GitHub Actions实战GitHub Actions是GitHub自带的CI/CD服务不需要装任何东西。在仓库里放一个配置文件就行# .github/workflows/ci.ymlname:Java CIon:push:branches:[main,develop]pull_request:branches:[main]jobs:build:runs-on:ubuntu-lateststeps:-name:Checkout codeuses:actions/checkoutv4-name:Set up JDK 17uses:actions/setup-javav4with:java-version:17distribution:temurincache:maven-name:Compilerun:mvn compile-name:Run testsrun:mvn test-name:Packagerun: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是什么不是让你背定义而是看你有没有真正用过、踩过坑、理解了它为什么这么设计。所以我这一周每个概念都亲手做了一遍不是看看文档就过去了。下周开始进入微服务和中间件的学习会继续记录。如果这篇文章对你有帮助点个赞再走吧。