Node.js Docker 容器化

核心知识点

Docker 容器化概述

Docker 是一个开源的容器化平台,允许开发者将应用及其依赖打包到一个轻量级、可移植的容器中,然后部署到任何支持 Docker 的环境中。容器化可以确保应用在不同环境中运行的一致性,简化部署流程,提高开发和运维效率。

Docker 容器化的主要优势:

  • 环境一致性:消除开发、测试和生产环境的差异
  • 轻量级:容器共享宿主机内核,启动速度快,资源占用少
  • 可移植性:容器可以在任何支持 Docker 的环境中运行
  • 隔离性:容器之间相互隔离,提高安全性
  • 可扩展性:易于水平扩展应用
  • 版本控制:容器镜像支持版本管理

Docker 核心概念

  1. 镜像(Image)

    • 应用及其依赖的打包文件
    • 只读模板,用于创建容器
    • 可以从 Docker Hub 拉取或本地构建
  2. 容器(Container)

    • 镜像的运行实例
    • 可读写的层,在镜像之上
    • 包含应用运行所需的完整环境
  3. Dockerfile

    • 定义如何构建 Docker 镜像的文本文件
    • 包含一系列构建指令
    • 用于自动化镜像构建
  4. Docker Compose

    • 用于定义和运行多容器 Docker 应用的工具
    • 使用 YAML 文件定义服务、网络和卷
    • 简化多容器应用的管理
  5. Docker Hub/Registry

    • 存储和分享 Docker 镜像的仓库
    • Docker Hub 是公共的镜像仓库
    • 也可以搭建私有仓库

Docker 容器化流程

  1. 编写 Dockerfile:定义镜像构建步骤
  2. 构建镜像:使用 docker build 命令构建镜像
  3. 运行容器:使用 docker run 命令运行容器
  4. 管理容器:使用 Docker 命令管理容器的生命周期
  5. 编排容器:使用 Docker Compose 或 Kubernetes 编排多容器应用

实用案例分析

案例 1:基本的 Node.js 应用容器化

问题:需要将简单的 Node.js 应用容器化

解决方案:编写 Dockerfile,构建镜像并运行容器。

项目结构

├── app.js          # Node.js 应用
├── package.json    # 依赖配置
└── Dockerfile      # Docker 构建文件

app.js

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

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

app.listen(port, () => {
  console.log(`应用运行在 http://localhost:${port}`);
});

package.json

{
  "name": "docker-node-app",
  "version": "1.0.0",
  "description": "Node.js app with Docker",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

Dockerfile

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

# 设置工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json(如果存在)
COPY package*.json ./

# 安装依赖
RUN npm install --production

# 复制应用代码
COPY . .

# 暴露应用端口
EXPOSE 3000

# 启动应用
CMD ["npm", "start"]

构建和运行

# 构建镜像
docker build -t node-docker-app .

# 查看构建的镜像
docker images

# 运行容器
docker run -p 3000:3000 --name node-app node-docker-app

# 查看运行中的容器
docker ps

# 停止容器
docker stop node-app

# 删除容器
docker rm node-app

案例 2:使用 Docker Compose 管理多容器应用

问题:需要管理包含 Node.js 应用和数据库的多容器应用

解决方案:使用 Docker Compose 定义和运行多容器应用。

项目结构

├── app.js          # Node.js 应用
├── package.json    # 依赖配置
├── Dockerfile      # Docker 构建文件
└── docker-compose.yml  # Docker Compose 配置

app.js

const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = process.env.PORT || 3000;
const mongoUrl = process.env.MONGO_URL || 'mongodb://mongo:27017/node-docker';

// 连接 MongoDB
mongoose.connect(mongoUrl)
  .then(() => console.log('MongoDB 连接成功'))
  .catch(err => console.error('MongoDB 连接失败:', err));

// 定义用户模型
const User = mongoose.model('User', {
  name: String,
  email: String
});

// 路由
app.get('/', async (req, res) => {
  try {
    // 创建测试用户
    const user = new User({ name: '测试用户', email: 'test@example.com' });
    await user.save();
    
    // 获取所有用户
    const users = await User.find();
    
    res.send(`
      <h1>Hello Docker Compose!</h1>
      <p>MongoDB 连接成功</p>
      <p>用户数量: ${users.length}</p>
      <ul>
        ${users.map(u => `<li>${u.name} - ${u.email}</li>`).join('')}
      </ul>
    `);
  } catch (err) {
    res.send(`错误: ${err.message}`);
  }
});

app.listen(port, () => {
  console.log(`应用运行在 http://localhost:${port}`);
});

package.json

{
  "name": "docker-node-mongo-app",
  "version": "1.0.0",
  "description": "Node.js app with MongoDB using Docker Compose",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^6.10.0"
  }
}

Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

docker-compose.yml

version: '3'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - PORT=3000
      - MONGO_URL=mongodb://mongo:27017/node-docker
    depends_on:
      - mongo
    restart: always

  mongo:
    image: mongo:4.4
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    restart: always

volumes:
  mongo-data:

构建和运行

# 构建和启动服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看应用日志
docker-compose logs app

# 查看数据库日志
docker-compose logs mongo

# 停止服务
docker-compose down

# 停止服务并删除卷
docker-compose down -v

# 重启服务
docker-compose restart

案例 3:优化 Node.js Docker 镜像

问题:需要构建更小、更安全的 Node.js Docker 镜像

解决方案:使用多阶段构建、Alpine 基础镜像和最佳实践优化镜像。

优化的 Dockerfile

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

WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装所有依赖(包括开发依赖)
RUN npm install

# 复制应用代码
COPY . .

# 运行测试(如果有)
# RUN npm test

# 构建应用(如果需要)
# RUN npm run build

# 第二阶段:生产阶段
FROM node:18-alpine AS production

# 设置环境变量
ENV NODE_ENV=production
WORKDIR /app

# 从构建阶段复制 package.json 和 node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

# 只复制必要的文件
COPY --from=builder /app/app.js ./
# 如果有构建产物,复制构建产物
# COPY --from=builder /app/dist ./dist

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

# 切换到非 root 用户
USER nodejs

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["npm", "start"]

优化措施说明

  1. 多阶段构建

    • 第一阶段(builder):安装所有依赖,运行测试,构建应用
    • 第二阶段(production):只复制必要的文件,减小最终镜像大小
  2. Alpine 基础镜像

    • 使用 node:18-alpine 作为基础镜像,体积更小
    • Alpine 是一个轻量级的 Linux 发行版
  3. 非 root 用户

    • 创建并使用非 root 用户运行应用
    • 提高容器的安全性
  4. 只复制必要的文件

    • 避免复制不必要的文件到最终镜像
    • 减小镜像大小,提高构建速度
  5. 环境变量

    • 设置 NODE_ENV=production,优化 Node.js 运行环境

案例 4:使用 Docker 多阶段构建优化前端应用

问题:需要容器化包含前端构建的 Node.js 应用

解决方案:使用多阶段构建,在一个阶段构建前端,在另一个阶段运行后端。

项目结构

├── backend/        # 后端代码
│   ├── app.js
│   └── package.json
├── frontend/       # 前端代码
│   ├── src/
│   ├── package.json
│   └── webpack.config.js
└── Dockerfile      # Docker 构建文件

Dockerfile

# 第一阶段:构建前端
FROM node:18-alpine AS frontend-builder

WORKDIR /app/frontend

COPY frontend/package*.json ./

RUN npm install

COPY frontend/ ./

RUN npm run build

# 第二阶段:构建后端
FROM node:18-alpine AS backend-builder

WORKDIR /app/backend

COPY backend/package*.json ./

RUN npm install

COPY backend/ ./

# 第三阶段:生产阶段
FROM node:18-alpine AS production

WORKDIR /app

# 复制后端代码和依赖
COPY --from=backend-builder /app/backend ./

# 复制前端构建产物
COPY --from=frontend-builder /app/frontend/build ./public

# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000

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

USER nodejs

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["node", "app.js"]

后端 app.js

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

// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));

// API 路由
app.get('/api', (req, res) => {
  res.json({ message: 'Hello from API' });
});

// 前端路由(SPA 应用)
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.listen(port, () => {
  console.log(`应用运行在 http://localhost:${port}`);
});

案例 5:使用 Docker 卷实现数据持久化

问题:需要在容器重启后保留应用数据

解决方案:使用 Docker 卷实现数据持久化。

Docker Compose 配置

version: '3'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./app:/app          # 开发时使用,实时同步代码
      - node_modules:/app/node_modules  # 持久化依赖
    environment:
      - PORT=3000
    restart: always

volumes:
  node_modules:

使用卷的优势

  1. 数据持久化:容器重启后数据不会丢失
  2. 性能优化:卷的 I/O 性能通常比容器的可写层更好
  3. 代码同步:开发时可以使用绑定挂载实时同步代码
  4. 共享数据:多个容器可以共享同一个卷

Docker 容器化最佳实践

1. 镜像优化

  • 使用官方基础镜像:确保镜像的安全性和可靠性
  • 使用 Alpine 基础镜像:减小镜像大小
  • 多阶段构建:分离构建环境和运行环境
  • 最小化镜像层数:合并 RUN 指令,减少镜像层数
  • 清理构建缓存:在构建过程中清理不必要的文件和缓存
  • 使用 .dockerignore 文件:避免复制不必要的文件到镜像

2. 容器安全

  • 使用非 root 用户:避免以 root 用户运行容器
  • 最小化容器权限:只授予容器必要的权限
  • 定期更新镜像:及时修复安全漏洞
  • 扫描镜像漏洞:使用工具如 Trivy 扫描镜像漏洞
  • 限制容器资源:设置 CPU 和内存限制
  • 避免在容器中存储敏感信息:使用环境变量或 secrets 管理敏感信息

3. 容器配置

  • 使用环境变量:管理应用配置,避免硬编码
  • 设置健康检查:确保容器的健康状态
  • 配置日志:使用 Docker 日志驱动,集中管理日志
  • 设置合理的重启策略:确保容器在失败后能够自动重启
  • 使用标签:为镜像和容器添加标签,便于管理

4. 开发和部署

  • 开发环境

    • 使用绑定挂载同步代码
    • 启用热重载
    • 配置开发特定的环境变量
  • 生产环境

    • 使用固定版本的基础镜像
    • 禁用开发依赖
    • 启用生产模式
    • 配置适当的资源限制
  • CI/CD 集成

    • 在 CI/CD 管道中构建和测试镜像
    • 自动推送到镜像仓库
    • 自动部署到生产环境

5. 多容器应用管理

  • 使用 Docker Compose:管理开发和测试环境的多容器应用
  • 使用 Kubernetes:管理生产环境的多容器应用
  • 合理设计服务:将应用拆分为多个微服务
  • 配置网络:使用 Docker 网络实现服务间通信
  • 管理数据:使用 Docker 卷或外部存储服务管理数据

常见问题与解决方案

问题 1:容器启动后立即退出

症状

  • 容器启动后立即退出
  • 使用 docker ps 看不到运行中的容器
  • 使用 docker logs 查看日志,可能有错误信息

解决方案

  • 检查应用是否在前台运行
  • 确保 Dockerfile 中的 CMD 指令正确
  • 查看容器日志,定位错误原因
  • 确保应用启动时没有立即崩溃

问题 2:容器间通信失败

症状

  • 容器无法连接到其他容器
  • 服务间 API 调用失败
  • 数据库连接超时

解决方案

  • 确保容器在同一个网络中
  • 使用服务名称作为主机名进行通信
  • 检查防火墙设置
  • 确保目标服务正在运行且监听正确的端口

问题 3:镜像构建速度慢

症状

  • docker build 命令执行时间长
  • 每次构建都需要重新安装依赖
  • 构建过程中下载大文件

解决方案

  • 使用 Docker 构建缓存:合理安排 Dockerfile 指令顺序
  • 只复制必要的文件:使用 .dockerignore 文件
  • 多阶段构建:分离构建环境和运行环境
  • 使用本地镜像仓库:减少下载时间

问题 4:容器内存使用过高

症状

  • 容器内存使用超过限制
  • 容器被 OOM Killer 杀死
  • 应用响应缓慢

解决方案

  • 优化应用代码,减少内存使用
  • 设置合理的内存限制:使用 --memory 参数
  • 监控容器内存使用:使用 Docker 监控工具
  • 考虑使用更轻量级的基础镜像

问题 5:数据持久化问题

症状

  • 容器重启后数据丢失
  • 卷挂载失败
  • 数据权限问题

解决方案

  • 使用 Docker 卷或绑定挂载持久化数据
  • 确保卷的权限正确
  • 备份重要数据:定期备份卷中的数据
  • 考虑使用外部存储服务:如 AWS S3、MongoDB Atlas 等

Docker 工具和命令

常用 Docker 命令

# 镜像相关
docker build -t <image-name> .    # 构建镜像
docker pull <image-name>           # 拉取镜像
docker push <image-name>           # 推送镜像
docker images                      # 列出镜像
docker rmi <image-name>            # 删除镜像

# 容器相关
docker run -d -p 3000:3000 <image-name>  # 运行容器
docker ps                          # 列出运行中的容器
docker ps -a                       # 列出所有容器
docker stop <container-id>         # 停止容器
docker start <container-id>        # 启动容器
docker restart <container-id>      # 重启容器
docker rm <container-id>           # 删除容器
docker logs <container-id>         # 查看容器日志
docker exec -it <container-id> bash  # 进入容器

# 网络相关
docker network ls                  # 列出网络
docker network create <network-name>  # 创建网络
docker network connect <network-name> <container-id>  # 连接容器到网络

# 卷相关
docker volume ls                   # 列出卷
docker volume create <volume-name>  # 创建卷
docker volume inspect <volume-name>  # 查看卷信息

Docker Compose 命令

# 基本命令
docker-compose up -d               # 启动服务(后台运行)
docker-compose down                # 停止并删除服务
docker-compose ps                  # 查看服务状态
docker-compose logs                # 查看服务日志
docker-compose build               # 构建服务镜像
docker-compose restart             # 重启服务

# 其他命令
docker-compose exec <service> bash  # 进入服务容器
docker-compose pull                # 拉取服务镜像
docker-compose push                # 推送服务镜像
docker-compose config              # 验证配置文件

总结

Docker 容器化是现代应用开发和部署的重要技术,它可以确保应用在不同环境中运行的一致性,简化部署流程,提高开发和运维效率。通过本文的学习,你应该:

  1. 理解 Docker 容器化的核心概念和优势
  2. 掌握 Dockerfile 的编写和镜像构建
  3. 学会使用 Docker Compose 管理多容器应用
  4. 了解 Docker 容器化的最佳实践和常见问题解决方案
  5. 能够将 Node.js 应用成功容器化并部署

Docker 容器化不仅是一种技术,更是一种思维方式。通过容器化,你可以实现开发、测试和生产环境的标准化,提高团队协作效率,加速应用的交付和部署。随着容器技术的不断发展,Docker 已经成为现代应用开发和部署的标准工具之一。

记住,容器化是一个持续优化的过程。通过不断学习和实践,你将能够构建更加高效、安全、可靠的容器化应用。

« 上一篇 Node.js 部署基础 下一篇 » Node.js CI/CD 流程