跳转到内容
123xiao | 无名键客

《Docker Compose 到 Kubernetes:中级团队的容器化应用迁移实战与避坑指南》

字数: 0 阅读时长: 1 分钟

Docker Compose 到 Kubernetes:中级团队的容器化应用迁移实战与避坑指南

很多团队第一次做容器化,都是从 docker-compose.yml 开始的:本地启动快、配置直观、适合多人协作。但业务一旦进入测试、预发、生产阶段,Compose 的边界很快就会暴露出来:

  • 服务扩缩容不方便
  • 故障自愈能力弱
  • 发布策略有限
  • 服务发现和配置管理能力不足
  • 对资源隔离、权限控制、监控治理支持偏弱

这时候,Kubernetes 往往就会进入视野。

但问题也很现实:Compose 到 Kubernetes 不是“语法翻译”。我见过不少团队一上来就把 YAML 硬转一遍,结果服务是跑起来了,但上线后出现各种问题:探针配错导致反复重启、配置和密钥混放、持久化目录丢数据、Ingress 路由不通、资源限制缺失导致节点被打爆。

这篇文章不打算只讲概念,而是带你从一个中等复杂度的 Compose 应用出发,完成一次可运行、可验证、能避坑的迁移过程。


背景与问题

先看一个典型场景:一个 Web 应用依赖 PostgreSQL 和 Redis,本地开发通过 Compose 启动。

典型 Compose 结构

  • web:业务服务
  • db:PostgreSQL
  • redis:缓存
  • volumes:数据库持久化
  • depends_on:启动顺序依赖
  • .env:环境变量注入

很多团队会误以为:

  1. depends_on 能等价迁移到 Kubernetes
  2. Compose 里的网络互通,到 K8s 里天然没问题
  3. 本地 volume 写法,换成 PVC 就行
  4. 把环境变量照搬进 Deployment 就算完成迁移

实际上,这几个点几乎都是迁移中的高频坑。


前置知识与环境准备

建议你至少具备这些基础:

  • 会读写 docker-compose.yml
  • 了解容器镜像、端口映射、环境变量
  • 知道 Kubernetes 里的基本对象:
    • Deployment
    • Service
    • ConfigMap
    • Secret
    • PersistentVolumeClaim
    • Ingress

本文示例环境

为了保证示例可运行,这里默认你本地有一套 Kubernetes 环境,例如:

  • minikube
  • kind
  • 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 对应关系

ComposeKubernetes说明
servicesDeployment / StatefulSet / Pod通常业务服务对应 Deployment
portsService / Ingress容器端口不直接等于外部访问
environmentConfigMap / Secret建议拆分配置与敏感信息
volumesPersistentVolumeClaim持久化要显式声明
depends_on探针 + 初始化逻辑K8s 不保证简单启动顺序
restart控制器自愈机制与 Pod 重建机制相关
networksService DNS / NetworkPolicy网络模型完全不同

迁移时真正要重建的能力

迁移不是“把 Compose 翻译成 K8s”,而是把这些能力重新建模:

  1. 服务发现
  2. 配置管理
  3. 健康检查
  4. 持久化存储
  5. 发布与回滚
  6. 权限与网络边界
  7. 资源治理

迁移路线图

我比较推荐中级团队按下面这个顺序推进,而不是一次性全量切:

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
  • 合理设置 maxUnavailablemaxSurge

5. 用 Horizontal Pod Autoscaler 前先量化指标

不要因为“有 K8s”就立刻上自动扩缩容。先确保:

  • 有可靠指标源(如 Metrics Server / Prometheus)
  • 知道 CPU、内存是否真能反映业务负载
  • 下游依赖能承受扩容后的并发

发布与回滚策略建议

中级团队在迁移阶段,最实用的是保守滚动发布,而不是一上来蓝绿、金丝雀全上。

推荐顺序

  1. 先单副本验证功能
  2. 再双副本验证无状态行为
  3. 配置就绪探针
  4. 开启滚动发布
  5. 演练回滚

一个简单但实用的发布心法

  • 先让系统可观测,再让系统可扩展
  • 先跑通最小闭环,再追求优雅架构
  • 先迁应用,再迁状态

这三条看起来朴素,但真的能少踩很多坑。


一份更贴近生产的迁移检查表

配置层

  • 配置与密钥已分离
  • 镜像版本已固定
  • latest
  • 环境变量命名统一

可用性层

  • readinessProbe 已配置
  • livenessProbe 已验证
  • 必要时添加 startupProbe
  • 有回滚命令和回滚预案

存储层

  • 持久化路径已确认
  • PVC 已绑定成功
  • 有备份与恢复方案
  • 明确哪些服务不适合立即迁入 K8s

网络层

  • Service 发现正常
  • Ingress 已通
  • 域名解析已配置
  • 网络策略按需限制

资源层

  • requests/limits 已配置
  • 做过基本压测
  • 节点容量能覆盖副本扩容

安全层

  • 容器非 root
  • Secret 不明文入库
  • RBAC 最小权限
  • 基础镜像经过漏洞扫描

总结

从 Docker Compose 迁移到 Kubernetes,真正的难点从来不是 YAML 语法,而是思维模型切换

  • Compose 是“把服务跑起来”
  • Kubernetes 是“持续维持服务以某种方式稳定运行”

如果你的团队已经熟悉 Compose,迁移时最稳妥的策略不是“一次性重做所有东西”,而是:

  1. 先拆出配置、密钥、存储
  2. 先迁无状态服务
  3. 补齐探针、资源限制、Service 和 Ingress
  4. 通过小步验证建立发布与排障信心
  5. 最后再决定是否迁数据库等有状态组件

最后给几个可执行建议,适合中级团队直接落地:

  • 如果是第一次迁移,先不要把数据库搬进 Kubernetes
  • 不要迷信自动转换工具,迁移结果一定要人工校验
  • 每个服务至少补上:readinessProberesourcesService
  • 每次迁一个服务,完成后做连通性、发布、回滚演练
  • 把“能启动”升级成“可观测、可恢复、可回滚”

如果你们当前还在 Compose 阶段,不必焦虑“Kubernetes 不上不行”。真正适合迁移的信号是:你们已经需要更稳定的发布、更细的资源治理、更标准的运维边界。到了这个阶段,再上 Kubernetes,收益才会真正体现出来。


分享到:

上一篇
《Node.js 中基于 Worker Threads 与队列的 CPU 密集型任务处理实战》
下一篇
《前端中台实践:基于 Vite + TypeScript 搭建可扩展的微前端工程体系》