Docker Compose 到 Kubernetes 迁移实战:中型项目的容器编排改造与避坑指南
很多团队都是这样起步的:本地开发先用 Docker Compose,把 web、api、db、redis 一把拉起来,简单直接,效率很高。
但项目一旦进入多人协作、测试环境、灰度发布、资源隔离、自动恢复这些阶段,Compose 往往就开始吃力了。
这篇文章不讲“概念大全”,而是从中型项目迁移的视角,带你把一个典型的 Compose 系统拆解、映射到 Kubernetes,并且把常见坑一并说清楚。你可以把它理解成一份“从能跑到能稳定跑”的迁移手册。
背景与问题
先看一个常见的中型项目场景:
- 一个
nginx网关 - 一个
app应用服务 - 一个
worker异步任务服务 - 一个
redis - 一个
postgres
在 Docker Compose 里,大家通常这么干:
- 用一个
docker-compose.yml启动所有服务 - 通过服务名互相访问,比如
redis:6379 - 用
depends_on控制启动顺序 - 用宿主机目录或命名卷持久化数据库
- 用
.env管理配置
这在单机上很好用,但迁移到 Kubernetes 时,问题会集中冒出来:
- Compose 的“启动顺序”不等于服务就绪
- Kubernetes 不接受“我先启动数据库再启动应用”这种简单逻辑
- 本地卷挂载模式不能直接照搬
- 环境变量、密钥、配置文件需要拆分治理
- 一个 Compose 文件里揉在一起的关注点,在 K8s 里会被拆成多个资源对象
- 服务发现、暴露方式、健康检查、滚动更新都要重做
我当时第一次迁移时,最大的误区就是:以为只是把 Compose 翻译成 YAML。
后来发现,真正的迁移不是“语法转换”,而是编排模型转换。
前置知识与环境准备
建议你至少具备这些基础:
- 会写基本的 Dockerfile
- 理解容器端口、卷、环境变量
- 知道 Kubernetes 的这些对象:
- Pod
- Deployment
- Service
- ConfigMap
- Secret
- PersistentVolumeClaim
本地实验环境可以这样准备:
- Docker Desktop + Kubernetes
- 或者
minikube - 或者
kind
本文默认你有:
kubectl- 一个可用的 Kubernetes 集群
- 可以构建并推送镜像到仓库
验证命令:
kubectl version --client
kubectl get nodes
一个典型 Compose 项目长什么样
先给一个可运行的 Compose 示例,后面我们就围绕它迁移。
version: "3.8"
services:
nginx:
image: nginx:1.25
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
app:
image: myregistry.example.com/demo-app:1.0.0
environment:
APP_ENV: production
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: demo
DB_USER: demo
DB_PASSWORD: demo123
REDIS_HOST: redis
REDIS_PORT: "6379"
ports:
- "3000:3000"
depends_on:
- postgres
- redis
worker:
image: myregistry.example.com/demo-app:1.0.0
command: ["python", "worker.py"]
environment:
APP_ENV: production
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: demo
DB_USER: demo
DB_PASSWORD: demo123
REDIS_HOST: redis
REDIS_PORT: "6379"
depends_on:
- postgres
- redis
redis:
image: redis:7
ports:
- "6379:6379"
postgres:
image: postgres:15
environment:
POSTGRES_DB: demo
POSTGRES_USER: demo
POSTGRES_PASSWORD: demo123
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
这个文件在开发环境没有问题,但放到 Kubernetes 里,不能直接“一键翻译”。
核心原理
迁移前,先把 Compose 和 Kubernetes 的思维差异理顺。这个部分非常关键,后面的 YAML 才不会写成“看起来像 K8s,实际上还是 Compose 思维”。
1. 对象模型映射
Compose 和 Kubernetes 常见映射关系如下:
| Docker Compose | Kubernetes |
|---|---|
| service | Deployment / StatefulSet / Job |
| ports | Service / Ingress / containerPort |
| environment | ConfigMap / Secret / env |
| volumes | PVC / PV / emptyDir / hostPath |
| depends_on | readinessProbe / initContainer / 重试机制 |
| restart | Deployment 控制器 / Pod 重启策略 |
| scale | Deployment replicas / HPA |
关键点在于:
Compose 的 service 是一种“应用描述单元”,Kubernetes 则把“运行、访问、配置、存储”拆成多个资源对象。
2. 为什么 depends_on 在 Kubernetes 里不好使
Compose 的 depends_on 只是让容器按顺序启动,不保证服务真的能用。
Kubernetes 更强调:
- 应用要能处理依赖暂时不可用
- 用健康检查表达“我准备好了”
- 必要时用
initContainer做前置等待 - 更推荐业务层自己做重试
下面这张图能说明差异。
flowchart TD
A[Compose 启动 service] --> B[按 depends_on 顺序拉起容器]
B --> C[容器已启动]
C --> D[但数据库未必可连接]
E[Kubernetes 创建 Pod] --> F[容器启动]
F --> G[readinessProbe 检查]
G --> H[通过后才接收流量]
F --> I[依赖未就绪时应用需重试]
3. Kubernetes 更关注“声明式期望状态”
在 Compose 里,我们经常是在描述“怎么启动”。
在 Kubernetes 里,我们更像是在声明:
- 我要几个副本
- 我要暴露什么端口
- 我要什么配置
- 健康状态如何判断
- 资源上限和下限是什么
控制器会不断把实际状态拉回期望状态。
sequenceDiagram
participant U as 用户
participant A as API Server
participant C as Controller
participant N as Node/Kubelet
participant P as Pod
U->>A: kubectl apply -f deployment.yaml
A-->>C: 存储期望状态
C->>N: 调度并创建 Pod
N->>P: 拉镜像并启动容器
P-->>A: 上报运行状态
C-->>A: 持续对比实际与期望
4. 中型项目迁移的推荐拆分方法
不要一口气把所有服务全迁走。比较稳妥的顺序是:
- 无状态服务先迁移:
app、worker、nginx - 依赖改为集群内 Service 名称
- 配置和密钥分离
- 健康检查补齐
- 再处理有状态服务:如
postgres - 最后加 Ingress、自动扩缩容、监控告警
如果数据库已经有稳定的托管方案,通常建议优先使用云数据库或外部托管数据库,而不是在第一阶段就把数据库也塞进集群。
迁移设计:先定边界,再写 YAML
为了让实践更清晰,我这里采用一个比较现实的迁移边界:
app、worker、nginx迁移到 Kubernetesredis和postgres先在 Kubernetes 内运行,便于演示- 生产上可以替换为托管 Redis/Postgres
资源规划如下:
Namespace:demoConfigMap: 非敏感配置Secret: 数据库密码Deployment:app、worker、nginxService:app、redis、postgres、nginxPVC:postgres数据卷
实战代码(可运行)
下面给出一套可以直接 kubectl apply 的示例。
建议你按文件拆分保存。
1. Namespace
apiVersion: v1
kind: Namespace
metadata:
name: demo
保存为 00-namespace.yaml。
2. ConfigMap 与 Secret
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-config
namespace: demo
data:
APP_ENV: "production"
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "demo"
DB_USER: "demo"
REDIS_HOST: "redis"
REDIS_PORT: "6379"
---
apiVersion: v1
kind: Secret
metadata:
name: demo-secret
namespace: demo
type: Opaque
stringData:
DB_PASSWORD: "demo123"
POSTGRES_PASSWORD: "demo123"
保存为 01-config.yaml。
这里为了演示用了
stringData,生产里建议接入外部密钥系统,后面会讲。
3. Redis Service + Deployment
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: demo
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 5
保存为 02-redis.yaml。
4. Postgres Service + PVC + Deployment
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: demo
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: demo-config
key: DB_NAME
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: demo-config
key: DB_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: demo-secret
key: POSTGRES_PASSWORD
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 20
periodSeconds: 10
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
保存为 03-postgres.yaml。
严格来说,数据库更适合
StatefulSet,本文先用最容易理解的最小可运行方案。
如果是正式生产,推荐使用StatefulSet + Headless Service + 备份策略,或者直接上托管数据库。
5. App Service + Deployment
这里模拟一个 Python Web 服务。
apiVersion: v1
kind: Service
metadata:
name: app
namespace: demo
spec:
selector:
app: app
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- >
until nc -z postgres 5432;
do echo waiting for postgres;
sleep 2;
done
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- >
until nc -z redis 6379;
do echo waiting for redis;
sleep 2;
done
containers:
- name: app
image: myregistry.example.com/demo-app:1.0.0
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: demo-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: demo-secret
key: DB_PASSWORD
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 20
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
保存为 04-app.yaml。
这里有两个迁移重点:
depends_on换成了initContainers + readinessProbe- 应用自身最好仍然有数据库/Redis 重试逻辑
6. Worker Deployment
Worker 通常不需要 Service,因为它不对外接流量。
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command:
- sh
- -c
- >
until nc -z postgres 5432;
do echo waiting for postgres;
sleep 2;
done
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- >
until nc -z redis 6379;
do echo waiting for redis;
sleep 2;
done
containers:
- name: worker
image: myregistry.example.com/demo-app:1.0.0
command: ["python", "worker.py"]
envFrom:
- configMapRef:
name: demo-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: demo-secret
key: DB_PASSWORD
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
保存为 05-worker.yaml。
7. Nginx ConfigMap + Service + Deployment
先准备 Nginx 配置,通过集群内 Service 访问 app:3000。
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: demo
data:
default.conf: |
server {
listen 80;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: demo
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config-volume
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: nginx-config-volume
configMap:
name: nginx-config
保存为 06-nginx.yaml。
8. 一次性部署
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-config.yaml
kubectl apply -f 02-redis.yaml
kubectl apply -f 03-postgres.yaml
kubectl apply -f 04-app.yaml
kubectl apply -f 05-worker.yaml
kubectl apply -f 06-nginx.yaml
检查资源状态:
kubectl get all -n demo
kubectl get pvc -n demo
查看 Pod 启动细节:
kubectl describe pod -n demo <pod-name>
kubectl logs -n demo <pod-name>
逐步验证清单
迁移时我很推荐按下面的顺序验,不要“全部 apply 完了再一起看”。
第一步:存储是否就绪
kubectl get pvc -n demo
确认 postgres-pvc 已经 Bound。
第二步:基础依赖是否可达
kubectl get svc -n demo
kubectl get pods -n demo -o wide
确认:
redisService 在 6379postgresService 在 5432- Pod 状态为
Running
第三步:应用是否通过 readiness
kubectl get pods -n demo
看 READY 列是否正常,比如 1/1。
如果一直是 0/1,通常是:
- readinessProbe 配错
- 应用接口
/readyz没实现 - 应用依赖外部服务超时
第四步:应用服务发现是否正确
进入 nginx Pod 内测试:
kubectl exec -it -n demo deploy/nginx -- sh
然后执行:
wget -qO- http://app:3000/healthz
如果能返回结果,说明集群内服务发现正常。
第五步:从集群外访问
kubectl get svc nginx -n demo
拿到 NodePort 后访问:
curl http://<node-ip>:30080/
本地 minikube 的话可以这样:
minikube service nginx -n demo
从 Compose 到 Kubernetes 的迁移对照表
这部分适合边迁边查。
1. 端口暴露
Compose:
ports:
- "8080:80"
Kubernetes 中通常拆为:
- 容器端口:
containerPort - 集群内暴露:
Service - 集群外暴露:
NodePort/LoadBalancer/Ingress
2. 配置文件挂载
Compose:
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
Kubernetes:
- 用
ConfigMap - 再通过
volumeMounts + subPath挂载
3. 环境变量
Compose:
environment:
DB_HOST: postgres
DB_PASSWORD: demo123
Kubernetes:
- 普通配置放
ConfigMap - 密码放
Secret
这一步不要偷懒。我见过不少迁移项目把所有密码直接塞到 Deployment 里,短期省事,长期一定出事。
4. 卷
Compose:
volumes:
pgdata:
Kubernetes:
PersistentVolumeClaim- 底层由集群存储类提供
PersistentVolume
注意:K8s 里的存储不是“宿主机目录换个写法”那么简单。
尤其跨节点调度时,本地目录方案很容易出问题。
常见坑与排查
这是迁移里最值钱的部分。下面这些问题,我几乎都见过。
坑 1:Pod 已启动,但服务不可用
现象
kubectl get pods显示Running- 但是访问应用报 502、连接超时、数据库连接失败
原因
- 容器启动 != 应用准备好
- 没配
readinessProbe - 依赖服务虽然启动了,但还没就绪
排查
kubectl describe pod -n demo <pod-name>
kubectl logs -n demo <pod-name>
重点看:
- readiness probe 失败信息
- 应用日志里的连接错误
initContainer是否卡住
建议
- 所有对外服务都配
readinessProbe - 数据库、缓存等依赖尽量让应用有重试逻辑
initContainer适合做简单等待,但别把它当万能药
坑 2:Service 名字对了,还是连不上
现象
应用里配置了 DB_HOST=postgres,但就是解析失败或连接不到。
常见原因
- Service selector 不匹配 Pod label
- Pod 不在同一 namespace
- 容器监听地址错误,只监听
127.0.0.1 - 应用实际使用了旧环境变量
排查
查看 Service 和 Endpoints:
kubectl get svc -n demo
kubectl get endpoints -n demo
如果 Endpoints 为空,基本就是 selector 没匹配上。
坑 3:配置更新了,Pod 没生效
现象
修改了 ConfigMap,但应用行为没变。
原因
- 环境变量注入到 Pod 后,不会自动更新
- 有些文件挂载会更新,但应用未热加载
建议
修改后主动滚动重启:
kubectl rollout restart deployment app -n demo
kubectl rollout restart deployment nginx -n demo
检查发布状态:
kubectl rollout status deployment app -n demo
坑 4:数据库数据丢了
现象
Pod 重建后,Postgres 里的数据没了。
原因
- 没有使用 PVC
- 误用了
emptyDir - 重建资源时删掉了 PVC 或底层卷
建议
- 数据库务必确认持久卷绑定成功
- 先做备份,再做迁移
- 正式环境尽量使用托管数据库或成熟的数据库 Operator
坑 5:镜像拉取失败
现象
Pod 一直 ImagePullBackOff
排查
kubectl describe pod -n demo <pod-name>
常见原因:
- 镜像地址写错
- 镜像标签不存在
- 私有仓库缺少拉取凭证
私有仓库凭证示例:
kubectl create secret docker-registry regcred \
--docker-server=myregistry.example.com \
--docker-username=myuser \
--docker-password=mypass \
--docker-email=my@example.com \
-n demo
Deployment 中引用:
spec:
template:
spec:
imagePullSecrets:
- name: regcred
坑 6:资源限制没配,集群被“吃爆”
现象
- 某个应用突然占满 CPU/内存
- 节点不稳定
- Pod 被驱逐
原因
- 没有
requests/limits - 没做容量预估
- Worker 类服务无限抢资源
建议
哪怕是初版,也至少为每个服务配基础资源边界。
安全最佳实践
迁移到 Kubernetes 后,安全边界会比 Compose 更复杂,但也更可控。
1. 不要把密钥直接写进 Deployment
错误方式:
env:
- name: DB_PASSWORD
value: "demo123"
更好的方式:
Secret- 外部密钥管理系统,如 Vault、云厂商 KMS/Secrets Manager
2. 给容器最小权限
如果镜像支持,尽量使用非 root 用户运行:
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
当然,readOnlyRootFilesystem 不是所有镜像都兼容,像某些需要写临时文件的程序要先验证。
3. 限制命名空间内的网络访问
中型项目迁移后,服务变多,建议逐步引入 NetworkPolicy,避免“谁都能访问数据库”。
一个最简思路是:
- 只允许
app和worker访问postgres - 只允许
app和worker访问redis
4. 镜像固定版本,不用 latest
坏习惯:
image: myapp:latest
更稳妥的方式:
image: myapp:1.0.0
更进一步可以使用 digest。
性能最佳实践
Compose 迁移到 Kubernetes 后,性能问题常常不是代码变慢,而是资源与探针配置不合理。
1. requests 和 limits 要分开考虑
requests决定调度基础limits决定资源上限
经验上:
- Web 应用先保守估计
- Worker 根据任务峰值做单独配置
- 数据库不要套用 Web 服务模板
2. readinessProbe 不要太激进
我踩过一个坑:应用启动要 15 秒,readinessProbe 5 秒开始查,连续失败后上游不断摘流量、重试、告警,看起来像“服务抖动”。
建议:
- 根据真实启动时间设置
initialDelaySeconds - 探针接口尽量轻量,不要在
/readyz里做重 SQL
3. 无状态服务优先水平扩展
像 app 这种服务,适合先做多副本:
replicas: 2
后续可以再配 HPA。
但数据库、带本地状态的服务不要随便“加副本”。
4. 用 Ingress 代替自建网关暴露
本文为了最小示例保留了 nginx + NodePort。
如果你已经进入正式环境,通常建议:
- 使用 Ingress Controller
- 让流量入口、TLS、路由规则统一治理
nginx是否还保留,要看你是否有自定义反向代理逻辑
一个更稳妥的迁移路线
如果是中型项目,我推荐下面这个顺序,风险更低:
flowchart LR
A[梳理 Compose 服务] --> B[拆分配置/密钥/存储]
B --> C[先迁移无状态服务]
C --> D[补齐探针与资源限制]
D --> E[迁移缓存和异步任务]
E --> F[最后迁移数据库或接入托管服务]
F --> G[接入 Ingress 监控 告警 自动扩缩容]
这条路线的核心是:
优先把“应用编排能力”迁过去,再处理最敏感的状态数据。
边界条件:什么时候不该急着迁 Kubernetes
这点也很重要。不是所有 Compose 项目都值得立刻迁 K8s。
如果你的项目符合下面情况,可以先别急:
- 只有 2~3 个服务
- 单机部署已稳定
- 没有弹性扩缩容需求
- 团队缺少 Kubernetes 运维经验
- 监控、日志、CI/CD 还没建立
因为 Kubernetes 会带来额外复杂度:
- YAML 增多
- 调试链路变长
- 网络、存储、权限模型更复杂
我的建议是:
当你已经明显被 Compose 的单机边界、恢复能力、发布治理卡住时,再迁最值。
总结
把 Docker Compose 迁移到 Kubernetes,真正难的不是 YAML 语法,而是这几个观念切换:
- 从“启动顺序”转向“就绪与自恢复”
- 从“单文件描述一切”转向“资源对象拆分治理”
- 从“本地卷和环境变量”转向“PVC、ConfigMap、Secret”
- 从“能跑”转向“可观测、可扩缩、可回滚”
如果你准备开始动手,我建议按下面的最小步骤执行:
- 先列出 Compose 中所有服务及依赖
- 标记哪些是无状态、哪些是有状态
- 先迁
app/worker/nginx这类无状态服务 - 补齐
readinessProbe、livenessProbe、资源限制 - 配置拆到
ConfigMap/Secret - 数据库优先考虑托管方案,实在要上集群再谨慎做持久化
- 每迁一个服务就做连通性和回滚验证
最后给一句很实在的建议:
不要追求一次性“完美迁移”,而要追求“每一步都能验证、能回退”。
中型项目最怕的不是迁得慢,而是看起来迁完了,结果一上线问题成串。
如果你愿意,这篇文章里的 YAML 就可以作为你第一个迁移基线,从一个最小可运行版本开始,逐步演进。