Node.js Docker 容器化与 Kubernetes 部署实战

Node.js 生产级容器化部署完整指南:Docker 多阶段构建、镜像体积优化、Docker Compose 本地开发、健康检查与优雅关闭、K8s 基础资源对象、HPA 自动扩缩容、滚动更新与蓝绿部署、Ingress 与 TLS、资源配额优化、Helm 打包与 GitHub Actions CI/CD 流水线。

将 Node.js 应用从本地代码仓库迁移到生产环境,容器化是现代软件交付的必经之路。容器技术为应用提供了统一的运行环境,消除了"在我的机器上可以运行"的经典困境。而 Kubernetes 作为容器编排领域的事实标准,进一步解决了大规模部署、服务发现、自动扩缩容和故障恢复等核心运维难题。

本文面向已将 Node.js 应用容器化或计划容器化的工程团队,从 Dockerfile 多阶段构建开始,系统覆盖镜像体积优化、Docker Compose 本地协同开发、健康检查与优雅关闭、Kubernetes 核心资源对象的声明式管理、HPA 自动扩缩容策略、滚动更新与蓝绿部署方案、Ingress 流量路由与 TLS 证书自动化、容器资源配额精细化调优、Helm 应用打包与多环境管理,以及 GitHub Actions 驱动的完整 CI/CD 流水线。每个环节都配有可直接使用的配置文件与命令示例,帮助团队快速建立起成熟、可运维的容器化交付体系。

1. Docker 多阶段构建:开发环境与生产环境分离

多阶段构建(Multi-stage Build)是减少生产镜像体积的核心手段。基本思路是:先用一个包含完备构建工具链的镜像完成编译与打包,再将产物复制到一个精简的运行时镜像中。

基础 Node.js Dockerfile(不推荐用于生产)

# 反面示例:构建与运行混在同一个阶段
FROM node:20
WORKDIR /app
COPY package*.json /app/
RUN npm install
COPY . /app/
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]

该方案的缺陷:

  • 构建产物中残留 node_modules 开发依赖(如 TypeScript、Jest、ESLint)
  • 源码直接暴露在镜像中
  • 体积动辄 1GB 以上

推荐的多阶段 Dockerfile

# ==== 阶段一:依赖安装与构建 ====
FROM node:20-slim AS builder
WORKDIR /app

# 仅复制依赖清单,利用 Docker 缓存层
COPY package*.json tsconfig.json ./
RUN npm ci --only=production=false

# 复制源码并构建
COPY src ./src
RUN npm run build

# ==== 阶段二:生产运行 ====
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app

# 使用 --chown 避免运行时权限问题
COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=builder --chown=nonroot:nonroot /app/package.json ./

# distroless 默认用户即为 nonroot,无需 USER 指令
EXPOSE 3000
CMD ["dist/main.js"]

多阶段构建的关键要点,以及开发阶段常用技巧:

  • builder 阶段负责编译 TypeScript 与安装完整依赖
  • 产物阶段仅保留编译后的 dist、生产级 node_modulespackage.json
  • --from=builder 实现跨阶段文件复制
  • 构建阶段使用 npm ci 而非 npm install,前者基于 package-lock.json 精确安装,速度更快且可复现
  • 通过 npm prune --production 移除开发依赖,进一步缩小镜像体积

Dockerfile.dev 用于本地开发环境,与生产 Dockerfile 分离:

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

开发版保留源码映射、热重载能力和开发依赖,而生产版只运行编译后的产物。两套 Dockerfile 各司其职,避免在开发时重复编译的繁琐。

2. 优化 Docker 镜像体积:Distroless、Alpine 与 Slim

Node.js 官方镜像提供多种变体,选择合适的基镜像直接影响安全面与镜像体积。

基镜像大致体积适用场景注意点
node:20~1.1GB兼容性测试体积最大,含完整 Debian
node:20-slim~240MB构建阶段精简 Debian,保留 apt
node:20-alpine~190MB通用运行使用 musl libc,部分原生模块需重新编译
gcr.io/distroless/nodejs20~150MB生产运行无 shell、无包管理器,最小攻击面

Alpine 版本 Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache dumb-init
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main.js"]

Alpine 使用注意事项:

  • musl libcglibc 存在行为差异,部分 C++ addon 编译可能失败
  • 使用 dumb-init 处理 PID 1 信号转发,确保 SIGTERM 正确送达 Node 进程

Distroless 选型建议

Distroless 镜像是 Google 维护的最小化镜像,特点包括:

  • 无 shell、无包管理器、无文本编辑器
  • 攻击面最小化
  • 适合已经对 Node.js 运行时有充分信心的团队

Distroless 镜像的适用判断标准:团队是否已建立完善的日志收集、指标监控和分布式追踪体系?如果问题的排查依赖于容器内执行命令,那么 Alpine 可能是更务实的选择。Distroless 的优势是安全而不是开发体验。

需要调试时,可以通过 Kubernetes 的 kubectl debug 临时注入调试容器:

kubectl debug -it <pod-name> --image=busybox --target=<container-name>

镜像层缓存策略

Docker 构建缓存按层命中,合理的文件复制顺序直接影响构建速度:

# 推荐顺序:先复制不常变更的文件
COPY package*.json ./
RUN npm ci --only=production

# 后复制经常变更的源码
COPY src ./src
RUN npm run build

package.json 变更频率远低于源码,因此先复制依赖清单可以有效利用缓存层。只有在依赖清单发生变化时,Docker 才会重新执行 RUN npm ci

3. Docker Compose 本地开发

Docker Compose 将应用及其依赖(数据库、缓存、消息队列)一键拉起,是本地开发与集测试的标准方案。

# docker-compose.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: node-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://postgres:postgres@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: ["npx", "nodemon", "src/main.ts"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s

  db:
    image: postgres:15-alpine
    container_name: node-db
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: node-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Docker Compose 常见命令:

docker compose up -d          # 后台启动全部服务
docker compose logs -f app    # 追踪 app 服务日志
docker compose exec app sh    # 进入 app 容器
docker compose restart app    # 重启单个服务
docker compose down -v        # 停止并清理数据卷

volumes 中的 /app/node_modules 空挂载技巧:将宿主机 node_modules 目录与容器隔离,避免跨平台原生模块冲突。例如开发环境使用 macOS,而容器基于 Linux,某些 C++ 原生模块在不同平台编译产物并不兼容,空挂载确保了容器始终使用容器内安装的正确版本。

本地开发时建议将环境变量管理集中化,避免在不同文件中重复维护。可以通过 .env 文件配合 env_file 指令统一注入:

services:
  app:
    env_file:
      - .env.local

.env.local 文件加入 .gitignore,每位开发者自行维护本地数据库密码等敏感配置,避免泄露到代码仓库。

4. 健康检查与优雅关闭

生产环境的服务必须具备两个能力:向编排系统报告自身状态,以及在收到停止信号时完成正在处理的请求

应用层面:健康检查端点

// health.controller.js
const express = require('express');
const router = express.Router();

const db = require('./db');
const redis = require('./redis');

// 存活探针(Kubernetes livenessProbe)
router.get('/health/live', (req, res) => {
    res.status(200).json({ status: 'alive' });
});

// 就绪探针(Kubernetes readinessProbe)
router.get('/health/ready', async (req, res) => {
    try {
        await db.query('SELECT 1');
        await redis.ping();
        res.status(200).json({ status: 'ready' });
    } catch (err) {
        res.status(503).json({ status: 'not ready', error: err.message });
    }
});

module.exports = router;
  • livenessProbe 失败时 Kubernetes 会重启容器
  • readinessProbe 失败时容器会被移出 Service 后端,停止接收新请求

优雅关闭信号处理

// graceful-shutdown.js
const http = require('http');

const app = require('./app');
const server = http.createServer(app);
const PORT = process.env.PORT || 3000;

const gracefulShutdown = (signal) => {
    console.log(`Received ${signal}. Starting graceful shutdown...`);
    server.close(() => {
        console.log('HTTP server closed.');
        // 关闭数据库连接、Redis 连接、消息队列等
        Promise.all([
            db.destroy(),
            redis.quit(),
        ]).then(() => {
            console.log('All connections closed. Exiting.');
            process.exit(0);
        }).catch((err) => {
            console.error('Error during shutdown:', err);
            process.exit(1);
        });
    });

    // 若超时未关闭,强制退出
    setTimeout(() => {
        console.error('Shutdown timeout. Forcing exit.');
        process.exit(1);
    }, 30000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

server.listen(PORT, () => {
    console.log(`Server listening on port ${PORT}`);
});

在 Dockerfile 中使用 dumb-inittini 作为 PID 1,确保信号正确转发给 Node 进程。

# 使用 tini 处理信号
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "graceful-shutdown.js"]

探针参数调优建议

探针配置不是越频繁越好,过高的探测频率会增加应用负载,尤其在数据库连接池紧张的 Node.js 应用中:

  • initialDelaySeconds 应大于应用启动耗时,避免启动阶段被误认为故障
  • periodSeconds 建议设为 10 秒,既保证响应灵敏度又不过度占用资源
  • failureThreshold 设为 3 次,给予应用短暂抖动缓冲
  • readinessProbe 建议检查所有外部依赖,包括数据库、缓存和消息队列
  • livenessProbe 应尽量轻量,仅检测进程存活即可,避免误判导致无意义的重启

5. Kubernetes 基础资源对象

Kubernetes(K8s)是目前容器编排的事实标准。对于 Node.js 应用,需要掌握的核心资源对象包括 Deployment、Service、ConfigMap 与 Secret。

Deployment:声明式部署

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app
  labels:
    app: node-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-app
  template:
    metadata:
      labels:
        app: node-app
    spec:
      containers:
        - name: app
          image: myregistry/node-app:v1.0.0
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: log-level
          livenessProbe:
            httpGet:
              path: /health/live
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 15"]
      terminationGracePeriodSeconds: 30

关键配置说明:

  • replicas: 3 保证三个 Pod 副本同时运行
  • livenessProbe 探测失败触发容器重启
  • readinessProbe 探测失败将 Pod 从 Service 流量中摘除
  • lifecycle.preStopSIGTERM 发送前执行 sleep 15,给就绪探针时间生效

Service:集群内服务发现

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: node-app-service
spec:
  selector:
    app: node-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP

ClusterIP 类型仅在集群内部访问。对外暴露需要使用 Ingress 或 LoadBalancer

ConfigMap 与 Secret:配置分离

# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  log-level: "info"
  api-timeout: "30000"
  max-concurrency: "100"
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  database-url: "postgres://user:password@db:5432/myapp"
  jwt-secret: "your-256-bit-secret-key-here"
  redis-password: "redis-auth-pass"

Secret 通过 stringData 而非 data 定义时,K8s 会自动执行 base64 编码。实际生产环境建议使用外部 Secret 管理方案,如 Sealed Secrets、External Secrets Operator 或云厂商托管服务。

6. Horizontal Pod Autoscaling(HPA)

HPA 根据指标自动扩缩 Pod 副本数,是应对流量波动的核心机制。

基于 CPU 与内存的 HPA

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: node-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: node-app
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60

HPA 配置要点:

  • minReplicas 防止流量低谷时缩容至零
  • maxReplicas 设置上限防止资源耗尽
  • scaleDownstabilizationWindowSeconds: 300 避免缩容过快导致抖动
  • 缩容速度通常显著慢于扩容,这是有意为之的安全策略

基于自定义指标的 HPA

对于 Node.js 应用,更精准的扩缩容可以基于应用层的指标,如:

  • 消息队列堆积深度
  • 请求延迟 p95
  • 每秒请求数(RPS)

使用自定义指标需要部署 Prometheus AdapterMetrics Server,在 HPA 中通过 PodsExternal 指标类型引用。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: node-app-hpa-custom
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: node-app
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "1000"

自定义指标扩缩容的实现需要三步:

  1. 在 Node.js 应用中通过 prom-client 暴露 /metrics 端点
  2. Prometheus 采集自定义指标
  3. Prometheus Adapter 将指标转换为 K8s Metrics API 可消费的形式

7. 滚动更新与蓝绿部署

滚动更新(Rolling Update)

Deployment 默认的更新策略即为滚动更新:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  • maxSurge: 25%:更新过程中最多超出目标副本数 25%
  • maxUnavailable: 25%:更新过程中允许最多 25% 的副本不可用

执行滚动更新:

kubectl set image deployment/node-app app=myregistry/node-app:v1.1.0
kubectl rollout status deployment/node-app
kubectl rollout history deployment/node-app
kubectl rollout undo deployment/node-app  # 回滚

蓝绿部署(Blue-Green)

蓝绿部署通过同时运行两套完全独立的 Deployment,在流量切换瞬间完成零停机发布。

# k8s/blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app-blue
  labels:
    app: node-app
    version: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-app
      version: blue
  template:
    metadata:
      labels:
        app: node-app
        version: blue
    spec:
      containers:
        - name: app
          image: myregistry/node-app:v1.0.0
          ports:
            - containerPort: 3000
# k8s/green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app-green
  labels:
    app: node-app
    version: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: node-app
      version: green
  template:
    metadata:
      labels:
        app: node-app
        version: green
    spec:
      containers:
        - name: app
          image: myregistry/node-app:v1.1.0
          ports:
            - containerPort: 3000

流量切换通过修改 Service 的 selector 完成:

apiVersion: v1
kind: Service
metadata:
  name: node-app-service
spec:
  selector:
    app: node-app
    version: green   # 从 blue 改为 green 完成切换
  ports:
    - port: 80
      targetPort: 3000

蓝绿部署的优势:

  • 发布与回滚都是瞬时切换
  • 新版本问题可立即切回旧版本
  • 适合对稳定性要求极高的场景

缺点是需要双倍资源,成本较高。

8. Ingress 控制器与 TLS 终止

Ingress 是 Kubernetes 的七层负载均衡器,统一管理外部流量到集群内部 Service 的路由。

Ingress 资源配置

# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: node-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/rate-limit-connections: "100"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls-secret
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: node-app-service
                port:
                  number: 80
          - path: /health
            pathType: Exact
            backend:
              service:
                name: node-app-service
                port:
                  number: 80

常用 Ingress 控制器对比:

控制器特点适用场景
NGINX Ingress成熟稳定,社区最大通用场景
Traefik原生支持 Let’s Encrypt,配置动态云原生环境
HAProxy高性能低延迟高吞吐量场景
Ambassador / Emissary基于 Envoy,支持 gRPC 路由微服务架构

TLS 证书自动管理

通过 cert-manager 与 Let’s Encrypt 配合,实现证书签发与续期的全自动化:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx

Node.js 应用无需处理 TLS 握手,所有加密和解密由 Ingress 控制器完成,Pod 内部保持明文 HTTP。这既降低了应用复杂度,又方便集中管理证书与加密策略。

9. 资源 Limits 与 Requests 优化

K8s 的资源配置直接影响调度效率与成本控制。

核心概念

  • requests:调度请求值,K8s 根据此值决定 Pod 可以调度到哪个节点
  • limits:运行时硬上限,超出 limits 的 CPU 会被节流,超出 limits 的内存会被 OOMKilled

Node.js 应用的资源基准

resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

Node.js 应用资源调优要点:

  1. 内存 requests 必须覆盖 V8 堆空间

    Node.js 默认堆上限约 1.4GB(64位系统)。若容器 limits 低于此值,需要在启动参数中调低:

    args:
      - "--max-old-space-size=768"
    
  2. CPU limits 与线程池

    libuv 线程池默认 4 线程。若应用有大量文件 I/O、crypto 运算或 zlib 压缩,CPU limits 至少设置为 1000m(1 核),否则线程竞争导致延迟。一个简单的判断标准是:如果 Node.js 进程的 UV_THREADPOOL_SIZE 超过了 limits 中 CPU 核心数的四倍,就需要考虑调高 CPU 配额。

  3. 请求值与服务等级

    资源充足时将 requests 与 limits 设为相同值,获得 Guaranteed QoS 等级,减少节点压力下被驱逐的概率。

    resources:
      requests:
        memory: "1Gi"
        cpu: "1000m"
      limits:
        memory: "1Gi"
        cpu: "1000m"
    

    K8s 在节点资源紧张时优先驱逐 Burstable 或 BestEffort 等级的 Pod。Node.js 应用通常是业务核心服务,应追求 Guaranteed 等级以保证稳定性。

10. Helm Charts:Node.js 应用打包

Helm 是 Kubernetes 的包管理工具,通过模板化的方式管理一组 K8s 资源。

目录结构

node-app-chart/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
    service.yaml
    ingress.yaml
    hpa.yaml
    configmap.yaml
    secret.yaml
    _helpers.tpl

Chart.yaml

apiVersion: v2
name: node-app
description: A Helm chart for Node.js application
type: application
version: 1.0.0
appVersion: "1.0.0"

values.yaml

replicaCount: 3

image:
  repository: myregistry/node-app
  pullPolicy: IfNotPresent
  tag: "v1.0.0"

service:
  type: ClusterIP
  port: 80
  targetPort: 3000

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: api-tls-secret
      hosts:
        - api.example.com

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

config:
  logLevel: info
  apiTimeout: 30000

secrets:
  databaseUrl: ""
  jwtSecret: ""

deployment.yaml 模板

templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "node-app.fullname" . }}
  labels:
    {{- include "node-app.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "node-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "node-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 3000
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /health/live
              port: http
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          env:
            - name: NODE_ENV
              value: "production"
            - name: LOG_LEVEL
              value: {{ .Values.config.logLevel | quote }}
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "node-app.fullname" . }}-secrets
                  key: database-url

Helm 部署命令

helm upgrade --install node-app ./node-app-chart \
  --namespace production \
  --create-namespace \
  --set image.tag=v1.1.0 \
  --set replicaCount=5 \
  --set "secrets.databaseUrl=postgres://prod:pass@db:5432/app"

Helm 的优势:

  • 同一套 Chart 可在开发、测试、生产环境复用
  • 通过 values-*.yaml 文件管理环境差异
  • 版本化管理,支持升级与回滚
  • 可在 CI/CD 流程中自动执行

自定义指令函数

Helm 提供丰富的模板函数,_helpers.tpl 文件定义可复用的模板片段:

templates/_helpers.tpl
{{/* Generate full name */}}
{{- define "node-app.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Generate labels */}}
{{- define "node-app.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

模板函数统一了资源命名、标签和选择器的生成逻辑,避免因手动维护造成的命名冲突。

11. CI/CD 流水线:GitHub Actions + Docker + K8s

完整的持续交付流水线覆盖代码提交、构建、测试、镜像推送与集群部署。

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run unit tests
        run: npm run test:unit

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=,suffix=,format=short

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Setup Helm
        uses: azure/setup-helm@v3
        with:
          version: "v3.13.0"

      - name: Setup kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: "v1.28.0"

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-southeast-1

      - name: Update kubeconfig
        run: aws eks update-kubeconfig --name production-cluster

      - name: Deploy with Helm
        run: |
          helm upgrade --install node-app ./helm/node-app \
            --namespace production \
            --set image.tag=${{ github.sha }} \
            --set replicaCount=3 \
            --wait \
            --timeout 5m

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/node-app -n production
          kubectl get pods -n production -l app=node-app

流水线设计要点

测试阶段

  • 在 PR 阶段即运行全量测试,防止问题代码合并
  • 集成测试可以复用 Docker Compose 在 CI 环境中临时启动数据库

构建阶段

  • docker/build-push-actioncache-from / cache-to 使用 GitHub Actions 缓存加速
  • 多阶段 Dockerfile 确保推送的镜像已经过构建验证
  • 基于语义化版本(SemVer)的 tag 策略,便于追踪与回滚

部署阶段

  • main 分支推送触发部署
  • 使用 AWS IAM Role 而非长期 Access Key 认证,零密码
  • --wait --timeout 5m 确保 Helm 等待 Pod 就绪后才视为部署成功

多环境管理

helm-values/
  values-development.yaml
  values-staging.yaml
  values-production.yaml
# 开发环境
helm upgrade --install node-app-dev ./helm/node-app \
  -f helm-values/values-development.yaml \
  --namespace dev

# 生产环境
helm upgrade --install node-app-prod ./helm/node-app \
  -f helm-values/values-production.yaml \
  --namespace production

部署 Checklist

在将 Node.js 应用交付生产前,逐项确认以下事项:

  • Dockerfile 使用多阶段构建,镜像体积控制在合理范围
  • 生产镜像使用 Distroless 或 Alpine,无开发依赖残留
  • 应用暴露 /health/live/health/ready 端点
  • 优雅关闭已处理 SIGTERMSIGINT 信号
  • HPA 已配置 CPU 与内存指标,并设定合理的扩缩容策略
  • Deployment 已配置 livenessProbereadinessProbe
  • 资源 Requests 与 Limits 已调优,内存不超出 V8 默认堆上限
  • Ingress 已启用 TLS,证书由 cert-manager 自动管理
  • Helm Chart 已统一管理所有 K8s 资源
  • CI/CD 流水线覆盖测试、构建、推送与部署全链路
  • 滚动更新策略已设定,具备快速回滚能力

结语

Node.js 应用的容器化与 Kubernetes 编排不是一个"一次性配置"的任务,而是一个持续迭代的过程。从 Dockerfile 的多阶段构建到 HPA 的自动扩缩容,从健康检查到 Helm 打包,从滚动更新到蓝绿部署,每个环节都在为同一个核心目标服务:快速、稳定、可回滚地将代码送达最终用户

在工程实践中,没有放之四海而皆准的最优配置。Docker 基镜像选择 Distroless 还是 Alpine,Ingress 控制器选择 NGINX 还是 Traefik,资源 requests 设为 256Mi 还是 512Mi,都取决于团队对开发体验、运维复杂度与安全边界的取舍。关键在于团队是否建立了系统性的度量与调优机制,能从日志、指标和追踪数据中持续发现瓶颈并迭代改进。

掌握 Docker 与 K8s 的核心原生能力,配合 Helm 的模板化管理与 GitHub Actions 的流水线自动化,Node.js 团队可以建立起生产级的交付体系。在这个体系之下,发布不再是深夜依赖人工值守的紧张操作,而是一个可观测、可自动化、可随时中断并回滚的标准流程。对每个环节的理解越深,生产环境出故障时的恢复速度就越快,团队的迭代信心也就越强。

容器化的本质是将运行时环境的不确定性降至最低,而 Kubernetes 的价值在于将这种确定性扩展到集群规模。两者结合,为 Node.js 这样的动态运行时语言提供了与静态编译语言同等级别的部署可靠性和运维可预见性。无论是微服务架构还是单体应用,这套交付框架都是现代后端工程不可或缺的基础设施。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nodejs」更多文章

  1. Node.js ORM 深度对比:Prisma、TypeORM、Sequelize 与 Drizzle
  2. Node.js 设计模式与最佳实践:从 SOLID 到六边形架构
  3. Node.js 高级测试策略:从单元测试到混沌工程的完整实践