Docker 容器化部署实战:从零搭建一套完整的 Nginx + Node.js + MySQL + Redis 项目环境

很多开发者在本地跑得好好的项目,一部署到服务器就出各种问题------环境不一致、依赖版本冲突、配置文件散落各处。Docker 就是解决这些问题的利器,本文从零开始,手把手搭建一套包含 Nginx、Node.js、MySQL、Redis 的完整项目运行环境,所有服务一条命令启动,换任何机器都能一键复现。

项目结构

先把整体目录结构理清楚:

复制代码
project/
├── docker-compose.yml
├── nginx/
│   ├── nginx.conf
│   └── conf.d/
│       └── default.conf
├── node-app/
│   ├── Dockerfile
│   ├── package.json
│   ├── app.js
│   └── ...
├── mysql/
│   ├── Dockerfile
│   ├── my.cnf
│   └── init/
│       └── init.sql
└── redis/
    └── redis.conf

每个服务独立一个目录,配置和代码分离,结构清晰,方便维护。

第一步:编写 Node.js 应用

先写一个简单的 Express 应用,作为示例后端服务:

javascript 复制代码
// node-app/app.js
const express = require('express');
const mysql = require('mysql2/promise');
const Redis = require('ioredis');

const app = express();
app.use(express.json());

// MySQL 连接池
const db = mysql.createPool({
  host: process.env.DB_HOST || 'mysql',
  port: process.env.DB_PORT || 3306,
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || 'root123',
  database: process.env.DB_NAME || 'myapp',
});

// Redis 连接
const redis = new Redis({
  host: process.env.REDIS_HOST || 'redis',
  port: process.env.REDIS_PORT || 6379,
});

// 健康检查接口
app.get('/health', async (req, res) => {
  try {
    await db.query('SELECT 1');
    await redis.ping();
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
  } catch (err) {
    res.status(500).json({ status: 'error', message: err.message });
  }
});

// 示例接口:获取用户列表
app.get('/api/users', async (req, res) => {
  try {
    // 先查 Redis 缓存
    const cached = await redis.get('users:list');
    if (cached) {
      return res.json({ source: 'cache', data: JSON.parse(cached) });
    }
    // 缓存未命中,查数据库
    const [rows] = await db.query('SELECT id, name, email FROM users');
    await redis.set('users:list', JSON.stringify(rows), 'EX', 60);
    res.json({ source: 'db', data: rows });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
第二步:编写 Node.js 的 Dockerfile
dockerfile 复制代码
# node-app/Dockerfile

# 构建阶段:安装依赖
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# 运行阶段:只复制必要文件
FROM node:20-alpine
WORKDIR /app

# 创建非 root 用户,提升安全性
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

COPY --from=builder /app/node_modules ./node_modules
COPY . .

USER appuser

EXPOSE 3000

# 使用 dumb-init 处理信号转发,避免僵尸进程
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "app.js"]

几个关键点:

  • 多阶段构建:把依赖安装和运行环境分开,最终镜像更小。
  • 非 root 用户运行:容器内不要用 root 权限跑应用,降低安全风险。
  • dumb-init :容器内的 PID 1 进程需要正确处理系统信号(如 SIGTERM),否则 docker stop 时应用不能优雅退出。
第三步:配置 MySQL
sql 复制代码
-- mysql/init/init.sql
CREATE DATABASE IF NOT EXISTS myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE myapp;

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(200) NOT NULL UNIQUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 插入测试数据
INSERT INTO users (name, email) VALUES
  ('张三', 'zhangsan@example.com'),
  ('李四', 'lisi@example.com'),
  ('王五', 'wangwu@example.com');

MySQL 的自定义配置:

ini 复制代码
# mysql/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
default-time-zone='+08:00'
max_connections=200
innodb_buffer_pool_size=256M
slow_query_log=ON
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=1

[client]
default-character-set=utf8mb4
第四步:配置 Redis
conf 复制代码
# redis/redis.conf
# 设置密码
requirepass redis123

# 最大内存限制
maxmemory 256mb
maxmemory-policy allkeys-lru

# 持久化策略
save 900 1
save 300 10
save 60 10000

# 关闭危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
第五步:配置 Nginx 反向代理
nginx 复制代码
# nginx/conf.d/default.conf
upstream node_backend {
    server node-app:3000;
    # 如果有多个实例,可以加更多 server
    # server node-app:3001;
    # server node-app:3002;
}

server {
    listen 80;
    server_name localhost;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Gzip 压缩
    gzip on;
    gzip_types text/plain application/json application/javascript text/css;
    gzip_min_length 1024;

    # 反向代理到 Node.js
    location /api/ {
        proxy_pass http://node_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置
        proxy_connect_timeout 10s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }

    location /health {
        proxy_pass http://node_backend;
    }

    # 静态文件(如果有前端页面)
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}
第六步:编写 docker-compose.yml

这是整个方案的核心,把所有服务编排在一起:

yaml 复制代码
version: '3.8'

services:
  # MySQL 数据库
  mysql:
    image: mysql:8.0
    container_name: myapp-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: app123
      TZ: Asia/Shanghai
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./mysql/init:/docker-entrypoint-initdb.d
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    command: --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # Redis 缓存
  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
    command: redis-server /usr/local/etc/redis/redis.conf
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis123", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # Node.js 应用
  node-app:
    build:
      context: ./node-app
      dockerfile: Dockerfile
    container_name: myapp-node
    restart: unless-stopped
    environment:
      PORT: 3000
      DB_HOST: mysql
      DB_PORT: 3306
      DB_USER: appuser
      DB_PASSWORD: app123
      DB_NAME: myapp
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: redis123
      NODE_ENV: production
    ports:
      - "3000:3000"
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - backend
      - frontend

  # Nginx 反向代理
  nginx:
    image: nginx:alpine
    container_name: myapp-nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - node-app
    networks:
      - frontend

# 数据卷:持久化存储
volumes:
  mysql_data:
    driver: local
  redis_data:
    driver: local

# 网络隔离
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

几个设计要点:

  • depends_on + healthcheck:Node.js 应用会等 MySQL 和 Redis 健康检查通过后才启动,避免应用启动了但数据库还没就绪导致的连接错误。
  • 网络隔离 :Nginx 和 Node.js 在 frontend 网络通信,Node.js、MySQL、Redis 在 backend 网络通信。Nginx 无法直接访问数据库,提升了安全性。
  • 数据卷持久化:MySQL 和 Redis 的数据存储在 Docker Volume 中,容器删除重建后数据不会丢失。
第七步:一键启动

所有配置写好后,在项目根目录执行:

bash 复制代码
# 构建并启动所有服务
docker compose up -d --build

# 查看服务状态
docker compose ps

# 查看日志
docker compose logs -f node-app

第一次启动时,MySQL 会自动执行 init/init.sql 初始化数据库和表结构。等所有服务启动完成后,测试一下:

bash 复制代码
# 健康检查
curl http://localhost/health

# 获取用户列表
curl http://localhost/api/users
常用运维命令
bash 复制代码
# 停止所有服务(保留数据)
docker compose down

# 停止并删除数据卷(慎用,会清空数据库)
docker compose down -v

# 只重启某个服务
docker compose restart node-app

# 进入 MySQL 容器执行 SQL
docker exec -it myapp-mysql mysql -u root -proot123 myapp

# 进入 Redis 容器
docker exec -it myapp-redis redis-cli -a redis123

# 查看资源占用
docker stats
生产环境注意事项

以上配置适合开发和测试环境,上生产还需要做以下调整:

  • 不要在 docker-compose.yml 中明文写密码 ,改用 Docker Secrets 或环境变量文件(.env),并将 .env 加入 .gitignore
  • MySQL 和 Redis 不要暴露端口到宿主机 ,去掉 ports 映射,只通过内部网络通信。
  • Nginx 配置 HTTPS,可以用 Let's Encrypt + certbot 自动申请证书。
  • 日志收集:配置日志驱动,将容器日志输出到 ELK 或 Loki 等日志平台,方便排查问题。
  • 资源限制:给每个服务设置 CPU 和内存上限,防止某个服务异常时耗尽宿主机资源:
yaml 复制代码
services:
  node-app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

这套方案的核心价值在于环境一致性一键部署------新同事入职,clone 代码后一条命令就能跑起整个项目;服务器迁移,把代码和配置拷过去,同样一条命令就能恢复。再也不用对着 README 里一堆"先装这个再装那个"的步骤头疼了。

相关推荐
晴天161 小时前
npm install -f(--force)深度解析:作用原理、报错根源与风险避坑指南
前端·npm·node.js
还是大剑师兰特1 小时前
Swagger 在 Node.js 项目中的安装和完整使用方法
node.js
FungLeo7 小时前
成为全栈·Node 后端篇·全文搜索:从 LIKE 到全文索引
node.js·全文索引·模糊搜索·全文搜索·成为全栈·like 搜索
SendTomo11 小时前
【技术交流】从0到1构建一个汉字学习平台:汉字吧(hanzi8.com)的技术架构猜想
redis·mysql·nginx·php·个人开发
全栈攻略13 小时前
Docker 中 ROS2 工作流 Topic 验证与常用命令指南
运维·docker·容器
名字还没想好☜15 小时前
Docker 数据卷实战:volume、bind mount、tmpfs 到底怎么选,数据持久化与权限坑
运维·docker·容器·kubernetes
唐青枫16 小时前
Docker diff 详解:看清容器里到底改了什么
docker
lpfasd12317 小时前
Docker存储清理与防膨胀实践
运维·docker·容器
滨哥GPT18 小时前
Codex新增文件上传后为什么小文件正常,大文件总失败?Multipart、Body Limit与Nginx限制排查
nginx·ai编程·文件上传·codex·multipart