简介容器化技术改变了应用交付方式而Kubernetes作为编排平台已成为集群管理的事实标准。但部署一套完整的K8s集群涉及众多组件与参数特别是K8s 1.24后移除Dockershim使得Docker与kubelet需通过cri-dockerd桥接。本文围绕Shell脚本自动化部署方案讲解如何通过一键脚本快速安装Docker、cri-dockerd、kubeadm并初始化控制平面、接入Worker节点同时涵盖内核参数配置、CNI网络插件安装、常见故障排查等关键环节。对中小规模集群、离线环境及学习K8s原理的开发者极具参考价值。1. 一键部署Shell脚本把Docker和K8s集群装进一条命令“一键部署”这四个字背后是无数个手工步骤被压缩成了一条bash命令。这个标题真正要解决的是在一台Linux服务器上通过一个shell脚本同时完成Docker运行时安装、cri-dockerd适配层部署、kubeadm组件安装、控制平面初始化最后让工作节点一键加入形成一个可用的容器化K8s集群。适合内网测试环境、小规模生产集群也适合想弄懂k8s安装部署每一步在干什么的初学者。最反直觉的一点是K8s从1.24开始移除了内置Dockershim直接装Docker是不能被kubelet当作容器运行时使用的必须额外装一个cri-dockerd做桥接。这个坑脚本里必须处理。2. 为什么用Shell脚本完成容器化K8s部署方案拆解与选型理由2.1 从Docker到K8scri-dockerd在集群里补了哪块短板很多刚开始接触k8s和docker区别的读者会误以为“Docker容器化K8s集群”就是把Docker装好集群就能用了。实际不是这样。K8s作为编排层通过CRIContainer Runtime Interface与底层容器运行时通信。K8s在1.24版本以前自带了Dockershim可以把Docker的API翻译成CRI能懂的协议。1.24之后官方把这个内置垫片删掉了于是Docker直接“失联”。补救方案就是Mirantis维护的cri-dockerd它像翻译机一样把Docker的接口重新映射回CRI协议。所以标题里的“Docker容器化”落到实际架构上就是控制平面组件以静态Pod容器方式运行在master节点上所有工作负载也由Docker创建但kubelet必须通过cri-dockerd的socket才指挥得动Docker。我一般会把整个方案拆成四条主线Shell脚本负责编排流程Docker负责具体容器生命周期cri-dockerd负责兼容适配kubeadm负责把K8s的控制平面组件以容器形式拉起来。选Shell而不是Ansible或Kubespray是因为在中小规模集群、离线环境、学习场景下Shell脚本依赖最少一个文件就能完成从零到Ready的过程也方便后续按需改参数。2.2 脚本要覆盖的7个环节从环境预检到CNI生效把一次完整的k8s安装部署拆开一个合格的一键脚本至少覆盖7个环节漏掉任何一个都会让集群卡在某个隐性问题上。第一环境预检。检查操作系统类型、CPU核数、内存大小、磁盘空间顺手关闭swap。K8s要求swap必须是0否则kubelet拒绝启动。第二内核模块与系统参数。加载overlay和br_netfilter模块配置bridge-nf-call-iptables否则Pod之间通信会变得很诡异。第三安装Docker并调整cgroupdriver为systemd。第四安装cri-dockerd并注册为systemd服务。第五安装kubeadm、kubelet、kubectl这三个K8s基础组件。第六初始化master节点这一步会生成证书、etcd集群和静态Pod清单。第七安装CNI网络插件让Pod获得集群IP并互通。这7个环节的顺序不能乱。比如有人先安装kubeadm再装Docker结果kubelet起来时找不到运行时服务反复重启有人跳过内核参数配置最后发现Pod网络不通查了半天。脚本的价值就是把这些顺序锁死同时允许用环境变量覆盖默认值。2.3 版本组合与系统前置条件版本搭配是方案里需要预先锁定的部分我做这个标题下的部署时常用一套比较稳的版本组合Ubuntu 22.04系统、Kubernetes 1.28.x、Docker 24.0.x、cri-dockerd 0.3.x、Calico v3.27。需要注意的是cri-dockerd的版本与K8s版本有对应关系不能随便拿最新版硬凑。另外K8s 1.28对应的安装源在pkgs.k8s.io不再是老的apt源脚本里必须用新地址。系统前置条件方面master节点建议2核4G起步测试集群可以卡在这个下限但生产环境至少4核8G。硬盘不低于50G因为Docker镜像和K8s组件都会占空间。操作系统建议保持干净装过其他容器运行时或旧版本K8s的机器先彻底清理一遍再跑脚本否则会撞端口和socket冲突。3. 编写master一键部署脚本从环境函数到控制平面初始化3.1 环境预检函数检测系统、CPU、内存、主机名和内核参数脚本的一键性体现在所有判断都在代码里自动完成而不是让用户看提示手动敲命令。我习惯把每个环节封装成一个函数主流程像流水线一样顺序调用。#!/bin/bash set -euo pipefail K8S_VERSION${K8S_VERSION:-1.28.2} POD_CIDR${POD_CIDR:-10.244.0.0/16} MASTER_IP${MASTER_IP:-$(hostname -I | awk {print $1})} precheck() { echo [1/7] 环境预检... [[ -z $MASTER_IP ]] { echo 无法获取本机IP; exit 1; } local cpu_cores mem_gb cpu_cores$(nproc) mem_gb$(free -g | awk /^Mem:/{print $2}) if [[ $cpu_cores -lt 2 || $mem_gb -lt 4 ]]; then echo 资源不足需要≥2核4G当前${cpu_cores}核${mem_gb}G exit 1 fi swapoff -a sed -i s/^\/swap.img/#\/swap.img/ /etc/fstab cat /etc/modules-load.d/k8s.conf EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter cat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.ipv4.ip_forward 1 net.bridge.bridge-nf-call-ip6tables 1 EOF sysctl --system /dev/null echo [1/7] 环境预检通过Master IP: ${MASTER_IP} }逻辑说明set -euo pipefail是这类脚本的保命配置任何一条命令出错或变量未定义会立即退出避免带着错误继续往下跑。K8S_VERSION和POD_CIDR作为可覆盖的环境变量方便在离线或固定版本环境里改值。swapoff之后还需要注释掉fstab里的swap挂载否则重启后swap回来kubelet照样报警。参数说明POD_CIDR默认用Calico常用的10.244.0.0/16如果你要用Flannel或者其他CNI这里要对应改。MASTER_IP自动取第一个非回环地址多网卡机器建议手动指定。3.2 安装Docker与cri-dockerd关键配置与版本锁定Docker安装走官方apt源配置daemon.json时一定要把cgroupdriver设为systemd。原因很简单K8s为了维持系统稳定默认也使用systemd作为cgroup驱动两边不一致会导致kubelet认为节点上的资源被重复使用进而拒绝对接。install_docker() { echo [2/7] 安装Docker... apt-get update apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin cat /etc/docker/daemon.json EOF { exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: {max-size: 100m}, storage-driver: overlay2 } EOF systemctl enable --now docker }逻辑说明overlay2是当前推荐的存储驱动json-file日志限制到100M防止容器日志无上限增长把系统盘写满。systemctl enable --now同时完成设置开机自启和立即启动。cri-dockerd的安装建议下载编译好的release包不要自己从源码构建。下面这段是核心install_cri_dockerd() { echo [3/7] 安装cri-dockerd... local version${CRI_DOCKERD_VERSION:-0.3.9} wget https://github.com/Mirantis/cri-dockerd/releases/download/v${version}/cri-dockerd-${version}.amd64.tgz tar xzf cri-dockerd-${version}.amd64.tgz install -m 0755 ./cri-dockerd/cri-dockerd /usr/local/bin/ cat /etc/systemd/system/cri-docker.service EOF [Unit] DescriptionCRI Interface for Docker Application Container Engine Documentationhttps://docs.mirantis.com Afterdocker.service [Service] Typenotify ExecStart/usr/local/bin/cri-dockerd --container-runtime-endpoint fd:// --pod-infra-container-image registry.aliyuncs.com/google_containers/pause:3.9 Restartalways StartLimitInterval0 RestartSec10 [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable --now cri-docker }逻辑说明--pod-infra-container-image参数指定pause镜像地址。原版默认指向registry.k8s.io/pause国内网络环境很容易拉不动我一般直接换到registry.aliyuncs.com/google_containers/pause量产时如果在内网可以提前把这个镜像推到私有仓库。参数说明fd://是systemd通知机制不是普通socket路径如果不加这个参数cri-dockerd会默认监听/var/run/cri-dockerd.sock但kubelet找socket的方式会变。为了后续kubeadm配置里写socket地址清晰我习惯让cri-dockerd同时监听默认socket所以ExecStart里只显式给了containerd的网络端点实际安装后cri-dockerd仍会生成/var/run/cri-dockerd.sock文件kubelet就是通过它通信的。如果你在配置时发现找不到socket执行ls /var/run/cri-dockerd.sock看看是否被自动创建。3.3 kubeadm初始化控制平面参数、超时和日志排查安装kubelet、kubeadm、kubectl使用新的官方apt源注意版本要与K8S_VERSION匹配还要用apt-mark hold锁住防止系统更新把组件版本带走导致集群可用性隐患。install_k8s_components() { echo [4/7] 安装 kubeadm kubelet kubectl... apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ / /etc/apt/sources.list.d/kubernetes.list apt-get update apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectl }初始化控制平面时我倾向于把配置写进yaml文件再执行kubeadm init --config而不是在命令行上堆积一长串参数。这样后续改版本、改网段都有据可查。init_cluster() { echo [5/7] 初始化控制平面... cat /etc/kubernetes/kubeadm-config.yaml EOF apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: ${K8S_VERSION} controlPlaneEndpoint: ${MASTER_IP}:6443 networking: podSubnet: ${POD_CIDR} --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock EOF kubeadm init --config/etc/kubernetes/kubeadm-config.yaml --upload-certs }逻辑说明ClusterConfiguration这一段定义了集群的版本、对外endpoint和Pod网段InitConfiguration这一段专门指定kubelet初始化时使用的容器运行时socket这里填的必须是cri-dockerd的socket。有人会漏掉criSocket导致kubeadm init抛错说找不到支持CRI的运行时。参数说明--upload-certs是为高可用控制平面准备证书分发用的如果只是单节点测试可以不加。controlPlaneEndpoint建议写master的IP不要写域名减少DNS解析带来的不可控因素。初始化过程卡住时不要急着CtrlC先看kubelet日志。常规命令是journalctl -u kubelet -f --no-pager。如果看到“Unable to connect to runtime service”就去检查cri-dockerd是否真的在运行socket是否生成。3.4 安装Calico CNI等待集群Ready控制平面初始化成功后kubeadm会提示你配置kubectl的admin.conf。脚本里要自动完成这套动作否则后续kubectl命令都连不上集群。init_cluster() { # ... 上面的kubeadm init ... mkdir -p $HOME/.kube cp /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config } install_cni() { echo [6/7] 安装Calico CNI... kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml echo [7/7] 等待节点Ready... kubectl wait --forconditionReady node --all --timeout120s }逻辑说明kubectl wait是一个很好的收尾动作脚本会阻塞到master节点状态变为Ready如果120秒内还没Ready就直接失败并返回非零退出码。这样整个一键脚本不需要人眼一直盯着屏幕CI/CD里也能通过退出码判断成功还是失败。参数说明Calico的默认manifest会读取/etc/kubernetes下的配置或通过环境变量覆盖。如果你的POD_CIDR不是默认的10.244.0.0/16需要在apply前下载manifest并修改CALICO_IPV4POOL_CIDR。我一般会把manifest下载到本地用sed替换那个CIDR再apply避免网络波动导致apply到一半失败。4. 工作节点接入脚本一键拿到join命令并完成Worker配置4.1 master端生成持久化Join令牌集群部署从来不只是单机Shell脚本的价值在批量添加worker节点时最能体现。master初始化完成后kubeadm会打印一条join命令但token默认24小时后过期证书有效期也比较短。脚本里应该主动生成新的token并把join命令整理成一个可直接执行的脚本文件分发出去。create_join_command() { echo 生成join命令... kubeadm token create --print-join-command --ttl 24h /root/join.sh sed -i s#kubeadm join#kubeadm join --cri-socket unix:///var/run/cri-dockerd.sock# /root/join.sh chmod x /root/join.sh echo join命令已生成/root/join.sh }逻辑说明kubeadm token create --print-join-command会输出一行不带cri-socket参数的join命令。因为worker节点的kubelet同样需要知道去哪个socket找容器运行时所以要用sed把--cri-socket unix:///var/run/cri-dockerd.sock参数硬插进去。如果不插这个参数kubelet会用默认的containerd socket而你的集群实际用的是Dockercri-dockerd加入时就会报“runtime connection failed”。参数说明--ttl 24h控制token有效时间内网环境可以给到24小时甚至更长。如果你做自动化批量交付建议把这个值改为自定义比如--ttl 0表示永不过期但生产环境安全合规不允许这么干。4.2 worker节点一键初始化脚本worker节点不需要执行kubeadm init但需要安装Docker、cri-dockerd、kubelet、kubeadm并做同样的内核参数配置。为了复用代码我习惯把2.2里的环境预检、安装运行时、安装组件这三段抽到一个公共脚本文件k8s-runtime.sh中然后master脚本和worker脚本都来source它。下面是worker脚本的骨架#!/bin/bash set -euo pipefail JOIN_CMD${JOIN_CMD:-/root/join.sh} source ./k8s-runtime.sh precheck install_docker install_cri_dockerd install_k8s_components echo [加入集群] 执行join脚本... if [[ -f $JOIN_CMD ]]; then bash $JOIN_CMD else echo 找不到join命令文件$JOIN_CMD exit 1 fi echo worker节点已加入集群等待master分配Pod...逻辑说明把公共逻辑抽到k8s-runtime.sh后master和worker的差异只剩“要不要初始化控制平面”和“要不要安装CNI”大大减少了代码冗余。JOIN_CMD设计成环境变量是为了支持直接把join命令字符串传进来而不是依赖共享文件系统。有一点必须提醒worker节点在执行join之前最好确认主机名不重复。K8s节点注册是以hostname为唯一标识的两个节点同名会造成其中一个注册不上或频繁报错。脚本里可以加一句hostnamectl set-hostname worker-XX的前置操作否则默认的localhost会冲突。4.3 验证集群节点状态与Pod分配部署完成后验证不能只看一行“success”。我通常会在脚本末尾追加两段检查一段看节点状态一段看核心Pod。verify_cluster() { echo echo 节点状态 kubectl get nodes -o wide echo echo 核心Pod状态 kubectl get pods -A | grep -E NAMESPACE|coredns|calico echo kubectl get pods -A | grep -v Running | grep -v Completed | grep -v NAMESPACE || true }逻辑说明kubectl get nodes -o wide能看到节点IP、内核版本、容器运行时版本和角色。grep -v Running | grep -v Completed | grep -v NAMESPACE这串是为了把非Running状态的Pod列出来一旦有异常直接暴露。如果输出什么都没有说明所有Pod都在Running或Completed集群状态健康。实际观察中coredns两个Pod可能会有一段时间处于Pending这通常是CNI还没完全起效等Calico的Pod全部Running后再看coredns就会正常。脚本里可以用kubectl wait --forconditionReady pod -l k8s-appkube-dns -n kube-system --timeout120s做更严格的等待。5. 部署避坑记录5个让集群起不来的真实原因5.1 现象master初始化报“the API server is not healthy after 4m0s”这个报错非常典型热词里都有人天天搜。表象是kubeadm init在4分钟超时后失败但根因不只有一个。我遇到最多的情况是kubelet根本没有启动成功导致apiserver的静态Pod不被创建。先看kubelet日志执行journalctl -u kubelet -f --no-pager如果日志里出现“Container runtime network not ready”或“failed to get sandbox image”那基本是cri-dockerd或pause镜像的问题。解决步骤确认systemctl status cri-docker是active确认ls /var/run/cri-dockerd.sock存在手动crictl pull registry.aliyuncs.com/google_containers/pause:3.9验证镜像能拉下来。还有一个容易被忽略的原因master节点本身内存不够apiserver容器启动后因内存压力被杀反复重启。建议直接查看kubectl get pods -A或docker ps里apiserver容器的重启计数。5.2 现象kubelet服务启动失败报“failed to run Kubelet”这条多半发生在刚装完组件、还没执行kubeadm init就手动启动kubelet的时候。kubelet在没有配置文件和集群地址时会尝试连接到默认config找不到就退出。实际上kubelet在kubeadm init阶段才生成配置不要提前systemctl start kubelet。脚本里我一般不主动启动它而是把systemctl enable kubelet设置好剩下交给kubeadm。如果你已经手动启动过并且报错清掉已知状态rm -rf /var/lib/kubelet /etc/kubernetes/kubelet.conf然后重新执行kubeadm init即可。另一种原因是kubelet找不到cri-dockerd的socket这时要在/etc/default/kubelet里加一行KUBELET_EXTRA_ARGS--container-runtime-endpointunix:///var/run/cri-dockerd.sock然后重启kubelet。5.3 现象cri-dockerd安装后Docker容器无法被K8s识别问题出现在你手动用docker run启动了一个容器以为K8s能看到结果发现kubectl get pods里完全没有。这是因为Docker守护进程和K8s之间隔着CRI层K8s只通过cri-dockerd的socket与Docker通信不会直接操作Docker的默认socket。解决办法不是去打docker socket的主意而是把所有工作负载都通过K8s API创建。如果你确实要验证Docker和cri-dockerd链路通不通可以用crictl --runtime-endpoint unix:///var/run/cri-dockerd.sock ps查看容器列表这个命令看到的才是K8s视角。另一个常见误配是kubelet组件配置里残留了/var/run/dockershim.sock这个socket在1.24之后已经不存在了必须改成cri-dockerd.sock。5.4 现象镜像拉取超时初始化卡在PullImage国内服务器kubeadm init卡住十有八九是拉取registry.k8s.io的镜像失败。kubeadm允许在ClusterConfiguration里修改imageRepository把默认仓库改成registry.aliyuncs.com/google_containers。同时cri-dockerd的pause镜像也要一并修改。脚本里我已经把这两处都设置了但如果你直接复制网上旧教程的command很可能没有处理这个参数就会无限卡在“Waiting for the kubelet to boot”。解决方式kubeadm config images list先列出需要拉取的镜像列表然后一条条确认前面的镜像仓库地址是否可达。离线环境下更可靠的做法是提前在能联网的机器上docker pull所有镜像再docker save打成tar包拷贝到目标服务器docker load最后给脚本写入--image-repository指向本地这会在第6章展开。5.5 现象工作节点加入后一直NotReadyworker节点成功join后kubectl get nodes状态是NotReady。排除kubelet正常运行后第一个检查项是CNI是否部署。很多场景只在master上装了Calico但Calico的DaemonSet需要运行在每个节点上如果节点之间网络策略不一致Pod无法通信节点就被标记NotReady。解决在master上kubectl get pods -A看有没有Calico相关的Pod处于CrashLoopBackOff。若有查看日志kubectl logs -n kube-system -l k8s-appcalico-node -c calico-node。常见原因是Pod网段与Calico期望的网段不一致这就是为什么脚本里POD_CIDR和Calico的CALICO_IPV4POOL_CIDR必须保持一致。另一个原因是BGP模式下的网络端口问题部分云主机安全组没放行TCP/179Calico无法建立BGP对等也会表现为NotReady。6. 进阶把脚本改造成离线部署工具包验证与自愈技巧当脚本能在干净机器上一次性跑通后下一步我建议把它扩展成离线部署包。做法是先在一台能联网的机器上把Docker镜像、K8s组件deb包、cri-dockerd二进制、Calico manifest全部下好归档成tar包再分发。关键点是列出镜像清单kubeadm config images list --image-repository registry.aliyuncs.com/google_containers # 然后逐条 docker pull docker save离线安装时将tar包里的镜像docker load进去再把安装源切换为本地目录。由于换源涉及apt配置我通常会在脚本里增加一个OFFLINE_MODE开关若为true则跳过apt update和网络下载直接安装预置的deb包。验证方面除了kubectl get nodes我还会反复使用kubectl get events --all-namespaces --sort-by.metadata.creationTimestamp来查看集群里所有异常事件。这个命令比逐一查Pod日志高效得多能一次看到节点调度、镜像拉取、健康检查等层面发生了什么。它也是我排查生产集群故障的第一抓手。自愈技巧是给脚本加幂等性检查。具体做法是每行关键步骤都判断当前状态已经装过的组件就跳过避免重复执行时把集群搞坏。比如如果/etc/kubernetes/kubelet.conf存在就说明这个节点已经加入过集群继续执行kubeadm init就是重复动作了。这时脚本应该提示并退出或者提供“清空重置”选项kubeadm reset --force。我的习惯是把这些检查放在脚本开头跑任何环境前先自检一遍而不是跑到中途才发现。开发过程中最让我记忆深刻的翻车是脚本写好了测试时发现第二次运行把集群初始化的token和证书全部覆盖导致apiserver重启后无法读取。后来给所有初始化动作加了“文件已存在则跳过”的守护逻辑才彻底解决。这个方向能不能投入核心取决于你要部署的节点规模和环境复杂度——如果只要3台以内的小集群这个Shell脚本方案的维护成本比Ansible低很多如果到了十几台建议加入初始化清单和离线包管理但整体思路依然成立。希望帮到你。本文还有配套的精品资源点击获取