从Docker Compose到K8s集群:kubeadm搭建与Web服务部署实战 📅 发布时间:2026/9/16 2:58:31 👁 浏览次数: 最近把一个老项目的运行环境从单机 Docker Compose 迁移到了真正的 K8s 集群上整个过程从零开始翻了官方文档、踩了不少坑也把之前一知半解的概念彻底捋顺了。这篇博文就按我实际操作的顺序把 K8s 集群搭建 和 Web 服务部署 完整记录下来目标是让已经会 Docker、但还没正式上手 K8s 的同学能照着这套流程在自己的机器上复现出一套可用集群并成功跑起来一个 Web 服务。我会把“为什么这么配”也一并写清楚而不只是贴命令。毕竟 K8s 这套东西参数和组件极多光会复制粘贴遇到问题还是两眼一抹黑。文章里涉及的版本是我实测过的组合Ubuntu 22.04 Kubernetes 1.28.x containerd整体思路同样适用于其他版本只是个别参数需要对应调整。如果你正准备搭集群、部署业务或者最近在看 k8s 相关面试题这篇文章应该能帮你省下不少试错时间。1. 动手前先想清楚这套集群到底怎么搭很多人一上来就百度“k8s安装部署”然后找一篇教程跟着敲敲到一半发现和自己环境不一样又开始换教程。K8s 集群搭建不是单纯的软件安装它涉及操作系统、容器运行时、网络插件、证书、kubelet 等多个层次。动手之前先把几个核心选择定下来后面会顺很多。1.1 单机版还是多节点先选对拓扑K8s 的部署方式多到让人眼花。minikube、k3s、kubeadm、二进制部署、云厂商托管集群每种适合的场景都不一样。如果你只是为了学习 K8s 的概念和命令minikube 或者 k3s 是最快的一条命令就能拉起一个单机环境。但如果你想把“集群搭建”这件事本身搞清楚尤其是想理解 master 和 worker 节点的关系、证书怎么签发、网络插件怎么工作那我还是推荐用 kubeadm 搭建多节点集群。我的环境是一台旧工作站上开了两台虚拟机一台 4 核 8G一台 2 核 4G分别作为控制平面节点和工作节点。这个配置在云服务器上也够用1 核 2G 的机器不建议尝试K8s 组件本身加上 etcd内存很容易被打满。节点规划建议至少是控制平面 2 核 4G 以上工作节点看业务负载测试环境 1 核 2G 也能勉强跑但我不建议。1.2 版本选型与操作系统约定K8s 版本迭代很快社区支持周期大约一年。我选择的是 1.28.x这是一个相对成熟且资料丰富的版本。操作系统用了 Ubuntu 22.04 LTS主要是因为它内核版本够新对 containerd 和 iptables 的兼容性好而且 apt 源里的软件包版本也比较新。这里有个容易被忽略的细节K8s 各组件版本相差不能太大。kubeadm、kubelet、kubectl 最好保持同一个 minor 版本比如都用 1.28.x否则可能出现 kubelet 与 API Server 之间握手失败的情况。etcd 和 CoreDNS 会由 kubeadm 自动安装版本匹配关系官方文档里都有不需要手动干预。1.3 先讲清楚 K8s 和 Docker 的关系每次聊 K8s 都会被问“k8s 和 docker 区别”。这个点不搞清楚后面配置容器运行时一定会懵。Docker 本身是一个容器引擎负责把应用打包成镜像、创建容器而 K8s 是一个容器编排平台负责管理很多台机器上的大量容器解决容器的调度、伸缩、服务发现、滚动更新等问题。你可以把 Docker 理解成一台台独立的“打包机”K8s 则是一个“调度中心”它自己并不会直接运行容器而是通过容器运行时来干活。需要注意从 K8s 1.24 开始kubelet 已经不再直接支持 Docker 的 dockershim 接口也就是说你不能让 K8s 直接调用 Docker 来创建容器了。但这不代表 Docker 没用了你依然可以用 Docker 构建镜像只是集群里的容器运行时换成了 containerd 或其他符合 CRI 标准的运行时。我们在部署 Web 服务时构建镜像用 Docker运行容器用 containerd两者是协作关系。2. 环境准备与前置细节很多翻车都是栽在这里集群搭建的报错十个里有八个出在环境准备阶段。有的是没关 swap有的是内核参数不对有的是安装源没配好。这部分我按顺序列清楚你照着做基本不会出大问题。2.1 节点规划与网络要求我规划了两台节点信息如下节点角色主机名配置IP 地址control-planek8s-master4C / 8G192.168.100.10workerk8s-node12C / 4G192.168.100.11主机名一定要提前设好不要用默认的 ubuntu 之类。K8s 集群内部会通过主机名互相识别如果主机名重复或者带下划线kubelet 注册时容易出问题。设置方法很简单hostnamectl set-hostname k8s-master hostnamectl set-hostname k8s-node1接着在每台机器的/etc/hosts里加上对方的解析记录保证两台机器能通过主机名互相 ping 通。网络层面要求节点之间内网互通且端口开放范围较大如果是在云平台安全组需要放行 6443、2379-2380、10250 等端口。我这里是虚拟机直接用的同一个网段省了这一步。2.2 关闭 Swap 与内核参数调整K8s 要求节点必须关闭 swap原因很实在如果内存不够时系统开始交换Pod 的资源配额就失去意义kubelet 的 QoS 策略也无法保证。关闭方法swapoff -a sed -ri s/.*swap.*/#/ /etc/fstab然后需要加载两个内核模块并调整网络转发参数cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter cat EOF | tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --systembr_netfilter的作用是让 iptables 能够处理 Linux 网桥上的流量而 flannel 这类网络插件依赖这个能力做数据包转发。我最初就是没加载这个模块导致 flannel 起来后 Pod 之间网络不通。2.3 安装容器运行时 containerd我选择 containerd 作为容器运行时。安装方式不只有一种可以用 Docker 仓库里的 containerd 包也可以直接用 apt。我推荐使用 Docker 官方源安装 containerd因为版本相对较新而且后续如果还要用 Docker 命令可以一并装好。apt-get update apt-get install -y containerd.io安装完成后需要生成默认配置并修改两个关键点。第一是 SystemdCgroup必须设置为 true保证 kubelet 和 containerd 使用同一个 cgroup 驱动否则节点状态会一直 NotReady。第二是 sandbox_image 的地址默认是registry.k8s.io/pause:3.9在国内直连速度很慢可以换成镜像加速地址。修改方式如下mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml然后编辑/etc/containerd/config.toml找到SystemdCgroup和sandbox_image两项修改。改完后重启 containerdsystemctl restart containerd systemctl enable containerd2.4 安装 kubeadm、kubelet、kubectl这三件套需要从一个固定源安装。先导入 Google Cloud 的 GPG 密钥并添加 apt 源然后指定版本安装。网上很多教程让你直接apt install kubeadm kubelet kubectl那样会装到最新版本如果和你的 containerd 或系统版本不匹配后面容易出幺蛾子。我习惯指定版本号apt-get install -y kubelet1.28.2-00 kubeadm1.28.2-00 kubectl1.28.2-00 apt-mark hold kubelet kubeadm kubectlapt-mark hold是锁定版本防止不小心升级导致集群版本分裂。装完之后可以先验证一下 kubeadm 版本同时查看一下 kubelet 是否正常运行。注意此时 kubelet 会不断重启因为还没有集群配置文件这是正常的不用担心初始化完成后它会自动稳定下来。3. 集群初始化与节点加入把架子立起来环境准备做好后真正的“集群搭建”才刚刚开始。这一章我会以控制平面节点为中心从kubeadm init开始再到工作节点join最后验证集群状态。3.1 在 master 节点执行 kubeadm init在控制平面节点上执行初始化命令。这里是整个搭建过程中最关键的参数配置kubeadm init \ --apiserver-advertise-address192.168.100.10 \ --pod-network-cidr10.244.0.0/16 \ --kubernetes-version1.28.2--apiserver-advertise-address必须填 master 节点的内网 IP不能填 127.0.0.1否则工作节点没法访问 API Server。--pod-network-cidr是 Pod 网段我填了10.244.0.0/16这是为了和后面的 Flannel 插件默认网段一致。如果你用 Calico可能需要改成192.168.0.0/16这个网段必须和 CNI 插件配置对得上否则网络起不来。初始化成功后终端会输出三块内容kubeconfig 配置命令、join 命令、token 信息。先执行 kubeconfig 配置不然 kubectl 没法连接集群mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后把 join 命令保存下来。如果丢了后面可以用kubeadm token create --print-join-command重新生成这个后面再讲。3.2 安装 CNI 网络插件我选了 Flannel初始化完成但集群网络还是空的节点会处于 NotReady 状态CoreDNS 也无法调度到可用节点。这是因为 K8s 只是管理容器调度Pod 间通信需要额外安装 CNI 插件。CNI 插件有很多种Flannel、Calico、Cilium 各有侧重。我这次选了 Flannel主要是因为它轻量、配置简单对新手友好。执行kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml如果你的网络环境拉取 GitHub 不稳定可以把这份 YAML 下载到本地再 apply。Flannel 默认使用10.244.0.0/16网段这正好和我在 init 时配置的--pod-network-cidr一致。装完之后等待镜像拉取和 DaemonSet 调度完成可以用下面的命令观察kubectl get pods -n kube-flannel -o wide kubectl get nodes当所有节点状态变为Ready说明网络插件已经生效。我第一次搭建时就卡在这里节点一直是 NotReady后面排查发现是 Flannel 镜像拉不下来手工拉取后解决。3.3 worker 节点加入集群与验证在 worker 节点上执行 join 命令。命令格式类似下面这样kubeadm join 192.168.100.10:6443 --token token \ --discovery-token-ca-cert-hash sha256:hash执行成功后去 master 节点查看节点状态kubectl get nodes如果节点 STATUS 是Ready说明加入成功。如果还在 NotReady不要急K8s 首次加入需要拉取 kube-proxy、pause 等镜像通常过一两分钟就会好。等所有节点 Ready 后集群就算搭起来了。这里提一个常见的后续需求默认 token 有效期是 24 小时过期的 token 在 worker 节点上重新 join 时会报错。解决办法是在 master 上重新生成kubeadm token create --print-join-command这个命令会打印一条新的完整 join 命令直接复制到 worker 节点执行即可。3.4 集群搭建后的健康检查节点全部 Ready不代表集群就完全健康了。我建议做一次快速体检kubectl get pods -A确认所有系统组件 Pod 都处于 Running 或 Completed 状态。kubectl get cs检查 etcd、scheduler、controller-manager 状态。在 master 节点kubectl run test --imagenginx跑一个测试 Pod验证调度和网络是否正常。如果 CoreDNS 一直 CrashLoopBackOff往往意味着网络插件没装好或网段冲突如果 scheduler 状态异常多半是证书或 apiserver 地址配置有问题。做完这些检查再进入 Web 服务部署心里会踏实很多。4. 部署 Web 服务让集群真正干活集群搭好之后终于到了核心环节把 Web 服务部署上去。这里我会用一个最简单的 Nginx 镜像做示例但会完整演示 Deployment、Service、Ingress 三种资源的配合使用。理解了这条链路部署任何 Web 服务都是同一个套路。4.1 用 Deployment 管理应用副本Deployment 是 K8s 里管理无状态应用最常用的资源它负责控制 ReplicaSet再由 ReplicaSet 保证指定数量的 Pod 一直运行。先创建一个nginx-deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: nginx-web labels: app: nginx-web spec: replicas: 3 selector: matchLabels: app: nginx-web template: metadata: labels: app: nginx-web spec: containers: - name: nginx image: nginx:1.26 ports: - containerPort: 80 resources: requests: memory: 128Mi cpu: 100m limits: memory: 256Mi cpu: 200m执行kubectl apply -f nginx-deployment.yaml然后查看状态kubectl get deployment nginx-web kubectl get pods -l appnginx-web -o wide你会看到 3 个 Pod 被调度到了集群的不同节点上。Deployment 的优势在于如果你手动删掉其中一个 Pod它会立即重新创建一个保证副本数始终是 3。这就是 K8s 里“声明式”管理的精髓你只需要告诉它期望状态它自己会不断修正实际状态。资源限制部分requests 是给调度器看的“最少预留量”limits 是运行时硬上限。如果只写 limits 不写 requests实际效果可能会和你预期不一样建议测试环境也养成把两个都写上的习惯。4.2 用 Service 把 Pod 的端口稳定暴露出来Pod 是动态的IP 会随着重建而变化所以不能直接通过 Pod IP 访问服务。Service 是 K8s 提供的一个稳定访问入口它会关联一组 Pod并做负载均衡。Service 的几种类型各有适用场景类型访问方式适用场景ClusterIP集群内部访问服务间调用、数据库NodePort通过节点IP端口访问测试环境、临时外露LoadBalancer云服务商负载均衡生产环境对外服务我先创建一个 ClusterIP 类型的 Service方便集群内部访问apiVersion: v1 kind: Service metadata: name: nginx-web-service spec: selector: app: nginx-web ports: - protocol: TCP port: 80 targetPort: 80执行后在集群内部可以通过nginx-web-service:80访问到后端 Pod。如果想在外部浏览器访问有两种方式一是把 type 改为 NodePort二是部署 Ingress Controller。测试环境我经常直接用 NodePortkubectl expose deployment nginx-web \ --namenginx-web-nodeport \ --typeNodePort \ --port80 \ --target-port80这样会随机分配一个 30000-32767 的端口然后通过http://任意节点IP:NodePort访问。比如我的 NodePort 是 31234那浏览器访问http://192.168.100.10:31234就能看到 Nginx 欢迎页。4.3 用 Ingress 做 HTTP 路由NodePort 虽然能用但生产环境不可能给每个服务都分配一个节点端口。Ingress 是更高级的访问入口它工作在七层能按域名、URL 路径做路由还能和 TLS 证书配合。Ingress 本身只是规则定义真正干活的是 Ingress Controller常见的有 ingress-nginx、traefik。安装 ingress-nginx 的方式比较直接kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/cloud/deploy.yaml部署完成后创建 Ingress 规则把web.example.com路由到刚才的 ServiceapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-web-ingress spec: ingressClassName: nginx rules: - host: web.example.com http: paths: - path: / pathType: Prefix backend: service: name: nginx-web-service port: number: 80然后在本地/etc/hosts里把域名指向 master 节点 IP浏览器就能通过域名访问了。这里有个新版本需要注意的点K8s 1.19 之后Ingress 必须指定ingressClassName否则 controller 不会接管这条规则。我见过不少人在这一步踩坑Ingress 建了但没有反应多半就是少了这一行。4.4 常用 K8s 命令速查平时用得最多的命令我整理成了一张表排查问题时直接对着填场景场景命令查看节点状态kubectl get nodes查看所有 Pod 及所在节点kubectl get pods -A -o wide查看某个 Pod 的详细事件kubectl describe pod查看 Pod 日志kubectl logs -f进入容器执行命令kubectl exec -it -- bash查看服务端口映射kubectl get svc -A查看某个资源当前配置kubectl get deployment nginx-web -o yaml删除资源kubectl delete -f xxx.yamlK8s 常用的管理工具也顺带提一下。命令行是 kubectl图形界面可以考虑 Octant、Lens、K9s其中 K9s 是终端下的工具轻量高效我排查问题时常开一个。如果希望更接近生产环境可以再搭一套 Dashboard但要注意 Dashboard 的 RBAC 权限别给太大否则有安全风险。5. 实际踩坑记录这些问题你一定也会遇到再顺的流程也不可能一次成功。下面这些坑是我这次搭建过程中真实遇到过的我按现象、原因、解决步骤列出来希望能帮你节省排查时间。5.1 Pod 一直 Pendingdescribe 提示磁盘压力现象是kubectl get pods看到 Pod 一直处于 Pending用kubectl describe pod查看Events 里出现0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory或类似磁盘压力提示。原因通常是集群资源不足。K8s 调度器会根据 Pod 的 requests 和节点剩余资源做匹配如果节点内存被系统组件占了大半本来很宽裕的配置也可能调不起来。我遇到的情况是 containerd 在拉取镜像过程中占用了临时磁盘空间且节点根分区太小导致镜像无法完整拉取。解决思路是先清理磁盘docker system prune -f # 如果节点上还能用 docker containerd 的临时文件一般不用手动清理直接重启节点上的 containerd 也可更保险的方式是在创建 Pod 时不要把 requests 写得太高测试环境给 100m/128Mi 这种小规格就够用。另外如果所有 Pod 都 Pending先检查节点是否 Ready、污点是什么再决定是补资源还是调整调度策略。5.2 节点 NotReadykubelet 日志全是 CNI 报错节点加入后一直是NotReady很常见。先看 kubelet 状态systemctl status kubelet journalctl -u kubelet -f常见错误有三类镜像拉取失败日志里能看到Failed to pull image解决办法是手动拉取所需镜像或者调整镜像地址到可访问的镜像源。CNI 配置不存在日志提示failed to find plugin flannel说明 Flannel 还没装好或者 Flannel Pod 没正常运行。cgroup 驱动不一致日志提示Failed to run kubelet ... misconfiguration: kubelet cgroup driver: systemd is different from docker cgroup driver: cgroupfs解决办法是把/etc/containerd/config.toml里的SystemdCgroup改为 true然后重启 containerd 和 kubelet。排查的时候一定要看日志不要盲目重启机器。journalctl -u kubelet -f是定位 NotReady 的第一利器。5.3 Service 创建后访问不通Pod 都 Running 了Service 也建了但浏览器就是访问不了。我把可能的情况列个清单NodePort 端口没放通云服务器安全组或本地防火墙需要放行 30000-32767 端口。我的虚拟机本机访问没问题但局域网其他机器访问不了后来发现是 ufw 没放行相关端口。Service selector 没有匹配到 Pod用kubectl get endpoints查看 Service 关联的 Endpoints如果没有记录说明 selector 写错了。Pod 内容器端口和 Service targetPort 不一致Nginx 监听 80Service 的 targetPort 也必须指向 80别写成 8080。如果 Service 类型是 ClusterIP外部当然访问不了这是设计如此不是 bug。想从集群外访问必须改成 NodePort 或走 Ingress。5.4 证书过期与升级提醒Kubeadm 默认签发的证书有效期是一年测试环境搭完放在那里不管过了几个月再打开就会看到大量组件报错。可以用这个命令检查证书有效期kubeadm certs check-expiration如果证书快过期执行kubeadm certs renew all续期然后重启 kubelet 和静态 Pod。如果一直长期使用建议在文档里记好到期时间或者给证书监控做个告警。另外集群升级不要在节点上直接升级二进制包应该遵循官方升级文档先升级 kubeadm再升级 kubelet最后 drain 节点逐台升级。5.5 给新手的建议别迷信一键脚本市面上有很多一键安装 K8s 的脚本点一下就能生成集群确实方便。但我还是建议新手至少手动跑一遍kubeadm init和kubeadm join因为在这个过程中你能看到证书、token、服务发现的细节后续排查问题时思路会更清楚。还有一个很多人问的问题k8s 学习要从哪里入手。我的体会是先理解四个核心对象Pod、Deployment、Service、Ingress再用它们部署几个实际服务最后再去啃调度器、控制器、RBAC 这些进阶内容。平时可以多看看 k8s 官方博客和 GitHub 上的 operator 项目面试常见的“k8s 调度过程”“Pod 的生命周期”“etcd 的 raft 协议”都能在实践基础上更好的理解。最后再分享一个小技巧我自己习惯在部署任何 K8s 资源之前先用kubectl create --dry-runclient -o yaml生成一个推荐配置在此基础上再修改。这样既不会漏字段又能快速得到一个符合规范的起点。K8s 是个庞大的系统一次失败很正常关键是把日志看明白、把状态理解透这个能力比记住一百条命令更值钱。