Docker Compose 到 Kubernetes:中级团队的容器化应用迁移实战与避坑指南
很多团队第一次做容器化,都是从 docker-compose.yml 开始的:本地启动快、配置直观、适合多人协作。但业务一旦进入测试、预发、生产阶段,Compose 的边界很快就会暴露出来:
- 服务扩缩容不方便
- 故障自愈能力弱
- 发布策略有限
- 服务发现和配置管理能力不足
- 对资源隔离、权限控制、监控治理支持偏弱
这时候,Kubernetes 往往就会进入视野。
但问题也很现实:Compose 到 Kubernetes 不是“语法翻译”。我见过不少团队一上来就把 YAML 硬转一遍,结果服务是跑起来了,但上线后出现各种问题:探针配错导致反复重启、配置和密钥混放、持久化目录丢数据、Ingress 路由不通、资源限制缺失导致节点被打爆。
这篇文章不打算只讲概念,而是带你从一个中等复杂度的 Compose 应用出发,完成一次可运行、可验证、能避坑的迁移过程。
背景与问题
先看一个典型场景:一个 Web 应用依赖 PostgreSQL 和 Redis,本地开发通过 Compose 启动。
典型 Compose 结构
web:业务服务db:PostgreSQLredis:缓存volumes:数据库持久化depends_on:启动顺序依赖.env:环境变量注入
很多团队会误以为:
depends_on能等价迁移到 Kubernetes- Compose 里的网络互通,到 K8s 里天然没问题
- 本地 volume 写法,换成 PVC 就行
- 把环境变量照搬进 Deployment 就算完成迁移
实际上,这几个点几乎都是迁移中的高频坑。
前置知识与环境准备
建议你至少具备这些基础:
- 会读写
docker-compose.yml - 了解容器镜像、端口映射、环境变量
- 知道 Kubernetes 里的基本对象:
DeploymentServiceConfigMapSecretPersistentVolumeClaimIngress
本文示例环境
为了保证示例可运行,这里默认你本地有一套 Kubernetes 环境,例如:
minikubekind- Docker Desktop 自带 Kubernetes
并准备以下工具:
kubectl version --client
docker version
minikube version
如果你用的是 minikube,可执行:
minikube start
minikube addons enable ingress
核心原理
Compose 和 Kubernetes 最大的差别,不在“文件格式”,而在控制模型。
Compose 的思路:一次性启动一组容器
你描述的是:
- 有哪些容器
- 用什么镜像
- 暴露什么端口
- 用什么挂载和环境变量
然后 Docker 按配置启动它们。
Kubernetes 的思路:声明期望状态
你描述的是:
- 我希望有几个副本
- 我希望容器如何被调度
- 我希望服务如何被访问
- 我希望失败后如何自愈
- 我希望配置、密钥、存储如何独立管理
Kubernetes 控制器会持续把现实状态往“期望状态”拉齐。
flowchart LR
A[docker-compose.yml] --> B[启动容器组]
B --> C[容器运行]
D[Kubernetes YAML] --> E[API Server 保存期望状态]
E --> F[Controller 持续对齐]
F --> G[Pod/Service/Volume 实际运行]
Compose 常见字段与 Kubernetes 对应关系
| Compose | Kubernetes | 说明 |
|---|---|---|
services | Deployment / StatefulSet / Pod | 通常业务服务对应 Deployment |
ports | Service / Ingress | 容器端口不直接等于外部访问 |
environment | ConfigMap / Secret | 建议拆分配置与敏感信息 |
volumes | PersistentVolumeClaim | 持久化要显式声明 |
depends_on | 探针 + 初始化逻辑 | K8s 不保证简单启动顺序 |
restart | 控制器自愈机制 | 与 Pod 重建机制相关 |
networks | Service DNS / NetworkPolicy | 网络模型完全不同 |
迁移时真正要重建的能力
迁移不是“把 Compose 翻译成 K8s”,而是把这些能力重新建模:
- 服务发现
- 配置管理
- 健康检查
- 持久化存储
- 发布与回滚
- 权限与网络边界
- 资源治理
迁移路线图
我比较推荐中级团队按下面这个顺序推进,而不是一次性全量切:
flowchart TD
A[梳理 Compose 服务] --> B[区分有状态/无状态]
B --> C[先迁无状态应用]
C --> D[补齐 ConfigMap Secret Service]
D --> E[增加探针与资源限制]
E --> F[接入 Ingress]
F --> G[再迁缓存/数据库]
G --> H[压测与灰度验证]
H --> I[上线与回滚预案]
这里有个经验之谈:数据库不要在第一阶段就急着上 Kubernetes。如果你们还没成熟的存储方案、备份方案、监控方案,优先把业务应用迁过去,数据库先继续托管在外部或原环境,风险会低很多。
实战代码(可运行)
下面我们用一个可运行的例子来演示迁移。
第一步:Compose 应用示例
先定义一个原始 docker-compose.yml。
version: "3.9"
services:
web:
image: python:3.11-slim
container_name: demo-web
working_dir: /app
command: sh -c "pip install flask psycopg2-binary redis && python app.py"
volumes:
- ./app:/app
environment:
APP_ENV: dev
DB_HOST: db
DB_PORT: 5432
DB_NAME: appdb
DB_USER: appuser
DB_PASSWORD: apppass
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "5000:5000"
depends_on:
- db
- redis
db:
image: postgres:15
container_name: demo-db
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7
container_name: demo-redis
volumes:
pgdata:
对应的 app/app.py:
from flask import Flask, jsonify
import os
import psycopg2
import redis
app = Flask(__name__)
def check_postgres():
conn = psycopg2.connect(
host=os.getenv("DB_HOST", "db"),
port=int(os.getenv("DB_PORT", "5432")),
dbname=os.getenv("DB_NAME", "appdb"),
user=os.getenv("DB_USER", "appuser"),
password=os.getenv("DB_PASSWORD", "apppass"),
)
cur = conn.cursor()
cur.execute("SELECT 1;")
result = cur.fetchone()
cur.close()
conn.close()
return result[0] == 1
def check_redis():
r = redis.Redis(
host=os.getenv("REDIS_HOST", "redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
decode_responses=True
)
r.set("health", "ok")
return r.get("health") == "ok"
@app.route("/")
def index():
return jsonify({"message": "hello from compose or k8s"})
@app.route("/health")
def health():
try:
pg_ok = check_postgres()
redis_ok = check_redis()
return jsonify({"postgres": pg_ok, "redis": redis_ok}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
本地可以先验证:
docker compose up
访问:
curl http://localhost:5000/
curl http://localhost:5000/health
第二步:把应用镜像化
在 Kubernetes 中,不建议像 Compose 示例那样启动时临时 pip install。应该先构建应用镜像。
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py /app/app.py
RUN pip install --no-cache-dir flask psycopg2-binary redis
EXPOSE 5000
CMD ["python", "app.py"]
构建镜像:
docker build -t demo-web:v1 ./app
如果你使用 minikube,可把镜像构建到 minikube 环境:
eval $(minikube docker-env)
docker build -t demo-web:v1 ./app
第三步:拆分配置与密钥
这是从 Compose 迁到 Kubernetes 的第一件正事。
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-web-config
data:
APP_ENV: "prod"
DB_HOST: "demo-db"
DB_PORT: "5432"
DB_NAME: "appdb"
DB_USER: "appuser"
REDIS_HOST: "demo-redis"
REDIS_PORT: "6379"
Secret
注意:Kubernetes Secret 默认只是 base64 编码,不是加密本身。
apiVersion: v1
kind: Secret
metadata:
name: demo-web-secret
type: Opaque
stringData:
DB_PASSWORD: "apppass"
POSTGRES_PASSWORD: "apppass"
应用:
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
第四步:迁移 PostgreSQL 与 Redis
对于演示环境,我们先在集群里跑起来。生产环境请结合你们的数据策略决定是否托管在 K8s。
PostgreSQL PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-db-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
PostgreSQL Deployment 与 Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-db
spec:
replicas: 1
selector:
matchLabels:
app: demo-db
template:
metadata:
labels:
app: demo-db
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: appdb
- name: POSTGRES_USER
value: appuser
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: demo-web-secret
key: POSTGRES_PASSWORD
volumeMounts:
- name: db-data
mountPath: /var/lib/postgresql/data
volumes:
- name: db-data
persistentVolumeClaim:
claimName: demo-db-pvc
---
apiVersion: v1
kind: Service
metadata:
name: demo-db
spec:
selector:
app: demo-db
ports:
- port: 5432
targetPort: 5432
Redis Deployment 与 Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-redis
spec:
replicas: 1
selector:
matchLabels:
app: demo-redis
template:
metadata:
labels:
app: demo-redis
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: demo-redis
spec:
selector:
app: demo-redis
ports:
- port: 6379
targetPort: 6379
应用:
kubectl apply -f postgres.yaml
kubectl apply -f redis.yaml
第五步:迁移 Web 应用
重点来了:不要再找 depends_on 的替代品。Kubernetes 中更合理的做法是:
- 服务通过
Service名称访问依赖 - 使用
readinessProbe控制接流 - 必要时在应用启动逻辑里做重试
- 或增加
initContainer做前置检查
Web Deployment 与 Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-web
spec:
replicas: 2
selector:
matchLabels:
app: demo-web
template:
metadata:
labels:
app: demo-web
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z demo-db 5432; do
echo "waiting for postgres...";
sleep 2;
done
- name: wait-for-redis
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z demo-redis 6379; do
echo "waiting for redis...";
sleep 2;
done
containers:
- name: web
image: demo-web:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
envFrom:
- configMapRef:
name: demo-web-config
- secretRef:
name: demo-web-secret
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: demo-web
spec:
selector:
app: demo-web
ports:
- port: 80
targetPort: 5000
type: ClusterIP
应用:
kubectl apply -f web.yaml
检查状态:
kubectl get pods
kubectl get svc
kubectl describe pod -l app=demo-web
第六步:通过 Ingress 暴露服务
如果你的集群已经安装 Ingress Controller,可以继续配置。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-web-ingress
spec:
ingressClassName: nginx
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-web
port:
number: 80
应用:
kubectl apply -f ingress.yaml
如果是 minikube:
minikube ip
然后在本机 /etc/hosts 添加:
<minikube-ip> demo.local
验证:
curl http://demo.local/
curl http://demo.local/health
逐步验证清单
实际迁移时,我建议每一步都做“小闭环验证”,不要一口气全上。
1. 基础资源是否创建成功
kubectl get all
kubectl get configmap
kubectl get secret
kubectl get pvc
kubectl get ingress
2. Pod 是否健康
kubectl get pods -o wide
kubectl describe pod <pod-name>
kubectl logs <pod-name>
3. Service 是否可达
进入一个临时 Pod 测试 DNS 和端口:
kubectl run netshoot --rm -it --image=nicolaka/netshoot -- /bin/bash
在容器里执行:
dig demo-db
dig demo-redis
curl http://demo-web
nc -zv demo-db 5432
nc -zv demo-redis 6379
4. 发布滚动更新是否生效
修改镜像版本后执行:
kubectl set image deployment/demo-web web=demo-web:v2
kubectl rollout status deployment/demo-web
kubectl rollout history deployment/demo-web
如果有问题可回滚:
kubectl rollout undo deployment/demo-web
Compose 到 Kubernetes 的迁移映射示意
classDiagram
class ComposeService {
image
ports
environment
volumes
depends_on
}
class Deployment {
replicas
template
probes
resources
}
class Service {
selector
port
targetPort
}
class ConfigMap {
data
}
class Secret {
stringData
}
class PVC {
storage
}
ComposeService --> Deployment : 主体迁移
ComposeService --> Service : 端口暴露
ComposeService --> ConfigMap : 普通配置
ComposeService --> Secret : 敏感配置
ComposeService --> PVC : 持久化卷
常见坑与排查
这一节是最值钱的部分。很多坑不是不会写 YAML,而是以 Compose 的直觉去理解 Kubernetes。
坑 1:把 depends_on 当成强依赖保障
现象:
- Web Pod 启动后立刻报数据库连接失败
- Pod 反复重启
原因:
- Kubernetes 不提供 Compose 风格的启动顺序保证
- 依赖服务“Pod 已启动”不代表“应用已可用”
排查:
kubectl logs deployment/demo-web
kubectl get pods
kubectl describe pod -l app=demo-web
建议:
- 使用
readinessProbe - 使用应用级重试
- 必要时使用
initContainer - 不要只靠端口检测,最好检测业务健康接口或实际连接能力
坑 2:探针写得太激进
现象:
- 应用明明能启动,但不停被重启
CrashLoopBackOff
原因:
livenessProbe过早触发- 启动慢,探针超时太短
- 健康检查依赖下游,导致下游短暂抖动时主服务被杀
我当时就踩过这个坑:把 /health 同时给 readiness 和 liveness 共用,结果数据库抖了一下,业务 Pod 被大量重启,雪上加霜。
建议:
readinessProbe可以检查依赖可用性livenessProbe更适合检查进程本身是否卡死- 启动慢的服务优先加
startupProbe
示例:
startupProbe:
httpGet:
path: /health
port: 5000
failureThreshold: 30
periodSeconds: 5
坑 3:数据库直接用 Deployment,上线后数据异常
现象:
- Pod 重建后数据丢失
- 多副本数据库行为异常
- PVC 绑定和调度问题频发
原因:
- 有状态服务通常不该简单照搬无状态部署模型
- 生产数据库涉及主从、备份、恢复、IO 性能、磁盘拓扑
建议:
- 中小团队优先使用云数据库或托管数据库
- 如果必须上 K8s,优先评估
StatefulSet、备份策略、存储类能力 - 不要把“能跑”当成“可生产”
坑 4:Service 通了,但 Ingress 不通
现象:
- 集群内部能
curl demo-web - 外部通过域名访问失败
排查路径:
kubectl get ingress
kubectl describe ingress demo-web-ingress
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
常见原因:
- 没安装 Ingress Controller
ingressClassName不匹配- 域名没解析到入口 IP
- 路径规则写错
- 后端 Service 端口不一致
坑 5:环境变量全塞 Secret 或全塞 ConfigMap
问题:
- 配置分类混乱,维护成本高
- 权限粒度不清晰
- 审计困难
建议边界:
- 普通配置:
ConfigMap - 密钥、密码、令牌:
Secret - 高敏感场景:结合 KMS、External Secrets、Sealed Secrets
坑 6:忘记资源限制,节点被“吃满”
现象:
- 一个服务 CPU 飙高拖慢整节点
- OOMKilled
- 多服务互相影响
建议:
至少为每个业务容器设置:
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
并基于压测持续调整,不要拍脑袋定终值。
安全最佳实践
Compose 时代很多安全问题不明显,到了 Kubernetes,边界变大了,安全就不能靠默认值了。
1. 容器不要默认 root 运行
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
2. 最小化镜像内容
- 优先使用 slim/alpine/distroless
- 删除构建时不必要工具
- 固定镜像版本,不要长期用
latest
3. Secret 不进 Git 明文
可选方案:
- CI/CD 注入
- External Secrets Operator
- Vault
- 云厂商 Secret Manager
4. 收紧网络访问
如果你的集群支持 NetworkPolicy,建议限制 Web 只能访问必要依赖。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: demo-web-egress
spec:
podSelector:
matchLabels:
app: demo-web
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: demo-db
ports:
- protocol: TCP
port: 5432
- to:
- podSelector:
matchLabels:
app: demo-redis
ports:
- protocol: TCP
port: 6379
5. 控制权限范围
默认 ServiceAccount 往往权限过大。生产环境建议:
- 为业务单独创建 ServiceAccount
- 按需绑定 RBAC
- 避免滥用 cluster-admin
性能最佳实践
迁移到 Kubernetes 后,性能问题经常不是“应用慢了”,而是资源模型变化导致行为不同。
1. 请求与限制要分开设置
requests决定调度基础limits决定资源上限
如果只配 limits 不配 requests,调度行为可能不符合预期。
2. 健康检查不要做重操作
/health 最好轻量、快速、可预期。不要在每次探针里做复杂 SQL 或大对象访问,不然探针本身会制造压力。
3. 缓存和数据库连接池参数要重调
Kubernetes 下副本数增加后,总连接数也会增加。原来单机 1 个实例的连接池参数,扩成 4 个副本时可能直接把数据库打满。
建议重新核算:
- 每 Pod 最大连接数
- 最大副本数
- 数据库允许总连接数
- 高峰期连接回收策略
4. 关注冷启动成本
如果镜像太大、启动依赖太多、探针过严,滚动发布时会变慢,进而影响可用性。
可优化项:
- 减少镜像层
- 延迟非关键初始化
- 使用
startupProbe - 合理设置
maxUnavailable和maxSurge
5. 用 Horizontal Pod Autoscaler 前先量化指标
不要因为“有 K8s”就立刻上自动扩缩容。先确保:
- 有可靠指标源(如 Metrics Server / Prometheus)
- 知道 CPU、内存是否真能反映业务负载
- 下游依赖能承受扩容后的并发
发布与回滚策略建议
中级团队在迁移阶段,最实用的是保守滚动发布,而不是一上来蓝绿、金丝雀全上。
推荐顺序
- 先单副本验证功能
- 再双副本验证无状态行为
- 配置就绪探针
- 开启滚动发布
- 演练回滚
一个简单但实用的发布心法
- 先让系统可观测,再让系统可扩展
- 先跑通最小闭环,再追求优雅架构
- 先迁应用,再迁状态
这三条看起来朴素,但真的能少踩很多坑。
一份更贴近生产的迁移检查表
配置层
- 配置与密钥已分离
- 镜像版本已固定
- 无
latest - 环境变量命名统一
可用性层
- readinessProbe 已配置
- livenessProbe 已验证
- 必要时添加 startupProbe
- 有回滚命令和回滚预案
存储层
- 持久化路径已确认
- PVC 已绑定成功
- 有备份与恢复方案
- 明确哪些服务不适合立即迁入 K8s
网络层
- Service 发现正常
- Ingress 已通
- 域名解析已配置
- 网络策略按需限制
资源层
- requests/limits 已配置
- 做过基本压测
- 节点容量能覆盖副本扩容
安全层
- 容器非 root
- Secret 不明文入库
- RBAC 最小权限
- 基础镜像经过漏洞扫描
总结
从 Docker Compose 迁移到 Kubernetes,真正的难点从来不是 YAML 语法,而是思维模型切换:
- Compose 是“把服务跑起来”
- Kubernetes 是“持续维持服务以某种方式稳定运行”
如果你的团队已经熟悉 Compose,迁移时最稳妥的策略不是“一次性重做所有东西”,而是:
- 先拆出配置、密钥、存储
- 先迁无状态服务
- 补齐探针、资源限制、Service 和 Ingress
- 通过小步验证建立发布与排障信心
- 最后再决定是否迁数据库等有状态组件
最后给几个可执行建议,适合中级团队直接落地:
- 如果是第一次迁移,先不要把数据库搬进 Kubernetes
- 不要迷信自动转换工具,迁移结果一定要人工校验
- 每个服务至少补上:
readinessProbe、resources、Service - 每次迁一个服务,完成后做连通性、发布、回滚演练
- 把“能启动”升级成“可观测、可恢复、可回滚”
如果你们当前还在 Compose 阶段,不必焦虑“Kubernetes 不上不行”。真正适合迁移的信号是:你们已经需要更稳定的发布、更细的资源治理、更标准的运维边界。到了这个阶段,再上 Kubernetes,收益才会真正体现出来。