Docker 化 Node.js 应用

Docker 是一个开源的应用容器引擎,它允许开发者将应用及其依赖打包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上。容器化使得应用的环境一致性、部署和扩展变得前所未有的简单。本章将带领你一步步将 Node.js 应用 Docker 化,并介绍生产环境的最佳实践。

1. 为什么需要 Docker?

  • 环境一致性:消除“在我的机器上可以运行”的问题,开发、测试、生产环境完全一致。
  • 快速部署:容器启动秒级,镜像构建后可快速分发。
  • 资源隔离:容器之间相互隔离,安全且易于管理。
  • 微服务架构:每个服务可以独立容器化,便于扩展和更新。
  • 简化依赖:应用及其所有依赖(Node.js 版本、系统库)都打包在一起。

2. Docker 基础概念

  • 镜像(Image):一个只读模板,包含运行应用所需的文件系统和配置。
  • 容器(Container):镜像的运行实例,可被启动、停止、删除。
  • Dockerfile:用于定义如何构建镜像的文本文件。
  • 仓库(Repository):存放镜像的地方,如 Docker Hub。
  • 卷(Volume):用于持久化数据和共享数据。
  • 网络(Network):容器间通信的通道。

3. 编写 Dockerfile

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 . .

# 暴露应用端口(假设应用监听 3000 端口)
EXPOSE 3000

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

说明:

  • FROM 指定基础镜像,这里使用基于 Alpine Linux 的 Node.js 镜像,体积更小。
  • WORKDIR 设置容器内的工作目录,后续命令都在此目录执行。
  • COPY 复制文件。先复制 package*.json 可以充分利用 Docker 缓存:如果依赖文件没变,则跳过 npm ci
  • RUN npm ci --only=production 安装生产依赖。--only=production 只安装 dependencies,不安装 devDependencies。
  • 再次 COPY 复制其余源代码。
  • EXPOSE 声明容器内应用监听的端口,仅起文档作用。
  • CMD 指定容器启动时执行的命令。

4. .dockerignore 文件

.gitignore 类似,.dockerignore 用于排除不需要复制到镜像中的文件,可以减小镜像体积并提高构建速度。示例:

.git
node_modules
npm-debug.log
.env
Dockerfile
.dockerignore
.gitignore
README.md
.vscode
coverage
tests

5. 构建镜像与运行容器

在 Dockerfile 所在目录执行以下命令构建镜像:

docker build -t my-node-app .

运行容器:

docker run -p 3000:3000 -d --name my-app my-node-app

-p 3000:3000 将宿主机的 3000 端口映射到容器的 3000 端口。-d 后台运行。--name 指定容器名称。

6. 多阶段构建

多阶段构建可以进一步减小镜像体积。第一阶段用于构建和安装依赖(包括开发依赖),第二阶段只复制必要的文件。以下是一个使用多阶段构建的示例:

# 第一阶段:构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci  # 安装所有依赖(包括开发依赖)
COPY . .
RUN npm run build  # 如果有构建步骤(如 TypeScript 编译)

# 第二阶段:生产
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist  # 假设构建输出在 dist 目录
EXPOSE 3000
CMD ["node", "dist/server.js"]

最终镜像只包含运行所需的文件,避免了开发依赖和源代码泄露。

7. 使用 docker-compose 编排

对于多容器应用(如 Node.js + Redis + MongoDB),docker-compose 可以简化管理。创建 docker-compose.yml 文件:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_URL=mongodb://mongo:27017/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
    volumes:
      - ./uploads:/app/uploads  # 持久化上传文件
  mongo:
    image: mongo:6
    volumes:
      - mongo-data:/data/db
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
volumes:
  mongo-data:
  redis-data:

然后使用 docker-compose up -d 启动所有服务。Compose 会自动创建网络,使得服务可以通过服务名(如 mongo)相互访问。

8. 环境变量配置

在生产环境中,敏感信息不应硬编码在 Dockerfile 或代码中。有几种传递环境变量的方式:

  • 通过 docker run -e "KEY=VALUE" 直接传递。
  • 使用 --env-file 指定环境变量文件。
  • 在 docker-compose 中通过 environmentenv_file 指定。
  • 在容器编排平台(如 Kubernetes)中通过 ConfigMap 或 Secret 管理。

示例:使用 .env 文件(确保该文件在 .dockerignore 中,避免包含在镜像内)

docker run -p 3000:3000 --env-file .env my-node-app

9. 数据持久化

容器是无状态的,当容器删除时,内部数据也会丢失。使用卷(volume)可以持久化数据。在 docker run 中可以通过 -v 参数指定:

docker run -v /host/path:/container/path ...

在 docker-compose 中可以使用顶级 volumes 定义命名卷(如上面的 mongo-data),然后挂载到服务。

10. 生产环境最佳实践

  • 使用非 root 用户:在 Dockerfile 中创建并使用普通用户运行应用,增强安全性。
  • 选择轻量基础镜像:如 node:18-alpine,避免不必要的系统库。
  • 多阶段构建:减少最终镜像大小,减少攻击面。
  • 依赖缓存优化:利用 Docker 层缓存,先复制 package.json 安装依赖,再复制源码。
  • 避免以 root 运行:创建 app 用户并切换:
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
  • 设置环境变量 NODE_ENV=production:使 Node.js 以生产模式运行,优化性能并禁用开发时警告。
  • 健康检查:在 Dockerfile 中添加 HEALTHCHECK 指令,让容器引擎可以监控应用状态。
  • 使用 .dockerignore 避免复制不必要的文件
  • 标签管理:为镜像打上版本标签,便于回滚。

11. 完整示例:Express 应用 Docker 化

假设我们有一个简单的 Express 应用,结构如下:

my-express-app/
├── package.json
├── server.js
└── .dockerignore

server.js

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello from Dockerized Express!');
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

CMD ["node", "server.js"]

.dockerignore

node_modules
npm-debug.log
.git
.env
Dockerfile
.dockerignore

构建并运行:

docker build -t express-app .
docker run -p 3000:3000 -d --name my-express express-app

访问 http://localhost:3000 即可看到结果。

12. 部署到云容器服务

构建好的镜像可以推送到容器镜像仓库(如 Docker Hub、阿里云容器镜像服务),然后通过云平台的容器服务(如 AWS ECS、阿里云 ECI、Google Cloud Run)部署。这些平台会自动拉取镜像并运行容器,并提供负载均衡、自动扩缩容等功能。

示例:推送到 Docker Hub

docker tag express-app yourusername/express-app:latest
docker push yourusername/express-app:latest

13. 常见问题

  • 容器启动后立即退出:检查应用是否在后台运行,或者是否有错误。使用 docker logs <container> 查看日志。
  • 端口映射不生效:确认 Dockerfile 中 EXPOSE 与实际监听端口一致,且宿主机端口未被占用。
  • 文件修改不生效:开发时可以使用卷挂载源码,实现热更新:docker run -v $(pwd):/app -p 3000:3000 ...
  • 镜像过大:检查是否包含了不必要的文件,使用 docker history 分析每层大小。
总结: Docker 化 Node.js 应用可以带来环境一致、部署简单、易于扩展等诸多好处。通过精心编写的 Dockerfile、利用多阶段构建、遵循安全最佳实践,你可以构建出轻量、安全的生产级镜像。结合 docker-compose 或容器编排平台,可以轻松管理复杂的微服务架构。