Docker Compose多服务编排实战:从零搭建Node.js+MySQL+Redis全栈应用

引言

在微服务架构日益普及的今天,一个应用往往由多个独立服务构成(如Web服务器、数据库、缓存、消息队列等)。如果每次都要手动敲 docker run 启动这些容器,并管理它们之间的网络、依赖顺序和数据卷,那将是一场灾难。Docker Compose 正是为解决这一痛点而生------它允许你通过一个 YAML 文件定义所有服务,然后一条命令启动整个应用栈。本文将带你从核心概念入手,最终完成一个生产可用的 Node.js + MySQL + Redis 多服务应用,并分享常见的问题与最佳实践。

一、Docker Compose 核心概念

Docker Compose 围绕三个核心抽象展开:

1. 服务 (Service)

一个服务对应一个容器,它定义了该容器如何构建(镜像或Dockerfile)、运行时的配置(端口、环境变量、资源限制等)以及与其它服务的关系。

2. 网络 (Network)

Compose 默认会创建一个 项目名_default 的自定义网络,所有服务自动加入该网络,并可以通过服务名相互访问。你也可以显式定义更复杂的网络拓扑。

3. 数据卷 (Volume)

用于持久化数据,可以命名卷或绑定挂载主机目录。Compose 能自动创建并管理命名卷,保证数据在容器重启甚至删除后依然存在。

此外,还有几个关键机制:

  • 依赖控制 (depends_on) :控制服务启动顺序(默认只保证启动先后,不等待服务就绪,需配合健康检查)。

  • 环境变量 :支持 .env 文件、environment 字段或直接引用宿主机变量。

  • 构建上下文:若服务通过构建 Dockerfile 生成,可指定上下文目录和 Dockerfile 名称。

理解这些概念后,我们用一个完整的实战项目来落地。

二、实战:Node.js + MySQL + Redis 全栈应用

2.1 项目结构

复制代码
myapp/
├── app/                     # Node.js 应用代码
│   ├── package.json
│   ├── server.js
│   └── Dockerfile
├── docker-compose.yml
└── .env                     # 环境变量(可选)

2.2 Node.js 应用 (app/server.js)

一个简单的 Express 服务,连接 MySQL 和 Redis,提供查询接口。

javascript 复制代码
const express = require('express');
const mysql = require('mysql2/promise');
const redis = require('redis');

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

// 使用环境变量配置连接信息
const dbConfig = {
  host: process.env.MYSQL_HOST || 'mysql',
  user: process.env.MYSQL_USER || 'root',
  password: process.env.MYSQL_PASSWORD || 'rootpass',
  database: process.env.MYSQL_DATABASE || 'mydb'
};

const redisClient = redis.createClient({
  url: `redis://${process.env.REDIS_HOST || 'redis'}:6379`
});

(async () => {
  await redisClient.connect();
  console.log('Connected to Redis');
})();

// 初始化数据库表(简单示例)
async function initDB() {
  const connection = await mysql.createConnection(dbConfig);
  await connection.execute(`CREATE TABLE IF NOT EXISTS visitors (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ip VARCHAR(45) NOT NULL,
    visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )`);
  await connection.end();
}

app.get('/', async (req, res) => {
  try {
    // 记录访问
    const connection = await mysql.createConnection(dbConfig);
    await connection.execute('INSERT INTO visitors (ip) VALUES (?)', [req.ip]);
    const [rows] = await connection.execute('SELECT COUNT(*) AS total FROM visitors');
    const total = rows[0].total;
    await connection.end();

    // 使用Redis缓存总访问量
    await redisClient.set('total_visitors', total, { EX: 60 }); // 60秒过期
    const cachedTotal = await redisClient.get('total_visitors');

    res.json({ message: 'Hello from Docker Compose!', totalVisitors: cachedTotal });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  initDB();
});

2.3 app/package.json

json 复制代码
{
  "name": "myapp",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mysql2": "^3.6.3",
    "redis": "^4.6.10"
  }
}

2.4 app/Dockerfile

dockerfile 复制代码
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

2.5 docker-compose.yml(核心编排文件)

完整的编排配置,包含健康检查、网络、卷和依赖。

yaml 复制代码
version: '3.8'

services:
  # Node.js Web 服务
  web:
    build:
      context: ./app        # Dockerfile 所在目录
      dockerfile: Dockerfile
    container_name: myapp_web
    ports:
      - "3000:3000"
    environment:
      - PORT=3000
      - MYSQL_HOST=mysql
      - MYSQL_USER=user
      - MYSQL_PASSWORD=pass
      - MYSQL_DATABASE=mydb
      - REDIS_HOST=redis
    depends_on:
      mysql:
        condition: service_healthy   # 等待 mysql 健康检查通过
      redis:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped

  # MySQL 数据库服务
  mysql:
    image: mysql:8.0
    container_name: myapp_mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mydb
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
    volumes:
      - mysql-data:/var/lib/mysql        # 持久化数据
      - ./mysql-init:/docker-entrypoint-initdb.d  # 初始化脚本(可选)
    networks:
      - app-network
    healthcheck:                         # 健康检查
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "user", "-ppass"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # Redis 缓存服务
  redis:
    image: redis:7-alpine
    container_name: myapp_redis
    volumes:
      - redis-data:/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    restart: unless-stopped

# 定义网络
networks:
  app-network:
    driver: bridge

# 定义命名卷
volumes:
  mysql-data:
  redis-data:

2.6 可选的 .env 文件

若不想在 docker-compose.yml 中硬编码密码,可使用 .env 文件:

复制代码
MYSQL_ROOT_PASSWORD=rootpass
MYSQL_USER=user
MYSQL_PASSWORD=pass
MYSQL_DATABASE=mydb

然后在 Compose 文件中用 ${MYSQL_ROOT_PASSWORD} 引用。

2.7 启动与测试

myapp/ 目录下执行:

bash 复制代码
docker-compose up -d

首次执行会构建 web 镜像并拉取 mysql、redis 镜像。启动后查看状态:

bash 复制代码
docker-compose ps

测试接口:

bash 复制代码
curl http://localhost:3000

输出类似:

json 复制代码
{"message":"Hello from Docker Compose!","totalVisitors":"1"}

数据库中的访问量会被 Redis 缓存,环境完美配合。

三、常见问题与注意事项

1. depends_on 不保证服务就绪

默认的 depends_on 只保证容器启动顺序,不等待数据库真正可接受连接。因此我们使用了 condition: service_healthy,这要求服务定义了健康检查。对于不支持健康检查的镜像,可在应用启动脚本中加入等待逻辑(如 wait-for-it.sh)。

2. 环境变量覆盖

Compose 会按以下优先级加载环境变量:

  • Compose 文件中的 environmentenv_file

  • 宿主机 Shell 中已 export 的变量

  • .env 文件

建议敏感信息使用 .env 并加入 .gitignore,生产环境使用 Docker Secrets 或外部密钥管理。

3. 端口冲突与网络模式

如果多个服务需要暴露相同端口,修改宿主机端口映射即可,如 "8080:3000"。网络默认使用自定义桥接网络,避免了容器端口冲突问题,因为容器间直接用服务名通信。

4. 构建缓存与镜像大小

通过分层构建优化 Dockerfile(先拷贝 package.json 安装依赖,再拷贝源代码)。生产环境应使用多阶段构建,将最终镜像体积降到最小。

5. 数据持久化

命名卷(如 mysql-data)存储在 Docker 管理的路径下(/var/lib/docker/volumes/),即使删除容器数据仍保留。绑定挂载适合开发阶段的热重载。注意数据库升级时卷的兼容性。

6. 资源限制与日志

可在服务中加入 deploy.resources 限制 CPU/内存。日志驱动建议设置为 json-file 并限制大小,避免磁盘被日志撑满。

yaml 复制代码
web:
  deploy:
    resources:
      limits:
        cpus: '0.5'
        memory: 256M
  logging:
    driver: "json-file"
    options:
      max-size: "10m"
      max-file: "3"

7. 生产环境调优

  • 使用单独的 docker-compose.override.yml 针对不同环境(开发/生产)做配置。
  • 移除不必要的端口暴露(生产环境中数据库不要映射到宿主机)。
  • 配置 restart: unless-stopped 保证服务自愈。
  • 结合 Docker Swarm 或 Kubernetes 进行大规模编排。

结语

通过本文,你不仅掌握了 Docker Compose 的核心概念,还获得了一个可直接运行的 Node.js + MySQL + Redis 多服务模板。从网络、依赖、健康检查到数据卷,这些正是生产级容器编排的基础。当你熟练之后,可以进一步扩展 Nginx 反向代理、消息队列等服务,构建更复杂的微服务架构。推荐将 Compose 文件纳入版本控制,实现"基础设施即代码"的 DevOps 实践。

开始用 Docker Compose 简化你的多容器应用管理吧!

相关推荐
木雷坞1 小时前
Firecrawl Docker Compose 自托管排查:镜像、Redis、队列和 Playwright
redis·docker·容器·firecrawl
就改了1 小时前
微服务异步场景链路断裂完整解决方案
微服务·云原生·架构
whyfail2 小时前
Colima:把 Docker Desktop 从 Mac 上“瘦身”的那把刀
macos·docker·容器
山东点狮信息科技有限公司3 小时前
点狮OA-企业级 OA 办公自动化系统架构设计与实践
spring cloud·微服务·性能优化·架构·系统架构
大佐不会说日语~3 小时前
在 Windows 本地用 Docker 部署向量模型(bge-m3)
windows·docker·容器·llm·ollama
热爱运维的小七4 小时前
深度解析|应用性能 + RUM + 拨测:现代 IT 运维的可观测性“铁三角”
运维·it运维·devops·apm·rum·网站拨测
逻极4 小时前
Spring Boot 微服务开发提速:我们如何将接口响应时间降低60%
java·spring boot·微服务·性能优化·自动配置
xsc-xyc4 小时前
CasaOS + Docker 挂载外接硬盘部署 Jellyfin 私人影院
运维·docker·容器
码云骑士4 小时前
27-Docker部署Django(上)-从2GB到180MB的镜像瘦身实战
docker·容器·django