先聊点实际的。我刚入行那会儿,团队里部署一个 Java 服务,从编译到上线得折腾一整天:开发说环境跑不起来,测试说代码没问题,运维说环境又崩了。后来我们逐步把 Docker 和 Kubernetes 引入整个研发链路,迭代节奏直接从“周”压缩到“天”。今天这篇总结,我不打算复述官方文档,而是结合我这些年踩过的坑、调过的参,把 Docker 和 K8s 从“零散概念”串成“完整闭环”,帮助你真正理解它们是什么、怎么配合、实操时该注意什么。文章覆盖了从安装、镜像构建、容器编排到常见问题排查的完整路径,适合正在学习容器化改造的开发者、想要搭建团队 CI/CD 流程的运维,以及所有对云原生技术感兴趣的读者。
1. 整体设计与思路拆解:为什么偏偏是 Docker 和 K8s
1.1 Docker 解决的是“单机环境一致性”问题
很多人第一次接触 Docker,是被“环境不一致”逼疯的。明明代码在本机跑得好好的,一上服务器就报错,要么缺依赖,要么版本不对。Docker 的核心思路非常朴素:把你的应用和它运行所需的一切(代码、运行时、系统工具、库、配置文件)打包成一个标准化的镜像,然后用这个镜像在任意安装了 Docker 的机器上启动容器。容器里的进程直接运行在宿主机的内核之上,但通过 NameSpace 和 Cgroup 实现了文件系统、网络、进程、资源的隔离与限制。
有朋友问,这和虚拟机有什么区别?虚拟机是虚拟出一套完整的硬件,然后在这套虚拟硬件上跑一个完整的操作系统,所以很重,启动也慢。容器则直接复用宿主机内核,只额外打包了应用层和必要依赖,所以镜像体积小,启动速度是毫秒级到秒级。拿搬家的例子类比:虚拟机等于把整个房子搬走,容器等于只带行李箱和随身衣物,轻便得多。
不过,容器虽然解决了“单机”的环境一致性和资源隔离问题,但当你的服务数量多起来,只靠 Docker 命令在单机上一个一个容器去管理,很快就失控了。哪台机器跑哪个容器?容器挂了谁拉起来?流量怎么分发?存储怎么做持久化?这时候,就需要一个更高层的调度和编排平台,也就是 Kubernetes。
1.2 Kubernetes 解决的是“集群调度与服务编排”问题
Kubernetes(简称 K8s,因为首尾字母之间有 8 个字符)本质是一个容器编排平台。它的定位不是“再封装一层 Docker”,而是站在集群的视角,告诉你应该把容器放在哪些节点上、怎么保证副本数量、怎么升级、怎么发现服务、怎么暴露给外部访问。
打个比方:Docker 是单台机器上的“集装箱”,K8s 是管理一万个集装箱的“港口调度中心”。所以 Docker 和 K8s 不是二选一的关系,而是天然的上下游配合关系。K8s 集群里的工作节点(Node)上运行着容器运行时(Container Runtime),目前最常见的就是 containerd,它负责真正启动和管理容器。早期 K8s 直接调用 Docker,后来从 K8s 1.24 版本开始移除了对 Docker 的直接依赖(即 Dockershim 被移除),现在默认用 containerd 作为运行时,但我们在开发调试阶段仍然大量使用 Docker 来构建镜像和跑本地容器。这套组合拳打下来,开发、测试、生产的交付链路就统一了。
所以,我的整体思路是:先学好 Docker,打好镜像构建和容器运行的基础,然后再学 K8s 的抽象模型和核心组件。这个顺序不能反,因为 K8s 里的一切概念(Pod、Deployment、Service 等)最终都要落到容器和镜像之上,容器基础不牢,K8s 学起来会非常吃力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心概念解析与实操要点:先从 Docker 的几个核心组件说起
2.1 镜像、容器、仓库三者关系
Docker 的三大核心概念是镜像(Image)、容器(Container)和仓库(Repository)。
- 镜像:一个只读的模板,是应用的“图纸”。
- 容器:镜像运行后的实例,是应用的“房子”。
- 仓库:存放镜像的地方,相当于“图纸的档案馆”。
实操中,我们会先写一个 Dockerfile 描述镜像构建步骤,然后使用 docker build 构建出镜像,最后用 docker run 启动容器。如果要把镜像分发到其他机器,可以把镜像 push 到一个仓库,比如 Docker Hub 或者公司内部的 Harbor。
这里的要点是:镜像是分层的。每条构建指令都会生成一个只读层,多个容器共享同一镜像的底层只读层,只有写入时才发生拷贝(Copy-on-Write),这既是镜像体积优化的关键,也是很多怪癖 bug 的来源。比如你在容器里删除了某个文件,镜像层里其实还保留着它,只是在上层加一个白条文件把它遮挡了。
2.2 Dockerfile 编写心得与镜像体积优化
编写 Dockerfile 是我觉得整个 Docker 实操里最需要经验沉淀的部分。新手最容易犯的错是:把 Dockerfile 写成一个一个命令的堆叠,完全不考虑层缓存和最终镜像体积。
一个值得推荐的 Java Spring Boot 项目 Dockerfile 结构:
dockerfile复制# 多阶段构建:先用 maven 镜像编译,再用 jre 镜像运行
FROM maven:3.8.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这个 Dockerfile 有几个讲究:
- 先用
dependency:go-offline把依赖拉好,因为pom.xml变化频率比源码低,可以充分利用构建缓存,源码一变不用重新拉依赖。 - 多阶段构建让最终镜像只包含 JRE 和打好的 jar,不含 Maven 和源码,镜像体积能缩减一半以上。
- 显式声明
EXPOSE 8080,给使用镜像的人一个端口提示,但注意它并不是真正发布端口,只是“声明”。
镜像体积优化还有两个实用技巧:
- 尽量使用精简的基础镜像,比如
alpine或slim变体。但要注意 alpine 使用 musl libc,某些依赖原生库的软件可能遇到 glibc 兼容问题。 - 合并 RUN 命令,减少镜像层数。每一条 RUN、COPY、ADD 都会产生新的一层,层越多,镜像拉取和解压的时间越长。
2.3 常用 Docker 命令清单与关键参数
我不打算列全所有命令,只挑日常使用频率最高的几个,并且重点说明那些容易被忽略但影响很大的参数。
bash复制# 构建镜像,注意最后的构建上下文路径
docker build -t myapp:1.0.0 .
# 启动并进入容器,-d 后台运行,-p 端口映射,--name 指定名称
docker run -d -p 8080:8080 --name myapp myapp:1.0.0
# 查看容器日志,-f 是持续跟踪
docker logs -f myapp
# 进入正在运行的容器内部调试,-it 分配交互式终端
docker exec -it myapp bash
# 清理所有停止的容器、未使用的网络、悬空镜像和构建缓存
docker system prune -af
这里特别提醒几个参数:
-p 8080:8080是“宿主机端口:容器端口”,容器内端口由应用监听,宿主机端口对外暴露,两者都可以改,但别搞反。-v /host/path:/container/path是数据卷挂载,宿主机目录会“盖”到容器目录上。这样容器销毁后数据还在宿主机上,日志和配置也方便查看修改。--network host让容器直接使用宿主机网络栈,性能最好但不建议在开发外的场景滥用,因为它会牺牲端口隔离。
2.4 Docker Desktop 安装与 Windows 上的经典报错
很多 Windows 用户的第一个拦路虎是 Docker Desktop 安装后启动失败。热搜里出现频率很高的一条报错是:Docker Desktop failed to start because virtualization support wasn’t detected。
这个报错的本质是 Docker Desktop 依赖硬件虚拟化来运行 Linux 虚拟机(WSL2 或 Hyper-V 后端),而你的电脑没有开启相关功能。处理步骤按顺序排查:
- 进入 BIOS/UEFI,找到 Intel Virtualization Technology(VT-x)或 AMD SVM Mode,设置为 Enabled,保存重启。
- Windows 功能里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,然后以管理员身份运行 PowerShell,执行
wsl --update更新 WSL2 内核。 - 在 PowerShell 执行
wsl -l -v检查默认发行版版本是否为 2,如果显示 1,用wsl --set-version <发行版名> 2转换。 - 确认以上操作后重新启动 Docker Desktop,通常就能正常启动了。
便宜大碗的办法:如果你实在不想折腾 WSL2 和 Hyper-V,也可以在 Linux 虚拟机里装原生 Docker Engine,但开发体验会打折扣,文件挂载和端口映射都要多一层网络转换。
3. 实操过程与核心环节实现:从 IDEA 打包镜像到部署到 K8s
3.1 用 IDEA 一站式打包 Docker 镜像
“Idea 打包 docker 镜像”也是热搜词,说明很多开发者在本地开发完成后,希望用 IDE 直接完成镜像构建和推送,而不用开命令行敲 docker build。IDEA 官方插件对 Docker 的支持已经很成熟,我通常这么操作:
- 安装并启用 IDEA 的 Docker 插件(新版 IDEA 默认自带)。
- 在 Settings -> Build, Execution, Deployment -> Docker 里添加 Docker 服务,本机开发选 TCP socket 或 Docker Desktop 自动检测到的管道即可。
- 在项目根目录创建
Dockerfile(内容参考上一节的多阶段示例)。 - 右键 Dockerfile,选择 “Build Image on Docker”,弹出窗口里可以输入镜像名和 tag。
- 构建完成后,在 IDEA 的 Services 窗口能看到镜像列表,右键也能直接 Run 成容器,进行运行时日志查看和环境变量配置。
这套流程可以覆盖日常大部分开发场景。不过真正上线时,我还是建议用 CI 流水线(比如 Jenkins 或 GitLab CI)统一构建,而不是依赖个人 IDE 环境,因为 IDE 构建的镜像可能携带本地环境变量、平台架构等不确定因素,而 CI 的环境是干净的。
3.2 K8s 核心概念和集群模型:Pod、Deployment、Service、Namespace
K8s 的学习曲线主要来自它那一整套抽象概念。我按“最小作战单元 -> 工作负载 -> 网络暴露 -> 命名空间”这条线来梳理。
- Pod:K8s 最小的调度单位,里面可以有一个或多个容器,它们共享同一个网络命名空间、IP 地址和存储卷。通常一个 Pod 只放一个主业务容器,但可以在旁边挂一个 Sidecar 容器,比如负责日志收集、流量劫持的辅助容器。
- Deployment:管理 Pod 副本的“控制器”。你在 Deployment 里声明期望的副本数、镜像版本、资源限制,Deployment 会通过 ReplicaSet 持续保证实际运行的 Pod 数量等于期望值。升级时用滚动更新(RollingUpdate)策略,逐批替换旧 Pod,期间服务不会中断。
- Service:Pod 的 IP 是不稳定的,每次重建都可能变化,所以要用 Service 提供一个稳定的访问入口。Service 通过标签选择器把流量转发到后端的 Pod。常用类型有 ClusterIP(集群内部访问)、NodePort(通过节点 IP 和端口访问)、LoadBalancer(云环境负载均衡器)。
- Namespace:逻辑隔离的分区。可以按环境(dev、test、prod)或按团队划分资源范围,互相之间默认是隔离的。
举个例子,一个常见的部署声明长这样:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: dev
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:1.0.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: myapp-svc
namespace: dev
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
这个 YAML 里面有几个点值得展开:
resources.requests是向调度器声明“我最少需要这么多资源”,用于 Pod 调度时的资源预估;limits是“最多只能用这么多”,超过会被限制或杀掉。requests 和 limits 一定要设置,否则一个失控的容器可能拖垮整个节点。readinessProbe是就绪探针,Pod 只有通过探针才会被标记为就绪,Service 才会把流量转发过来,避免把请求打到还在启动中的服务上。生产环境我强烈建议同时配置livenessProbe(存活探针),用于发现应用死锁或卡死,帮助 K8s 自动重启异常实例。
3.3 kubectl 核心操作与调试技巧
K8s 命令行工具 kubectl 是日常操作的主入口。我给你整理了一套我自己的操作习惯:
bash复制# 创建或更新资源对象,-f 指定 YAML 文件
kubectl apply -f deploy.yaml
# 查看所有命名空间的 Pod,-w 是持续观察变化
kubectl get pods -A -w
# 查看某个 Pod 的详细事件和状态
kubectl describe pod myapp-xxx
# 进入 Pod 内的容器排查问题
kubectl exec -it myapp-xxx -- bash
# 临时端口转发,把本地端口映射到集群内 Service
kubectl port-forward svc/myapp-svc 8080:80
# 查看日志,-l 按标签筛选,-f 持续跟踪
kubectl logs -l app=myapp -f
调试时最重要的命令是 kubectl describe,因为它会显示 Pod 的事件列表,包括镜像拉取失败、资源不足被驱逐、健康检查失败被重启等。这张事件表比任何日志都直白。
还有一个容器排查技巧:kubectl debug 可以在目标 Pod 里注入一个额外的调试容器,共享进程命名空间,当你的主容器里没有 bash 或 curl 时,用这个临时容器实在太香了。
3.4 从 Docker 到 K8s:镜像推送、拉取与私有仓库配置
想把自己的镜像部署到 K8s 集群,镜像必须先推到仓库里。Docker Hub 对公开镜像免费,但我们内部项目一般会用 Harbor 或 Nexus 搭建私有仓库。
当你使用私有仓库时,K8s 节点拉取镜像需要凭证,有两种常用方式:
- 在每个节点上执行
docker login <私有仓库地址>,凭证会写入~/.docker/config.json,K8s 使用 kubelet 配置中的imagePullSecrets引用这些凭证。 - 更规范的方式是创建 Secret:
bash复制kubectl create secret docker-registry registry-key \
--namespace=dev \
--docker-server=harbor.example.com \
--docker-username=admin \
--docker-password=your-password
然后在 Deployment 的 spec 里加上:
yaml复制spec:
imagePullSecrets:
- name: registry-key
这里有个大坑:不同 Namespace 之间的 Secret 不共享。如果你在 dev 命名空间创建了 registry-key,在 test 命名空间部署时也必须再创建一份,除非你用外部密钥管理插件(如 External Secrets Operator)统一同步。
4. 常见问题与排查技巧实录
4.1 Docker 相关常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Docker Desktop 启动失败,提示 virtualization support not detected | BIOS 未开启虚拟化,或 Windows 功能未启用 WSL2/Hyper-V | 进 BIOS 开启 VT-x/SVM,启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”,执行 wsl --update |
| docker build 很慢 | 基础镜像拉取网络问题,或者未利用缓存 | 配置镜像加速器,调整 Dockerfile 指令顺序把变化小的层放前面 |
| 容器内无法访问宿主机服务 | 网络模式默认是 bridge,容器网络和宿主机隔离 | 开发时可改用 --network host,或使用 host.docker.internal 访问宿主机 |
| 容器产生大量僵尸文件或磁盘爆满 | 未清理悬空镜像和构建缓存 | 周期性执行 docker system prune -af,并给 Docker 存储目录做容量监控 |
| 容器时区不对,日志时间是 UTC | 基础镜像默认时区是 UTC,未配置时区 | 在 Dockerfile 里安装 tzdata 并设置 ENV TZ=Asia/Shanghai |
关于镜像加速器,现在的公共加速服务变动频繁,建议你直接用云厂商提供的加速地址,或者在内网架设一个镜像代理,否则在拉取 Docker Hub 官方镜像时经常超时。
4.2 K8s 集群常见故障排查实录
K8s 的故障排查通常遵循一条链路:Pod 状态 -> 事件 -> 容器日志 -> 资源状态。
第一类:Pod 一直处于 Pending 状态。最常见的原因是资源不足和节点选择器不匹配。执行 kubectl describe pod <pod名>,如果事件里出现 Insufficient memory、Insufficient cpu,说明集群没有足够资源容纳新 Pod,可以扩容节点或降低 requests。
第二类:Pod 反复 CrashLoopBackOff。这个状态的意思是容器启动后马上崩溃,K8s 按退避策略不断重启。先 kubectl logs 看应用是否报错,比如端口被占用、数据库连接不上。如果日志没打出来,很可能是启动命令有问题,可以用 kubectl exec 进入容器手动启动进程来观察具体报错。
第三类:Service 无法访问。有次我排查了半小时,最后发现是 Service 的 selector 跟 Pod 的 labels 不匹配,导致 Endpoints 为空。遇到 Service 不通,先执行:
bash复制kubectl get endpoints <service名>
如果 ENDPOINTS 列为空,说明 selector 没匹配到任何 Pod,去检查 Deployment 的 template.metadata.labels 是否跟 Service selector 一致。这是一个极其容易踩的坑,新手一定要养成先看 endpoints 的习惯。
第四类:镜像拉取失败(ImagePullBackOff)。如果是私有仓库,先排查 Secret 是否存在以及密码是否正确;如果镜像 tag 是 latest,也可能因为缓存问题拉到了旧镜像,建议每次发布使用固定版本号。另外,确认节点上能否解析仓库域名,内网 DNS 解析失败也是常客。
4.3 资源限制与稳定性调优的独家经验
在 K8s 里,最让我感慨的一句话是“不设 requests/limits 的容器就是集群毒瘤”。我曾经遇到一个同事的测试服务没有设置资源限制,结果一连串内存泄漏把整台节点的 CPU 和内存全部打满,其他所有 Pod 都被连带拖垮,最尴尬的是这个测试环境还有生产流量在跑。
给初学者的设置建议:
- requests 不要拍脑袋,可以先跑一版应用,用
kubectl top pod观察实际使用量,再按峰值留 30% 余量设置。 - limits 不能比 requests 小,否则调度器和 kubelet 会直接报错。
- 给关键服务配 readinessProbe 和 livenessProbe 时,探针的
initialDelaySeconds一定要比应用启动时间更长,否则应用还在初始化就被误杀,导致无限重启。 - 使用 HPA(Horizontal Pod Autoscaler)自动扩缩容时,配合
metrics-server采集 CPU/内存指标,可以设置最小副本和最大副本,避免流量高峰时手动扩容的尴尬。
4.4 青龙、GitLab 等常见镜像部署的实践补充
热搜里还出现了“docker 青龙 依赖管理”和“docker 安装 gitlab”,这其实是容器化落地里非常典型的两个场景。青龙面板本质是一个定时任务管理平台,很多人用 Docker 部署它来做脚本或任务的定时执行。对它来说,依赖管理是最大的痛点:容器是临时的,任务脚本依赖的 Python 库、Node 模块如果每次重装一遍,效率极低。
我的建议是:不要反复在容器内手动装依赖,而是自定义一个基础镜像,把常用依赖封进镜像。哪怕是青龙这样偏向个人使用的工具,也值得遵循这个思路。
GitLab 用 Docker 部署则要重点关注存储和外部 URL。官方推荐的部署方式用 docker-compose 最省心,关键是把 config、logs、data 三个目录挂载到宿主机,这样升级容器、重启容器都不丢数据。另外,GitLab 非常吃内存,官方要求最低 4GB,否则运行起来极其卡顿,这是实测经验。
5. 学习路径与工具选型建议
我见过不少朋友的弯路:一上来就扎进 K8s 的各种组件源码,结果学了三个月还是云里雾里。更务实的路径是“先用起来,再逐步理解原理”。第一步,熟练掌握 Docker 的构建、运行、网络、存储,能够把任意项目打成镜像在本地跑起来;第二步,搭建一个单节点或三节点的 K8s 集群,推荐用 kind 或 k3s 这类轻量发行版,把业务应用通过 Deployment 和 Service 部署上去;第三步,理解 K8s 的控制器模式和声明式 API,学会排查问题;第四步,再深入原理,比如调度器、etcd、控制器循环等。
工具选型上,本地开发用 Docker Desktop(Win/Mac),需要快速搭建测试集群用 kind 或 k3s,生产环境根据云厂商选择托管 Kubernetes 服务,比如阿里云 ACK、腾讯云 TKE 等,性能和数据面可控性更好。学习资料方面,如果你愿意啃书,《Kubernetes 权威指南》(最新版到第 6 版)是个不错的系统参考,平时排查问题也可以多翻它的故障排查章节。
一个容易被忽略的能力是:用 kubectl 快速定位问题的速度。我建议每个读者都去刻意练习“Describe -> Events -> Logs -> Top”这个排查链路,直到形成肌肉记忆。很多时候,你离解决问题只差一条 kubectl describe 输出。
还有一些细节值得强调:Docker 和 K8s 都在快速迭代,网上很多教程已经过时。比如 Docker 官方已经不建议在 Linux 里使用 docker-compose up -d 这种方式了(虽然在实际工作中还是经常用),而 K8s 的 API 版本也在变化,写 YAML 时一定要留意 apiVersion,用弃用版本部署可能会遇到各种兼容问题。
在我实际操刀过的项目里,最省心的一次改造是:把应用容器化之后先不急着上 K8s,而是用 Docker Compose 在单机上把依赖的服务(MySQL、Redis、MQ)全部编排起来,把整套业务先跑通。等到流量的确需要多副本、需要滚动发布时,才把应用迁移到 K8s。这个节奏可以大幅降低学习期的心智负担。
最后分享一个我的习惯:所有 YAML 文件我都会提交到 Git 仓库,并且只通过命令行或 CI 的 kubectl apply 来变更集群状态,绝不在服务器上直接编辑 YAML 文件。这样每一次变更都有记录,回滚时只需要 kubectl rollout undo deployment/myapp 就能回到上一个版本。别小看这个习惯,它救过我无数次。
