一、为什么要把Node.js装进Docker?

咱们先来聊聊为什么要做这个组合。想象一下,你开发了一个特别棒的Node.js微服务,在自己电脑上跑得飞起。但当你把它交给运维同事部署到服务器时,突然各种报错:系统环境不一致、Node版本不对、依赖包缺失...这时候Docker就像个万能搬家箱,把整个运行环境打包带走。

Node.js的轻量特性和Docker的容器化简直是天作之合。Node应用本身就不需要太多系统资源,而Docker容器又比虚拟机轻得多。比如我们有个电商系统的商品服务,用Express框架写的,下面是个最简单的例子:

// 技术栈:Node.js + Express
const express = require('express');
const app = express();

app.get('/products', (req, res) => {
    res.json([{id: 1, name: 'Docker入门指南'}]);
});

// 注意这里的环境变量读取方式
const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`服务已在端口 ${port} 启动`);
});

这个小服务如果直接部署,可能会遇到端口冲突、Node版本要求等问题。但装进Docker后,这些烦恼就都不存在了。

二、从零开始构建你的第一个Docker化Node应用

让我们动手把上面的小服务装进Docker。首先要在项目根目录创建两个关键文件:

  1. Dockerfile - 这是Docker的构建说明书
  2. .dockerignore - 告诉Docker哪些文件不用打包
# 技术栈:Docker
# 使用官方Node镜像作为基础
FROM node:18-alpine

# 设置工作目录
WORKDIR /usr/src/app

# 先拷贝package.json(利用Docker缓存层)
COPY package*.json ./

# 安装依赖
RUN npm install

# 拷贝所有源代码
COPY . .

# 暴露端口(和代码中的保持一致)
EXPOSE 3000

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

.dockerignore文件内容:

node_modules
npm-debug.log
.DS_Store
.git

构建并运行的命令也很简单:

docker build -t product-service .
docker run -p 3000:3000 -d product-service

现在访问localhost:3000/products就能看到我们的商品数据了。这个例子虽然简单,但已经包含了最核心的要素。

三、进阶技巧:多容器协作与优化

真实的微服务架构往往需要多个容器协同工作。比如我们的商品服务可能需要连接MongoDB数据库。这时候docker-compose就派上用场了。

# 技术栈:Docker Compose
version: '3'
services:
  product-service:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=mongo
    depends_on:
      - mongo
    
  mongo:
    image: mongo:5.0
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

对应的Node.js代码需要稍作修改:

// 技术栈:Node.js + Mongoose
const mongoose = require('mongoose');

// 连接Docker Compose中定义的mongo服务
mongoose.connect(`mongodb://${process.env.DB_HOST}:27017/products`, {
    useNewUrlParser: true
});

const Product = mongoose.model('Product', new mongoose.Schema({
    name: String
}));

// 插入测试数据
async function init() {
    await Product.create([{name: 'Docker入门指南'}]);
}
init();

这样我们就实现了一个完整的微服务架构,包含应用服务和数据库服务。启动整个系统只需要一个命令:

docker-compose up -d

四、生产环境必备的实战经验

在实际生产环境中,我们还需要考虑更多因素。下面分享几个关键点:

  1. 日志处理:容器内的日志需要妥善处理
# 在Dockerfile中添加日志配置
RUN npm install winston
// 使用winston记录日志
const winston = require('winston');
const logger = winston.createLogger({
    transports: [
        new winston.transports.Console(),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});
  1. 健康检查:确保服务正常运行
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/health || exit 1
  1. 多阶段构建:减小镜像体积
# 构建阶段
FROM node:18 as builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

# 生产阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
  1. 环境变量管理:敏感信息处理
# 通过.env文件管理环境变量
docker run --env-file .env -p 3000:3000 product-service

五、常见问题与解决方案

在实际使用中,可能会遇到这些问题:

  1. 容器时区不对
# 在Dockerfile中设置时区
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
  1. 热重载失效
# 开发环境可以使用卷挂载
docker run -v $(pwd):/usr/src/app -p 3000:3000 product-service
  1. 内存限制
# 限制容器内存使用
docker run -m "512m" --memory-swap "1g" product-service
  1. 跨容器网络通信
# 在docker-compose中使用自定义网络
networks:
  app-network:
    driver: bridge

六、总结与最佳实践

经过上面的探索,我们可以得出一些最佳实践:

  1. 始终使用.dockerignore文件,避免把不必要的文件打包进镜像
  2. 多阶段构建可以显著减小镜像体积
  3. 生产环境一定要设置资源限制
  4. 使用docker-compose管理多服务架构
  5. 日志要输出到标准输出,方便Docker收集

这种架构特别适合以下场景:

  • 需要快速扩展的互联网应用
  • 混合多种技术的微服务系统
  • 需要频繁部署更新的项目
  • 开发环境与生产环境需要高度一致的场景

当然也有需要注意的地方:

  • 不要把所有东西都塞进一个容器
  • 容器不是虚拟机,要遵循单一职责原则
  • 数据持久化要使用卷(volume)
  • 镜像安全扫描不能忽视