人人都会AI编程

19.2 Docker 容器化部署

更新时间:2026-07-10

在上一节中我们讨论了如何使用 PM2 来守护 Node.js 进程、实现零停机重启和进程监控。但在现代运维体系里,仅仅保障单台机器上的进程稳定还不够——我们需要将应用打包为可移植的镜像,让它在任何安装了 Docker 的服务器上都能以完全一致的环境运行。这就是容器化部署的核心价值:将应用及其所有依赖(运行时、系统库、环境变量)封装在一个轻量级的隔离环境中,彻底告别“我电脑上没问题”的尴尬。

本节将带你完成从编写 Dockerfile、优化镜像到使用 docker-compose 编排的一整套 Node.js 容器化部署流程。

19.2.1 编写 Dockerfile:把 Node.js 应用装进镜像

Docker 通过 Dockerfile(一个纯文本的指令序列)来定义镜像的构建过程。对于典型的 Node.js 项目,一个最基本的 Dockerfile 长这样:

# 使用 Node.js 官方镜像作为基础
FROM node:18-alpine

# 设置容器内的工作目录
WORKDIR /app

# 拷贝 package.json 和 package-lock.json
COPY package*.json ./

# 安装生产依赖
RUN npm ci --only=production

# 拷贝应用源码
COPY . .

# 应用实际监听的端口(Express 默认 3000)
EXPOSE 3000

# 容器启动时执行的命令
CMD ["node", "src/index.js"]

这个 Dockerfile 做到了最基本的“能跑”,但镜像体积可能达到 150 MB 以上,而且包含了构建工具和开发依赖。下面我们会逐步优化它。

19.2.2 镜像优化:从细节入手减少体积与构建时间

在生产环境中,镜像体积直接影响拉取速度、存储成本和部署效率。以下几点优化建议非常实用:

1. 选择 Alpine 基础镜像

Node.js 官方提供了基于 Linux Alpine(一个极简发行版)的镜像,体积通常只有 Debian/Ubuntu 版本的 1/5。官方推荐使用 node:18-alpine 甚至 node:18-alpine-slim,除非你的应用依赖特定的 glibc 或编译工具。

2. 利用 .dockerignore 文件

在构建镜像时,Docker 会将当前目录下的所有文件发送给 Docker daemon(上下文)。如果包含 node_modules、日志、测试文件,不仅拖慢构建,还可能把敏感信息打进镜像。创建 .dockerignore

node_modules
npm-debug.log
.git
.gitignore
.env
dist
coverage
test

3. 合并 RUN 命令与清理缓存

Docker 的每一行 RUN 都会创建一个新的镜像层。合并多个命令可以减少层数,并在每一步后清理包管理器的缓存:

RUN apk add --no-cache python3 make g++ \
    && npm ci --only=production \
    && apk del python3 make g++

这里我们临时安装了编译工具来构建某些原生模块(如 bcryptsharp),完成后再卸载,目的是最终镜像不包含这些编译依赖。

4. 多阶段构建(Multi-stage Build)

如果项目中需要 TypeScript 编译、或者需要安装开发依赖来执行构建脚本,最简单的办法是使用多阶段构建:第一个阶段用完整的 Node 镜像进行构建,第二个阶段只复制构建产物和生产依赖。这会戏剧性地缩小最终镜像体积。

一个典型的 TypeScript + Express 项目的多阶段 Dockerfile 示例如下:

# ---------- 第一阶段:构建阶段 ----------
FROM node:18-alpine AS builder
WORKDIR /app

# 安装依赖
COPY package*.json ./
RUN npm ci

# 拷贝源码并编译 TypeScript
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# ---------- 第二阶段:生产运行阶段 ----------
FROM node:18-alpine
WORKDIR /app

# 只拷贝生产依赖所需的文件
COPY package*.json ./
RUN npm ci --only=production

# 从构建阶段拷贝编译后的 JS 文件
COPY --from=builder /app/dist ./dist

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

经过多阶段构建后,最终镜像里只有 Alpine 基础、生产依赖和编译好的纯 JavaScript 文件,整个镜像体积常常能被控制在 80 MB 以内,甚至更小。

19.2.3 环境变量与配置管理

容器化应用的一个重要原则是:配置应当通过环境变量注入,不应硬编码在镜像里。Node.js 项目通常用 dotenv 读取本地 .env,但在容器里可以直接使用 Docker 的环境变量,无需 .env 文件。

编写 Dockerfile 时不要将 .env 拷贝进去(.dockerignore 中已排除)。在 docker run 时注入:

docker run -d -p 3000:3000 \
  -e DB_HOST=mysql \
  -e DB_PORT=3306 \
  -e NODE_ENV=production \
  my-node-app:latest

也可以在 docker-compose.yml 中统一管理(见下文)。如果变量很多,可以使用 --env-file 传递一个文件(注意安全,不要在镜像中留下此文件)。

19.2.4 Docker Compose:编排多服务应用

实际项目通常不止一个 Node.js 服务,还会有 MySQL、Redis、Nginx 等依赖。docker-compose 让我们可以用声明式的方式定义多个服务,并管理它们之间的网络、数据卷和启动顺序。

以下是一个典型的 docker-compose.yml 示例,编排了 Node.js 应用 + MySQL + Redis:

version: "3.8"

services:
  app:
    build: .                    # 使用当前目录的 Dockerfile 构建
    container_name: my-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=secret
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    container_name: mysql-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: mydb
    volumes:
      - mysql-data:/var/lib/mysql   # 持久化数据
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    container_name: redis-cache
    restart: unless-stopped
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"

volumes:
  mysql-data:
  redis-data:

通过 depends_onapp 服务会在 mysqlredis 启动之后再启动(但请注意,depends_on 只保证容器启动顺序,不保证数据库服务已就绪;如果你的应用启动时必须数据库可用,最好在应用内部加入重试连接逻辑)。

进入项目目录后,一行命令即可启动所有服务:

docker-compose up -d

查看日志:docker-compose logs -f app;停止并删除容器(保留数据卷):docker-compose down;完全清除数据卷:docker-compose down -v

19.2.5 构建优化与 CI/CD 集成

在持续集成管道中,我们可以将 Docker 镜像构建作为流水线的一个步骤。为了提高速度,通常会:

  • 利用 Docker layer caching:在复制 package.json 和运行 npm ci 之前,尽量不复制其他代码,这样当依赖不变时这一层可以被缓存,大大缩短构建时间。
  • 使用远程镜像仓库:构建后 push 到 Docker Hub 或私有 Registry(如阿里云容器镜像服务、Harbor),然后在生产服务器上 pull 运行。
  • 为镜像打合理标签:使用 commit hash 或版本号 + latest 双标签,便于回滚和追溯。

一个简易的构建脚本片段:

docker build -t my-app:${GIT_COMMIT} -t my-app:latest .
docker push my-app:${GIT_COMMIT}
docker push my-app:latest

生产环境只需 docker pull my-app:latest && docker-compose up -d 即可更新。

19.2.6 安全性与最佳实践清单

容器化部署虽然方便,但如果忽视安全,可能引入新的风险。下面是一些经过验证的实践:

  • 不以 root 用户运行应用:在 Dockerfile 末尾添加 USER node(官方 Node 镜像提供 node 用户),降低容器逃逸后的权限。
  • 定期更新基础镜像:重新构建镜像时拉取最新的 Alpine/Node 安全补丁,或者使用 Dependabot 等工具监控。
  • 不在镜像中存储密钥:敏感信息通过环境变量或 Docker Secrets 注入,决不能写入镜像层。
  • 限制容器资源docker run 时添加 --memory="512m" --cpus="1.0" 或在 compose 中配置 resources 节点,防止单个容器吃光宿主机资源。
  • 使用非公开发布的自定义基础镜像:企业的默认镜像可以预先配置好内部证书、日志代理等,减少重复。

19.2.7 开发者本地环境与生产一致性的收益

容器化不仅简化了运维,对开发者也同样友好。新成员克隆项目后,只需要安装 Docker 和 docker-compose,然后执行 docker-compose up -d,就能获得一个包含数据库、缓存、Node.js 应用的完整开发环境,无需在自己电脑上安装 MySQL、Redis 或特定版本的 Node.js。对“在我机器上能跑”这个古老问题的终极解决方案,正是 Docker 带来的环境一致性。

将这样一个标准化的交付物部署到测试、预发、生产环境,也意味着你可以更有信心地自动化整个发布流程。结合 PM2 或容器编排平台的内置健康检查与自动重启,Node.js 服务就能获得可靠的基石。在下一节,我们将讨论如何将这些构建、测试、部署步骤串接为一个完整的 CI/CD 流水线。