从个人项目到公司生产环境,Docker和Kubernetes(K8s)这套组合现在基本成了后端开发与运维绕不开的必修课。我第一次接触Docker是在一个多服务的个人项目里,当时被各种版本依赖和部署环境折腾得够呛,后来从Docker Desktop装起,一路踩坑走到用K8s管理集群,中间积累了不少经验教训。这篇总结会把核心概念、环境搭建、常用命令、真实业务部署以及K8s企业落地要点都梳理一遍,适合刚接触容器化、想快速搭起一套可用环境的新手,也适合已经用了一阵子Docker、打算往K8s方向深入的初级工程师。
1. Docker的本质:镜像、容器和仓库的关系
1.1 容器和虚拟机的核心差异
很多刚接触Docker的朋友会问,容器跟虚拟机到底什么区别?一句话概括:虚拟机虚拟的是硬件,容器虚拟的是操作系统。虚拟机里跑的每个Guest OS都有一套完整的内核,所以启动慢、资源占用高;而容器直接共享宿主机的内核,只通过隔离机制把进程、文件系统、网络隔开,所以几毫秒就能启动,单台机器能跑几十上百个容器。
这里有个很重要的点:正因为容器共享宿主机内核,所以Linux容器在Linux上跑是最自然的形态,Windows上跑Linux容器本质是靠虚拟机套了一层WSL2来实现。这也是为什么Docker Desktop默认启用WSL2——没有WSL2,Windows环境下Docker的体验会大打折扣。
资源占用上,我自己测过一个典型场景:相同的服务,虚拟机可能占用500MB内存,容器往往只要几十兆。生产环境成本差异非常明显,这也是容器能在云原生时代迅速铺开的核心原因。
1.2 镜像分层与联合文件系统
Docker镜像由一层一层只读文件系统叠加而成,每一层对应Dockerfile里的一条指令。拉取镜像时可以只下载新增的层,已有的层直接复用;同一个基础镜像比如ubuntu,不管被多少个其他镜像引用,宿主机上只存一份底层数据。这种设计节省了大量磁盘空间和带宽。
联合文件系统(UnionFS)是分层的底层支撑机制。在Docker中默认的存储驱动是overlay2,它把多个目录合并挂载成一个统一视图。运行容器时,Docker会在镜像的只读层之上再加一个可写层,所有对文件系统的修改都写在这一层。容器删除后,可写层随之销毁,所以容器本身是“无状态”的。
这个特性也带来一个经典坑:不要把数据直接写在容器内部。容器重建后数据就丢了,必须用数据卷(volume)把数据挂载到宿主机。后面实战部分我会再详细展开。
1.3 容器从创建到运行的全过程
执行docker run nginx:latest之后,Docker引擎做的事大致如下:先从本地仓库找nginx:latest镜像,找不到就去配置的远端仓库拉取;确认镜像完整后创建容器(分配独立的文件系统、网络栈、进程命名空间);最后启动进程并挂接容器的标准输入输出。
整个过程中涉及docker image、docker container、docker network、docker volume等几个对象。我建议新手把这些对象分开理解,不要混在一起。镜像只管怎么打包,容器管运行实例,网络管通信,卷管持久化,各司其职,用起来逻辑就清晰了。
还有一个容易忽略的细节:docker run其实是“创建+启动”两步的合并,分开操作是docker create加docker start。调试阶段建议多用docker run -it进入交互模式,跑通了再用-d后台运行。
2. Docker环境搭建:从Windows到Linux的完整实操
2.1 Windows下安装Docker Desktop的三大坑
Windows用户装Docker Desktop,踩到最多的问题集中在三个地方。
第一是“virtualization support not detected”或者“Docker Desktop failed to start because virtualization support is not detected”。这个报错大概率是BIOS里虚拟化没开。进BIOS找到Intel VT-x或AMD-V/SVM选项,开启后重启再试。如果BIOS已经开了,还要检查Windows功能里的“虚拟机平台”“Windows虚拟机监控程序”是否启用。
第二是报“failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen”。这类npipe连接失败多半是Docker引擎没起来,常见原因是WSL2内核版本太旧。在管理员PowerShell里执行wsl --update更新内核,然后wsl --shutdown重启WSL,基本能解决。
第三是Docker Desktop安装成功后,拉取镜像慢或者超时。解决办法在后文镜像加速部分会详细说。
另外,Windows 11上建议直接装最新版Docker Desktop,它会自动配置WSL2。Windows 10用户如果开了Hyper-V,也可以让Docker Desktop走Hyper-V后端,但WSL2整体资源占用更小,我实测下来体验更好。
2.2 Linux环境下安装Docker
Linux装Docker其实很简单,Ubuntu和Debian系列推荐用官方脚本:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh装完执行sudo systemctl enable --now docker,然后确认状态:
sudo systemctl status docker docker versionCentOS 7需要注意,系统自带的Docker版本往往很老,升级到新版Docker Engine 24+会比较顺手。先用sudo yum remove docker*卸载旧包,再安装yum-utils并配置官方仓库,然后装docker-ce docker-ce-cli containerd.io。记住:CentOS 7的内核是3.10,如果要用较新的overlay2特性,建议先把内核升级到长期支持版本,不然某些功能会受限。
Ubuntu系统如果是从旧版本升级过来的,偶尔会遇到docker命令提示找不到daemon.json之类的路径问题。实际上Docker配置目录是/etc/docker/,没有就手动创建,权限设为0644。
2.3 镜像加速源的配置
docker pull慢是全世界新手都会撞上的问题。除了网络因素,更常见的解法是配置镜像加速器。在/etc/docker/daemon.json里加registry-mirrors:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com" ] }改完后执行sudo systemctl daemon-reload和sudo systemctl restart docker才能生效。
需要注意,公共加速源有时会失效或变慢,建议维护一个备选列表。另外,如果公司内部有Harbor或Nexus仓库,也可以把它作为mirror,内网拉取速度会非常快。对于下载慢的单个镜像,还有一个技巧:先找一台网络好的机器拉下来导出为tar包,再传到目标机器用docker load导入,适合内网环境批量同步。
2.4 常用命令速查与权限问题
Docker命令数量不少,但日常高频使用的其实就十来个。我把它们按使用场景整理成了一张速查表:
| 场景 | 命令 | 说明 |
|---|---|---|
| 查看镜像 | docker images | 列出本地镜像 |
| 拉取镜像 | docker pull 镜像名:tag | 不写tag默认latest |
| 运行容器 | docker run -d -p 8080:80 --name web nginx | -d后台,-p映射端口 |
| 查看容器 | docker ps -a | 加-a看停止的容器 |
| 进入容器 | docker exec -it 容器名 bash | 调试常用 |
| 查看日志 | docker logs -f 容器名 | -f跟随输出 |
| 停止/启动 | docker stop/start 容器名 | 不删容器 |
| 删除容器 | docker rm -f 容器名 | -f强制删除运行中的 |
| 删除镜像 | docker rmi 镜像名 | 先删依赖容器 |
| 清理资源 | docker system prune | 清掉悬空镜像和停止容器 |
| 查看资源占用 | docker stats | 类似top |
| 拷贝文件 | docker cp 容器:路径 本地路径 | 容器内外互传文件 |
权限问题也容易踩:Linux下docker命令提示权限不足,因为Docker守护进程监听在/var/run/docker.sock,这个socket文件默认属于root组。把用户加进docker组:
sudo usermod -aG docker $USER然后重新登录。但这里提醒一下,docker组的权限约等于root,不要在普通开发机上随便把人加进docker组,生产环境更要注意,后面安全部分会展开。
3. Docker实战:用Compose部署MySQL、Redis和GitLab
3.1 Dockerfile的常用写法
Dockerfile是把应用固化成镜像的配方。以Python项目为例,一个精简的Dockerfile长这样:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "app.py"]几点经验:尽量选官方基础镜像,精简版(-slim / alpine)能明显减小体积;WORKDIR一定要写,避免各种路径问题;RUN尽量合并,减少镜像层数;CMD用exec格式,保证能正确接收信号。
构建命令是docker build -t myapp:1.0 .。注意那个点不是可选项,它指定构建上下文目录。构建上下文里的所有文件都会传给Docker守护进程,如果目录里有大文件或者.git目录,构建会很慢。建议写一个.dockerignore,把无用文件排除掉。
3.2 使用Docker Compose编排多容器
单容器用docker run够用,多容器联动就必须上Compose了。Compose用YAML文件描述整个应用栈,一条docker compose up -d全部拉起。
一个典型的Compose文件结构:
version: "3.8" services: web: build: . ports: - "8080:80" depends_on: - redis redis: image: redis:7 volumes: - redis_data:/data volumes: redis_data:depends_on控制启动顺序,但如果Web应用依赖Redis就绪后才能工作,光靠depends_on不够,需要在应用里加重试逻辑或者用healthcheck。我在项目中就吃过这个亏:容器启动了,但应用连不上数据库,因为数据库还在初始化。后来加了健康检查才稳定。
3.3 MySQL 8.0主从部署实例
生产环境数据库是最容易出问题的环节,我建议即使是测试环境也养成用数据卷的习惯。部署单个MySQL 8.0:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourStrongPass \ -e MYSQL_ROOT_HOST=% \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql-conf:/etc/mysql/conf.d \ mysql:8.0MY_SQL_ROOT_HOST设成%,允许外部连接,但生产环境建议限定网段。数据卷务必挂出来,我见过不少人容器一删,数据库全没了的惨案。
主从复制的话,需要额外配置。先把主库的server-id和binlog打开,再在从库容器里执行CHANGE MASTER TO。一个简化的做法是直接把自定义配置写在挂载的conf.d目录下,两个容器用同一个自定义网络,从库通过容器名连接主库。这里强调的是:使用容器名而不是IP,因为容器重建后IP会变,容器名不会。
3.4 Redis主从部署实例
Redis的主从部署比MySQL简单。创建自定义网络,然后启动主从两个容器:
docker network create redis-net docker run -d --name redis-master --network redis-net \ -p 6379:6379 \ -v redis-master-data:/data \ redis:7 --appendonly yes docker run -d --name redis-slave --network redis-net \ -p 6380:6379 \ -v redis-slave-data:/data \ redis:7 --appendonly yes --replicaof redis-master 6379验证主从状态,进入从库容器执行redis-cli info replication,看到role:slave并且master_link_status:up就说明主从通了。
这里有个细节:用--replicaof redis-master 6379而不是IP,Redis会通过容器名解析到对应地址,网络里跑得很稳。如果你把Redis当缓存用,数据可以不用持久化;但一旦当数据库或者消息队列用,appendonly yes务必开。
3.5 数据卷、网络与备份恢复
Docker网络默认有三种模式:bridge(默认单机桥接)、host(共享宿主机网络)、none。自定义bridge网络支持容器名间的DNS解析,这是Compose和多容器部署最常用的网络形态。一次docker compose up创建的服务自动进入同一个网络,服务名就是主机名,互相访问非常方便。
数据卷的备份恢复有个实用套路。备份:
docker run --rm -v mysql8_data:/data -v /backup:/backup \ busybox tar czf /backup/mysql_data.tar.gz -C /data .恢复就反向解压。这套方法对测试环境迁移数据很有用,单机生产环境应急也能应付。真正的生产环境,数据库备份请交给专门的备份工具,并定期做恢复演练,不要等出事了才后悔。
4. Kubernetes核心架构与控制逻辑
4.1 K8s到底解决了什么问题
Docker解决了单机上的容器运行问题,但当你有一堆服务器要跑几十个容器时,一连串问题就来了:容器挂在哪台机器?扩容往哪扩?服务怎么发现彼此?升级时如何不停机?挂掉的容器谁来拉起?
K8s就是来回答这些问题的。它的核心思想是声明式管理:你告诉它“我期望有3个副本的nginx”,K8s会不断把实际状态往期望状态上靠拢。副本掉了就拉起新的,扩容就把副本数调大。这个“控制循环”的机制,比任何脚本轮询都可靠。
4.2 Master节点与工作节点的组件分工
一个K8s集群在逻辑上分Master(控制面)和Node(工作节点)两部分。Master跑四类组件:kube-apiserver是所有请求的入口,etcd存储集群状态,kube-controller-manager执行各类控制循环,kube-scheduler负责把Pod调度到合适的节点。每个工作节点上则跑着kubelet(与Master通信,管理Pod生命周期)、kube-proxy(维护网络规则)和容器运行时(通常还是containerd)。
我在企业搭建时会特别关注etcd:它是整个集群的“记忆”,必须定期备份。etcd挂了而没备份,就等于集群失忆,所有服务和数据配置都找不到。企业项目中etcd至少三副本并分开部署,这是底线。
4.3 Pod、Deployment、Service三层抽象
K8s有三层核心抽象,理解了它们,K8s的骨架就通了。
Pod是K8s调度的最小单位,一个Pod内含一个或多个容器,共享网络和存储。真正的生产实践里,我建议一个Pod尽量只放一个主容器,边车容器(sidecar)才额外塞进去,比如日志采集、流量代理这类辅助角色。
Deployment管理一组无状态应用副本,它负责创建ReplicaSet,再通过ReplicaSet控制Pod数量。日常的滚动更新、回滚,都是这个层面完成。举个例子:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80Service提供的是稳定访问入口。Pod的IP随时会变,Service用标签选择器找到一组Pod,把请求负载均衡到它们上面。它解决的问题很实在:访问方不需要知道具体是哪个Pod在响应。
4.4 Device Plugin与资源调度机制
K8s默认调度器主要看CPU和内存。但机器学习任务需要GPU,有些场景要用FPGA或特殊网卡,这时就要靠Device Plugin机制了。
设备插件是运行在节点上的一个小服务,它向kubelet上报设备列表,kubelet把设备作为可扩展资源暴露给调度器。你在Pod的YAML里声明resources.limits,比如nvidia.com/gpu: 2,调度器就会只把Pod调度到确实还有两块GPU空闲的节点上。
这块我在生产环境中感触很深。没有Device Plugin之前,GPU资源全靠运维手工标注,节点多了根本不敢乱调度。有了插件之后,GPU资源变成了和CPU内存一样普通的调度对象,数据科学团队申请资源就方便多了。
5. Kubernetes企业项目实战与安全加固
5.1 一个典型的企业项目部署流程
实际企业项目,很少有人直接裸写YAML,基本都走GitOps流程。我的推荐路径是这样的:代码提交到Git仓库,CI阶段构建Docker镜像并推送到私有仓库,CD阶段用Helm或Kustomize渲染K8s清单,然后执行kubectl apply发布。
举个例子,一个典型的Web服务,Helm Chart里会包含Deployment、Service、Ingress、ConfigMap、Secret这几类资源。ConfigMap存配置文件,Secret存密码和密钥,两者都可以挂载成环境变量或文件。
有一个我反复强调的细节:应用日志不要直接写进容器文件系统,要通过stdout输出,再靠采集器收集。这样日志跟着Pod生命周期走,采集和查看都统一。直接在容器里写日志文件,Pod一重启日志就断了。
5.2 未授权访问漏洞与RBAC配置
K8s是典型的“默认不够安全”的系统,早期最出名的问题就是未授权访问。比如kubectl的--insecure-skip-tls-verify配合错误的RBAC配置,可能导致任何人都能直接访问API Server。还有kubelet的10250端口,如果不设认证,外部可以读取Pod信息甚至执行命令。
我自己在企业里踩过类似坑,后来老老实实做了三件事:
一是开启RBAC授权,用最小权限原则。比如给某个应用只授它所在Namespace的只读权限,其他资源一概禁止。
二是禁止匿名访问。API Server启动参数里不能有--anonymous-auth=true,集群的ServiceAccount不能乱绑定cluster-admin权限。
三是对外暴露面收口。API Server的6443端口不要直接暴露到公网,kubelet的10250端口只在集群内部可达。Ingress和API Server是两码事,别混在一起配置。
5.3 滚动更新与回滚操作
企业里发版最怕的是“一次发版挂掉整个服务”。Deployment天然支持滚动更新,默认策略是逐个替换Pod,每新增一个健康Pod后才杀掉一个旧Pod。命令很简单:
kubectl set image deployment/nginx-deploy nginx=nginx:1.26如果新版本有问题,看RollingUpdate的状态一目了然。回滚也只需要一行:
kubectl rollout undo deployment/nginx-deploy但更推荐的做法是“渐进式发布”:先把新版本Deployment的副本数设为1,用一个独立的Service引流少量测试流量,确认稳定后再把副本数拉满。这个模式生产环境很实用,只是不同团队的实施成本差别比较大,小型团队可以先从RollingUpdate加健康检查开始,逐步演进。
5.4 网络不通、API连接失败等常见故障排查
Docker和K8s的排障,我把高频问题整理成一张速查表:
| 现象 | 可能原因 | 排查命令/动作 |
|---|---|---|
| Docker容器间网络不通 | 没放同一个自定义网络 | docker network inspect |
| 容器能连外网但域名解析失败 | DNS配置问题 | 检查daemon.json的dns配置 |
| K8s Pod一直Pending | 资源不够/污点限制 | kubectl describe pod看事件 |
| Pod频繁重启 | 启动命令有问题或存活探测失败 | kubectl logs加--previous |
| Service访问不通 | 标签选择器不匹配 | kubectl get endpoints |
| Docker命令报权限错误 | 用户不在docker组 | 参考2.4节方案 |
| Docker Desktop npipe连接失败 | 引擎未启动/WSL内核太旧 | wsl --update后重启 |
这里说一个最关键的排查习惯:先看事件,再看日志,最后看配置。很多人一上来就改配置,结果越改越乱。kubectl describe pod xxx能直接告诉你调度失败的原因,kubectl logs能告诉应用为什么起不来,把这两步跑完,大部分问题都已经定位了。
网络方面,K8s集群用Calico或Flannel这类CNI插件,Pod和Service的流量走iptables或IPVS规则。如果出现“部分服务间歇性连不上”,优先查CNI插件的健康状态,其次查节点上的iptables规则是否被其他软件覆盖。我碰到过一次,就是改防火墙规则时顺手刷掉了K8s的链,折腾了半天才恢复。
6. 学习路线与个人踩坑心得
6.1 我建议的学习路径
如果完全零基础,我的建议顺序是:先花一周把Linux基本命令练熟,然后学Docker,重点掌握docker run、docker build、docker compose这三件事。能用Compose把一套前后端加数据库的项目跑起来,Docker这关就算过了。
K8s的学习不要一上来就搭集群,先在单机环境或云服务商托管集群上操作,从kubectl get pods看起,逐步理解Deployment、Service、Ingress、ConfigMap这些对象。等把工作负载玩明白了,再学kubeadm搭建高可用集群,了解etcd、证书、调度器这些控制面的东西。
最后建议补上Helm和GitOps,一个是打包和分发K8s应用的工业标准,一个是把应用发布变成代码审查流程的工程实践。这两样在企业里几乎是标配,早学早受益。
6.2 几条比较实用的经验
这几年下来,有几点心得想分享给正在学习的朋友。
第一,不要过度包装镜像。我见过有人把所有工具都塞进一个镜像里,结果镜像体积好几个GB,拉取部署全是泪。尽量用官方基础镜像,运行时依赖什么装什么,体积小、漏洞面也小。
第二,环境隔离要分清边界。Docker解决的是应用级依赖隔离,K8s解决的是集群级资源调度和故障恢复,两者不是替代关系。别指望Docker能解决高可用,也别嫌K8s太重,搞清楚每种工具解决什么层次的问题,架构设计才不会走偏。
第三,安全是学出来的也是配出来的。早期学习阶段,把K8s的端口暴露公网练手,是我做过最后悔的事之一。好在当时只是测试环境,没酿成大祸。建议大家不论环境大小,都养成配置RBAC、关闭匿名访问、不暴露管理端口的习惯。
最后再提一个小技巧:学习期间多用kubectl explain和docker inspect。这两个命令是官方“说明书”,能让你不依赖搜索引擎也能搞清楚每个字段的含义。我自己遇到不确定的YAML字段,第一反应永远是kubectl explain deployment.spec,比翻文档快得多。
容器化这条路,越往后越有意思。Docker只是起点,K8s的世界里还有Operator、Service Mesh、Serverless这些更广阔的主题等着探索。希望这篇总结能帮你少踩几个坑,把时间和精力花在真正有价值的事情上。