Docker 与 Kubernetes 生产级实践:从容器化到集群管理
Docker容器最佳实践(多阶段构建、镜像优化、安全扫描)和Kubernetes集群管理(Pod设计模式、HPA/VPA自动伸缩、网络策略、存储管理、Helm Chart模板化部署),覆盖CICD流水线中的容器化全流程。
容器化不只是”写个 Dockerfile”
很多团队把应用容器化理解为”写个 Dockerfile,能跑就行”。但生产环境的容器化远不止于此——镜像大小、构建速度、安全性、资源管理,每个环节都可能成为线上事故的根源。
我们团队在过去两年容器化了 30+ 个微服务,从最开始 2GB 的臃肿镜像到现在 50MB 的精简镜像,踩过的坑值得分享。
Docker 最佳实践
多阶段构建
多阶段构建是 Docker 最被低估的特性之一。它的核心思想是:构建环境和运行环境分离。
# ============ 构建阶段 ============
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# ============ 运行阶段 ============
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
ENV TZ=Asia/Shanghai
COPY --from=builder /app/server /server
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/server"]
效果对比:
- 单阶段构建镜像:~800MB(包含 Go 编译器、源码、依赖)
- 多阶段构建镜像:~15MB(只有二进制 + ca-certificates)
选择正确的基础镜像
| 基础镜像 | 大小 | 安全性 | 适用场景 |
|---|---|---|---|
scratch | 0MB | 最高 | 静态编译的 Go/Rust 程序 |
alpine | ~5MB | 高 | 需要 shell 和基础工具 |
distroless | ~20MB | 最高 | 不需要 shell 的 Java/Node/Python |
ubuntu | ~77MB | 中 | 需要完整系统库 |
python:3.12 | ~1GB | 低 | 避免!用 slim 或 alpine 版本 |
推荐使用 distroless:
# 用 distroless 替代 alpine
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
distroless 没有包管理器、没有 shell、甚至没有 ls 命令。攻击者即使入侵了容器,能做的事情也非常有限。
层级优化
Docker 的每个指令创建一个层,层的顺序直接影响构建速度和镜像大小:
# ❌ 不好:频繁变化的放在前面
COPY . . # 每次代码改动都要重建
RUN pip install -r requirements.txt # 依赖不改也要重新下载
# ✅ 好:稳定的放前面,频繁变化的放后面
COPY requirements.txt .
RUN pip install -r requirements.txt # 只在依赖变化时重建
COPY . . # 代码变化只影响这一层
.dockerignore 的重要性
# 不传到 Docker context 中
node_modules/
.git/
.gitignore
*.md
.env
.env.local
dist/
build/
coverage/
*.log
Dockerfile
docker-compose.yml
没有 .dockerignore,COPY . . 会把所有文件(包括 node_modules、.git 目录)都复制进镜像。
安全扫描
# Trivy 镜像扫描
trivy image myapp:latest
# Docker Scout
docker scout quickview myapp:latest
docker scout recommendations myapp:latest
# 在 CI 中集成
docker build -t myapp:$CI_COMMIT_SHA .
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:$CI_COMMIT_SHA
Kubernetes 部署实战
Pod 设计模式
Sidecar 模式:主容器 + 辅助容器
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
containers:
# 主容器:应用
- name: app
image: myapp:latest
ports:
- containerPort: 8080
# Sidecar:日志采集
- name: log-collector
image: fluentd:latest
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
Init Container:在主容器启动前执行初始化任务
spec:
initContainers:
- name: init-db
image: postgres:16
command: ['sh', '-c', 'until pg_isready -h db-service; do sleep 2; done']
- name: init-migration
image: myapp:latest
command: ['./migrate']
containers:
- name: app
image: myapp:latest
Init Container 按顺序执行,全部成功后才启动主容器。适合数据库等待、数据迁移、配置初始化等场景。
资源管理
Requests vs Limits:
resources:
requests: # 调度器用来分配节点的保证资源
cpu: "500m"
memory: "512Mi"
limits: # 容器能使用的最大资源
cpu: "2000m"
memory: "1Gi"
关键原则:
- requests 设置合理:太低会导致节点过载,太高会浪费资源
- limits 必须设置:没有 limits 的 Pod 可能 OOM 整个节点
- 不要设 CPU limits(争议):CPU 是可压缩资源,设 limits 可能导致不必要的 throttling
健康检查
livenessProbe: # 容器是否活着(失败 → 重启)
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe: # 容器是否准备好接收流量(失败 → 从 Service 移除)
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe: # 容器是否启动完成(1.16+)
httpGet:
path: /startup
port: 8080
failureThreshold: 30
periodSeconds: 10
三个探针的区别:
startupProbe:只用于启动阶段,保护慢启动的应用livenessProbe:运行时检查,失败就重启readinessProbe:流量开关,失败就切断流量但不会重启
livenessProbe 的常见陷阱:
- 不要检查外部依赖(数据库、Redis)——依赖挂了重启应用没用
- 用独立的轻量端点(不要和业务逻辑耦合)
initialDelaySeconds设足够长
自动伸缩
HPA(水平自动伸缩):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 缩容前等待 5 分钟
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0 # 扩容立即生效
policies:
- type: Percent
value: 100
periodSeconds: 15
网络策略
默认情况下,K8s 集群中所有 Pod 之间可以互相通信。生产环境必须配置 NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network-policy
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
# 只允许来自 frontend 和 monitoring 的流量
- from:
- podSelector:
matchLabels:
app: frontend
- podSelector:
matchLabels:
app: monitoring
ports:
- protocol: TCP
port: 8080
egress:
# 允许访问数据库
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# 允许 DNS 查询
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
存储管理
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: ssd # 高性能存储
resources:
requests:
storage: 100Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd
resources:
requests:
storage: 100Gi
StatefulSet 的 volumeClaimTemplates 会为每个 Pod 自动创建独立的 PVC,确保数据隔离。
Helm Chart 模板化
# values.yaml
replicaCount: 3
image:
repository: myapp
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
滚动更新策略
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多多出几个 Pod
maxUnavailable: 0 # 最多几个不可用(0 = 一个都不能少)
minReadySeconds: 10 # Pod Ready 后等 10 秒再认为可用
生产环境推荐 maxUnavailable: 0,确保更新过程中服务不中断。
CICD 集成
GitLab CI 示例
stages:
- build
- test
- scan
- deploy
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build:
stage: build
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
test:
stage: test
script:
- docker run --rm $IMAGE_TAG npm test
security_scan:
stage: scan
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE_TAG
- trivy image --severity MEDIUM --exit-code 0 $IMAGE_TAG
deploy_staging:
stage: deploy
script:
- helm upgrade --install myapp ./helm/myapp
--set image.tag=$CI_COMMIT_SHORT_SHA
--namespace staging
--wait
--timeout 5m
environment:
name: staging
deploy_production:
stage: deploy
script:
- helm upgrade --install myapp ./helm/myapp
--set image.tag=$CI_COMMIT_SHORT_SHA
--namespace production
--wait
--timeout 5m
environment:
name: production
when: manual # 生产部署需要手动触发
我们的实践经验
经验一:镜像标签策略
不要用 latest 标签!推荐方案:
环境 + Commit SHA + 时间戳
staging-a1b2c3d-20260529
production-a1b2c3d-20260529
这样:
- 知道每个环境跑的是哪个版本
- 可以快速回滚到任意版本
- 审计时有据可查
经验二:Graceful Shutdown
// Go 应用优雅关闭
func main() {
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("强制关闭:", err)
}
}
对应的 K8s 配置:
spec:
terminationGracePeriodSeconds: 60 # 等待应用优雅关闭的时间
经验三:PodDisruptionBudget
防止维护操作导致服务不可用:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp-pdb
spec:
minAvailable: 2 # 至少保持 2 个 Pod 可用
selector:
matchLabels:
app: myapp
经验四:日志管理
不要在容器里写日志文件!输出到 stdout/stderr:
// 好的做法
log.Println("server started on :8080")
// 不要这样做
f, _ := os.Create("/var/log/app.log")
log.SetOutput(f)
stdout/stderr 会被 K8s 自动采集,可以用 kubectl logs 查看。如果需要持久化,用 Loki 或 ELK 采集。
总结
容器化和 Kubernetes 的核心不是”会用”,而是”用好”。关键要点:
- 多阶段构建缩小镜像,distroless 提高安全性
- 资源 requests/limits 必须设置,这是集群稳定的基础
- 三种探针各司其职,livenessProbe 不要检查外部依赖
- NetworkPolicy 默认拒绝,最小权限原则
- Helm 模板化管理多环境配置
- 安全扫描集成到 CI 流水线中
- 优雅关闭配合 terminationGracePeriodSeconds