引言
在微服务架构日益普及的今天,一个应用往往由多个独立服务构成(如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 文件中的
environment或env_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 简化你的多容器应用管理吧!