Skip to content
Published on

Docker & Kubernetes 入门完全指南 — 从容器到编排

分享
Authors

1. 容器到底是什么

虚拟机与容器的区别

传统的虚拟机 (VM) 在 hypervisor 之上运行一整套 Guest OS。每台 VM 都包含内核、库和二进制文件,因此需要数 GB 磁盘和数十秒的启动时间。

容器则共享宿主机 OS 的内核,只在进程级别做隔离。镜像大小只有几十 MB,启动时间是毫秒级。

项目虚拟机容器
隔离级别整个 OS进程
镜像大小数 GB几十~几百 MB
启动时间数十秒毫秒
资源开销
可移植性依赖 hypervisor内核一致就能到处跑

Linux 命名空间与 cgroups

容器的隔离由两项 Linux 内核功能实现。

命名空间(Namespace)限制进程所能看到的系统资源范围。

  • PID 命名空间:在容器内部看到的是从 PID 1 开始的独立进程树
  • NET 命名空间:拥有独立的网络接口、IP 地址与路由表
  • MNT 命名空间:拥有独立的文件系统挂载点
  • UTS 命名空间:拥有独立的主机名
  • IPC 命名空间:拥有独立的 IPC 资源
  • USER 命名空间:拥有独立的 UID/GID 映射

cgroups(Control Groups) 限制 CPU、内存、磁盘 I/O 等硬件资源的使用量。

# 用 cgroup 确认内存限制
cat /sys/fs/cgroup/memory/docker/CONTAINER_ID/memory.limit_in_bytes

正是靠这两项功能,容器才能一边表现得像一台独立机器,一边共享内核保持轻量。


2. Docker 基础

三个核心概念

镜像(Image)是只读的模板。应用代码、运行时、系统库、配置文件等以分层结构保存在其中。

容器(Container)是镜像的运行实例。它在镜像之上追加了一个可写层。

镜像仓库(Registry)是存储与分发镜像的服务。Docker Hub 最具代表性,此外还有 AWS ECR、GitHub Container Registry 等私有仓库。

必备命令

# pull 镜像
docker pull nginx:1.25

# 运行容器
docker run -d --name my-nginx -p 8080:80 nginx:1.25

# 查看运行中的容器
docker ps

# 查看容器日志
docker logs my-nginx

# 进入容器内部
docker exec -it my-nginx /bin/bash

# 停止并删除容器
docker stop my-nginx
docker rm my-nginx

# 构建镜像
docker build -t my-app:1.0 .

# 把镜像推送到镜像仓库
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0

理解镜像的分层结构

Docker 镜像由多个只读层组成。Dockerfile 中的每条指令都会生成一层。

# 查看镜像分层
docker history my-app:1.0

层是会被缓存的。没有变化的层不会重新构建,因此在 Dockerfile 中把变更频率低的指令放在上面,是构建速度优化的关键。


3. Dockerfile 最佳实践

基本的 Dockerfile 结构

# 指定基础镜像
FROM node:20-alpine

# 设置工作目录
WORKDIR /app

# 先复制依赖文件(缓存优化)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# 复制源代码
COPY . .

# 暴露端口
EXPOSE 3000

# 启动命令
CMD ["node", "server.js"]

多阶段构建

把构建工具与运行时环境分开,可以大幅缩小最终镜像的体积。

# 第 1 阶段: 构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# 第 2 阶段: 生产
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

只在构建阶段需要的 devDependencies、源代码和构建工具都不会进入最终镜像。

分层缓存优化

# 坏例子: 源码一有变更就会重新执行 npm install
COPY . .
RUN npm ci

# 好例子: 只要 package.json 没变就能命中缓存
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

.dockerignore 文件

用它避免把不必要的文件带进构建上下文。

node_modules
.git
.env
*.md
dist
.DS_Store
coverage

安全最佳实践

# 1. 用非 root 用户运行
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 2. 使用固定版本的基础镜像(禁止 latest 标签)
FROM node:20.11.1-alpine3.19

# 3. 使用 COPY,避开 ADD
COPY ./config /app/config

# 4. 不要把敏感信息打进镜像
# 构建时可以用 ARG,但要注意别让它残留在最终镜像里

4. Docker Compose

多容器编排

Docker Compose 用一个 YAML 文件定义并管理多个容器。

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src
    networks:
      - backend

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:
    driver: bridge

Compose 核心命令

# 启动全部服务
docker compose up -d

# 查看日志
docker compose logs -f app

# 重新构建指定服务后启动
docker compose up -d --build app

# 查看服务状态
docker compose ps

# 停止全部服务并清理资源
docker compose down -v

网络与卷

网络:同一个 Compose 文件中的服务之间可以用服务名互相通信。上面的示例里,app 服务通过 db:5432 访问数据库。

:即使容器被删除,数据也会保留。名为 postgres_data 的 named volume 保管着数据库文件。

开发环境 vs 生产环境

# docker-compose.override.yml(开发环境自动生效)
services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development
    command: npm run dev
# 生产部署时排除 override
docker compose -f docker-compose.yml up -d

5. Kubernetes 架构

为什么需要 Kubernetes

Docker Compose 适合在单台主机上管理多个容器,但真实的生产环境还需要这些能力。

  • 跨多台服务器部署容器
  • 自动伸缩
  • 服务发现与负载均衡
  • 滚动更新与回滚
  • 自愈 (self-healing)

Kubernetes(K8s) 就是提供上述全部能力的容器编排平台。

Control Plane 组件

API Server(kube-apiserver):处理集群全部请求的中央关口。kubectl 命令、内部组件、外部客户端都要经过 API Server。

etcd:保存集群全部状态的分布式键值存储。哪个 Pod 跑在哪里、存在哪些 Service,这类信息都在这里。

Scheduler(kube-scheduler):决定新创建的 Pod 该放到哪个节点上。它会综合考虑资源需求、节点状态、亲和性规则等。

Controller Manager(kube-controller-manager):负责把集群当前状态调整到期望状态 (desired state)。其中包含 ReplicaSet Controller、Node Controller、Job Controller 等。

Worker Node 组件

kubelet:运行在每个节点上,按照 API Server 的指示启动容器并汇报状态。

kube-proxy:在每个节点上管理网络规则,处理 Service 的负载均衡。

Container Runtime:实际运行容器的软件。containerd 使用得最为广泛。

Control Plane
  +------------------+
  | API Server       |<--- kubectl, 客户端
  | etcd             |
  | Scheduler        |
  | Controller Mgr   |
  +------------------+
        |
  Worker Node 1          Worker Node 2
  +-----------------+   +-----------------+
  | kubelet         |   | kubelet         |
  | kube-proxy      |   | kube-proxy      |
  | containerd      |   | containerd      |
  | [Pod] [Pod]     |   | [Pod] [Pod]     |
  +-----------------+   +-----------------+

6. Kubernetes 核心对象

Pod

Pod 是 K8s 中可部署的最小单位。它包含一个或多个容器,这些容器共享同一套网络与存储。

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0
      ports:
        - containerPort: 3000
      resources:
        requests:
          memory: "128Mi"
          cpu: "250m"
        limits:
          memory: "256Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /healthz
          port: 3000
        initialDelaySeconds: 10
        periodSeconds: 5
      readinessProbe:
        httpGet:
          path: /ready
          port: 3000
        initialDelaySeconds: 5
        periodSeconds: 3

ReplicaSet

保证指定数量的 Pod 副本始终处于运行状态。通常不直接使用,而是通过 Deployment 来管理。

Deployment

以声明式方式管理 Pod 与 ReplicaSet。支持滚动更新、回滚与伸缩。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:1.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
# 查看部署状态
kubectl rollout status deployment/my-app

# 更新镜像(触发滚动更新)
kubectl set image deployment/my-app app=my-app:2.0

# 回滚
kubectl rollout undo deployment/my-app

# 伸缩
kubectl scale deployment/my-app --replicas=5

Service

为 Pod 提供稳定的网络端点。Pod 是临时的,而 Service 的 IP 与 DNS 是固定的。

ClusterIP(默认值):只能从集群内部访问。

apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

NodePort:通过每个节点上的特定端口从外部访问。

apiVersion: v1
kind: Service
metadata:
  name: my-app-nodeport
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080

LoadBalancer:自动预置云厂商的负载均衡器。

apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

Ingress

把 HTTP/HTTPS 流量路由到集群内部的 Service。可以定义基于主机名与路径的路由规则。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-svc
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 80
  tls:
    - hosts:
        - app.example.com
      secretName: tls-secret

7. Kubernetes 配置管理

ConfigMap

把环境配置与容器镜像分离开来管理。

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db-service"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  app.properties: |
    server.port=3000
    cache.ttl=300
# 在 Pod 中使用 ConfigMap
spec:
  containers:
    - name: app
      image: my-app:1.0
      envFrom:
        - configMapRef:
            name: app-config
      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config
        items:
          - key: app.properties
            path: app.properties

Secret

管理密码、API 密钥等敏感数据。它们以 base64 编码后保存。

# 创建 Secret
kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=s3cret
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=
  password: czNjcmV0
# 在 Pod 中使用 Secret
spec:
  containers:
    - name: app
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password

PersistentVolume (PV) 与 PersistentVolumeClaim (PVC)

让数据独立于 Pod 生命周期而保留下来。

# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard
# 在 Deployment 中使用 PVC
spec:
  containers:
    - name: postgres
      image: postgres:16-alpine
      volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: postgres-storage
      persistentVolumeClaim:
        claimName: postgres-pvc

8. Helm - Kubernetes 包管理器

什么是 Helm

Helm 是把 K8s manifest 打包成 chart 来管理的工具。它可以把多个 YAML 文件当作一个整体来安装、升级和回滚。

chart 结构

my-app-chart/
  Chart.yaml          # chart 元数据
  values.yaml         # 默认配置值
  templates/          # K8s manifest 模板
    deployment.yaml
    service.yaml
    ingress.yaml
    configmap.yaml
    _helpers.tpl      # 模板辅助函数
  charts/             # 依赖 chart

values.yaml

# values.yaml
replicaCount: 3

image:
  repository: my-app
  tag: "1.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  hostname: app.example.com

resources:
  requests:
    cpu: 250m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilization: 70

Helm 核心命令

# 安装 chart
helm install my-release ./my-app-chart

# 用自定义 values 安装
helm install my-release ./my-app-chart -f production-values.yaml

# 升级 release
helm upgrade my-release ./my-app-chart --set image.tag=2.0

# 查看 release 列表
helm list

# 查看 release 状态
helm status my-release

# release 历史
helm history my-release

# 回滚
helm rollback my-release 1

# 删除 release
helm uninstall my-release

按环境管理 values 文件

# 按环境划分的文件结构
values.yaml              # 通用默认值
values-dev.yaml          # 开发环境
values-staging.yaml      # 预发环境
values-prod.yaml         # 生产环境
# 部署到预发环境
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-staging.yaml \
  --namespace staging

# 部署到生产环境
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-prod.yaml \
  --namespace production

9. 实战部署示例 - Web 应用 + DB + Redis

整体构成

这是把 Web 应用、PostgreSQL 数据库和 Redis 缓存部署到 Kubernetes 的完整示例。

创建命名空间

kubectl create namespace my-app

部署 PostgreSQL

# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: mydb
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
      volumes:
        - name: postgres-data
          persistentVolumeClaim:
            claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: my-app
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

部署 Redis

# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports:
            - containerPort: 6379
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "250m"
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: my-app
spec:
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379

部署 Web 应用

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: my-web-app:1.0
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              value: "postgres://$(DB_USER):$(DB_PASS)@postgres:5432/mydb"
            - name: REDIS_URL
              value: "redis://redis:6379"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: DB_PASS
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: web-app
  namespace: my-app
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: my-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80

部署顺序

# 1. 创建 Secret
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=secure-password-here \
  -n my-app

# 2. 创建 PVC
kubectl apply -f postgres-pvc.yaml

# 3. 部署数据库
kubectl apply -f postgres-deployment.yaml

# 4. 部署 Redis
kubectl apply -f redis-deployment.yaml

# 5. 部署 Web 应用
kubectl apply -f app-deployment.yaml

# 6. 确认状态
kubectl get all -n my-app

10. 故障排查

CrashLoopBackOff

Pod 反复启动又崩溃的状态。

# 确认原因
kubectl describe pod POD_NAME -n my-app
kubectl logs POD_NAME -n my-app --previous

# 常见原因:
# - 应用启动时发生错误
# - 环境变量配置错误
# - 无法连接到依赖的服务
# - livenessProbe 失败

解决方法:查看日志定位错误。调大 livenessProbe 的 initialDelaySeconds,或确认环境变量是否正确。

ImagePullBackOff

无法拉取容器镜像的状态。

# 确认原因
kubectl describe pod POD_NAME -n my-app

# 常见原因:
# - 镜像名或标签拼写错误
# - 私有镜像仓库未配置认证
# - 镜像不存在

# 配置私有镜像仓库认证
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass \
  -n my-app

OOMKilled

容器超出内存限制被强制终止的状态。

# 确认原因
kubectl describe pod POD_NAME -n my-app

# 查看实时资源用量
kubectl top pod -n my-app

# 解决: 调大内存 limits 值
resources:
  limits:
    memory: "512Mi"  # 从原来的 256Mi 提高

常用的调试命令

# 查看 Pod 列表与状态
kubectl get pods -n my-app -o wide

# Pod 详细信息(含事件)
kubectl describe pod POD_NAME -n my-app

# 实时查看日志
kubectl logs -f POD_NAME -n my-app

# 进入 Pod 内部
kubectl exec -it POD_NAME -n my-app -- /bin/sh

# 确认 Service 端点
kubectl get endpoints -n my-app

# 在集群内部确认 DNS
kubectl run debug --rm -it --image=busybox -- nslookup web-app.my-app.svc.cluster.local

# 节点资源现状
kubectl top nodes

结语

把本文讲过的内容整理如下。

  1. 容器基础:用命名空间与 cgroups 实现轻量隔离
  2. Docker:镜像构建、多阶段构建、安全实践
  3. Docker Compose:开发环境中的多容器管理
  4. K8s 架构:Control Plane 与 Worker Node 的职责
  5. K8s 对象:Pod、Deployment、Service、Ingress 的用法
  6. 配置管理:ConfigMap、Secret、PV/PVC
  7. Helm:用 chart 做发布管理
  8. 实战部署:Web 应用 + DB + Redis 的三层架构
  9. 故障排查:应对 CrashLoopBackOff、ImagePullBackOff、OOMKilled

Docker 与 Kubernetes 是现代基础设施的地基。希望你能在本地环境(minikube 或 kind)亲手把本文的示例跑一遍。真正去部署 Pod、配置 Service、动手排障,才是最快的学习方式。