将 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_modules与package.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 libc与glibc存在行为差异,部分 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-init 或 tini 作为 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.preStop在SIGTERM发送前执行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设置上限防止资源耗尽scaleDown的stabilizationWindowSeconds: 300避免缩容过快导致抖动- 缩容速度通常显著慢于扩容,这是有意为之的安全策略
基于自定义指标的 HPA
对于 Node.js 应用,更精准的扩缩容可以基于应用层的指标,如:
- 消息队列堆积深度
- 请求延迟 p95
- 每秒请求数(RPS)
使用自定义指标需要部署 Prometheus Adapter 或 Metrics Server,在 HPA 中通过 Pods 或 External 指标类型引用。
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"
自定义指标扩缩容的实现需要三步:
- 在 Node.js 应用中通过
prom-client暴露/metrics端点 - Prometheus 采集自定义指标
- 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 应用资源调优要点:
内存 requests 必须覆盖 V8 堆空间
Node.js 默认堆上限约 1.4GB(64位系统)。若容器 limits 低于此值,需要在启动参数中调低:
args: - "--max-old-space-size=768"CPU limits 与线程池
libuv 线程池默认 4 线程。若应用有大量文件 I/O、
crypto运算或zlib压缩,CPU limits 至少设置为 1000m(1 核),否则线程竞争导致延迟。一个简单的判断标准是:如果 Node.js 进程的UV_THREADPOOL_SIZE超过了 limits 中 CPU 核心数的四倍,就需要考虑调高 CPU 配额。请求值与服务等级
资源充足时将 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-action的cache-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端点 - 优雅关闭已处理
SIGTERM与SIGINT信号 - HPA 已配置 CPU 与内存指标,并设定合理的扩缩容策略
- Deployment 已配置
livenessProbe与readinessProbe - 资源 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 这样的动态运行时语言提供了与静态编译语言同等级别的部署可靠性和运维可预见性。无论是微服务架构还是单体应用,这套交付框架都是现代后端工程不可或缺的基础设施。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。