Docker Compose 实战:使用 Compose 部署 MySQL、Redis 与 Web 应用
一、什么是 Docker Compose?
在上一篇 Docker 入门教程中,我们学习了使用 docker run 创建和管理容器。
如果一个项目只需要运行一个 Nginx 容器,使用 docker run 就足够了。
但实际 Web 项目通常需要多个服务共同运行,例如:
-
MySQL:负责存储业务数据。
-
Redis:负责缓存、会话和临时数据。
-
FastAPI:负责处理业务请求。
-
Nginx:负责反向代理和静态资源访问。
如果继续使用多个 docker run 命令管理这些服务,就需要分别处理容器名称、端口、网络、数据卷和环境变量,维护起来比较麻烦。
Docker Compose 就是为了解决这个问题而设计的工具。
它允许开发者通过一个 YAML 配置文件定义多个服务,然后使用一条命令启动或停止整个项目。
1. Docker Compose 的核心概念
| 概念 | 说明 |
|---|---|
| Compose | 多容器应用编排工具 |
| services | 定义项目中的服务 |
| volumes | 定义持久化数据卷 |
| networks | 定义容器之间的网络 |
| environment | 设置容器环境变量 |
| depends_on | 定义服务启动依赖 |
| healthcheck | 检查容器服务健康状态 |
2. Docker Compose 的工作原理
例如,一个 Web 项目需要三个容器:
外部访问
|
v
Web 应用容器
|
+-----+-----+
| |
v v
MySQL 容器 Redis 容器
| |
v v
MySQL 数据卷 Redis 数据卷
Compose 会根据配置文件创建网络、数据卷和容器,并负责管理这些服务。
二、安装 Docker Compose
如果已经按照上一篇教程安装了 Docker 官方仓库中的 Compose 插件,通常不需要额外安装。
1. 检查 Docker Compose 版本
docker compose version
如果正常安装,会显示类似:
Docker Compose version v2.x.x
注意,现在推荐使用:
docker compose
而不是旧版本的:
docker-compose
两者属于不同的命令形式。新版 Docker Compose 已经作为 Docker CLI 插件提供。
2. 如果没有安装 Compose 插件
Debian 系统可以执行:
sudo apt update
sudo apt install -y docker-compose-plugin
再次检查:
docker compose version
三、创建项目目录
下面通过一个实际项目演示如何部署 MySQL、Redis 和 Python Web 应用。
项目结构:
compose-demo/
├── compose.yaml
├── .env
├── .gitignore
└── app/
├── Dockerfile
├── requirements.txt
└── main.py
创建项目目录:
mkdir -p ~/compose-demo/app
cd ~/compose-demo
本教程使用 FastAPI 编写一个简单的 Web 服务。
它将实现:
-
查询 MySQL 数据库。
-
向 MySQL 写入数据。
-
使用 Redis 保存缓存。
-
查看数据库和 Redis 的连接状态。
四、编写 FastAPI Web 应用
1. 创建 requirements.txt
进入应用目录:
cd ~/compose-demo/app
创建依赖文件:
nano requirements.txt
写入:
fastapi
uvicorn[standard]
sqlalchemy
pymysql
redis
这些依赖的作用:
| 依赖 | 作用 |
|---|---|
| fastapi | Web API 框架 |
| uvicorn | ASGI 服务器 |
| sqlalchemy | Python ORM |
| pymysql | MySQL 驱动 |
| redis | Redis Python 客户端 |
实际生产项目建议固定经过测试的依赖版本,并使用依赖锁定文件,避免每次构建安装到不同版本。
2. 创建 main.py
nano main.py
写入以下代码:
import os
from contextlib import asynccontextmanager
import redis
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
DATABASE_URL = os.environ["DATABASE_URL"]
REDIS_URL = os.environ["REDIS_URL"]
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
pool_size=5,
max_overflow=5,
)
redis_client = redis.Redis.from_url(
REDIS_URL,
decode_responses=True,
socket_connect_timeout=3,
socket_timeout=3,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
engine.dispose()
redis_client.close()
app = FastAPI(
title="Docker Compose Demo",
lifespan=lifespan,
)
class ItemCreate(BaseModel):
name: str
@app.get("/")
def index():
return {
"message": "Docker Compose Web Application",
"status": "running",
}
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/db")
def check_database():
try:
with engine.connect() as connection:
result = connection.execute(
text("SELECT VERSION()")
)
version = result.scalar()
return {
"mysql": "connected",
"version": version,
}
except SQLAlchemyError:
raise HTTPException(
status_code=503,
detail="MySQL connection failed",
)
@app.get("/redis")
def check_redis():
try:
redis_client.ping()
return {
"redis": "connected",
}
except redis.RedisError:
raise HTTPException(
status_code=503,
detail="Redis connection failed",
)
@app.post("/items")
def create_item(item: ItemCreate):
try:
with engine.begin() as connection:
connection.execute(
text("""
CREATE TABLE IF NOT EXISTS items (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
)
""")
)
result = connection.execute(
text(
"INSERT INTO items (name) VALUES (:name)"
),
{"name": item.name},
)
item_id = result.lastrowid
return {
"id": item_id,
"name": item.name,
}
except SQLAlchemyError:
raise HTTPException(
status_code=500,
detail="Failed to create item",
)
@app.get("/items")
def get_items():
try:
with engine.connect() as connection:
result = connection.execute(
text("SELECT id, name FROM items ORDER BY id DESC")
)
items = [
dict(row._mapping)
for row in result
]
return {"items": items}
except SQLAlchemyError:
raise HTTPException(
status_code=500,
detail="Failed to query items",
)
这里实现了几个接口:
| 接口 | 作用 |
|---|---|
| GET / | 查看应用是否运行 |
| GET /health | 简单健康检查 |
| GET /db | 测试 MySQL 连接 |
| GET /redis | 测试 Redis 连接 |
| POST /items | 写入一条数据 |
| GET /items | 查询数据列表 |
说明:
-
数据库连接地址通过环境变量读取。
-
Redis 连接地址通过环境变量读取。
-
使用 SQLAlchemy 管理 MySQL 连接。
-
pool_pre_ping=True可以在使用连接前检查连接是否有效。 -
数据表通过示例接口自动创建,仅适合教学演示;正式项目建议使用 Alembic 管理数据库迁移。
-
这里的同步数据库操作会阻塞当前工作线程,生产环境可以根据负载选择异步数据库驱动或合理配置线程池。
五、创建 Dockerfile
Dockerfile 用于构建 FastAPI 应用镜像。
在 app 目录创建:
nano Dockerfile
写入:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
RUN useradd --create-home appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
说明:
-
FROM:选择 Python 基础镜像。 -
WORKDIR:设置应用工作目录。 -
ENV:设置 Python 运行环境。 -
COPY:复制项目文件。 -
RUN:安装 Python 依赖并创建普通用户。 -
USER:使用普通用户运行应用。 -
EXPOSE:声明应用监听端口。 -
CMD:启动 FastAPI 服务。
这里没有使用 root 用户运行 Web 应用,可以减少部分安全风险。
六、创建环境变量文件
项目中有 MySQL 用户名、密码和数据库名称。
不建议将数据库密码直接写进 Compose 文件或应用代码。
可以通过 .env 文件统一管理。
回到项目根目录:
cd ~/compose-demo
nano .env
写入:
MYSQL_ROOT_PASSWORD=ChangeThisRootPassword_2026
MYSQL_DATABASE=app_db
MYSQL_USER=app_user
MYSQL_PASSWORD=ChangeThisAppPassword_2026
MYSQL_PORT=3306
REDIS_PORT=6379
WEB_PORT=8000
请将示例密码替换成自己生成的强随机密码。
创建 .gitignore:
nano .gitignore
写入:
.env
__pycache__/
*.pyc
.venv/
这样可以避免误将 .env 文件提交到 Git 仓库。
注意: Compose 的 .env 文件主要用于配置变量替换,并不意味着所有变量都会自动注入每个容器。需要在 compose.yaml 中明确配置 environment 或 env_file。
七、编写 compose.yaml
这是整个教程的核心部分。
回到项目根目录:
cd ~/compose-demo
nano compose.yaml
写入:
services:
mysql:
image: mysql:8.4
container_name: demo-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请设置MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-app_db}
MYSQL_USER: ${MYSQL_USER:-app_user}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?请设置MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
healthcheck:
test:
- CMD
- mysqladmin
- ping
- -h
- 127.0.0.1
- -uroot
- -p$${MYSQL_ROOT_PASSWORD}
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
redis:
image: redis:7.4
container_name: demo-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- app-network
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 10s
timeout: 3s
retries: 5
web:
build:
context: ./app
container_name: demo-web
restart: unless-stopped
environment:
DATABASE_URL: mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
REDIS_URL: redis://redis:6379/0
ports:
- "127.0.0.1:${WEB_PORT:-8000}:8000"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
volumes:
mysql-data:
redis-data:
networks:
app-network:
driver: bridge
1. 理解 services
services:
mysql:
redis:
web:
这里定义了三个服务:
-
mysql:MySQL 数据库。
-
redis:Redis 缓存。
-
web:FastAPI Web 应用。
Compose 会根据服务配置创建对应容器。
2. 理解 environment
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-app_db}
表示将环境变量传入 MySQL 容器。
${变量名:-默认值} 表示当变量未设置或为空时,使用默认值。
${变量名:?错误信息} 表示变量未设置或为空时,Compose 配置解析失败。
3. 理解 volumes
volumes:
- mysql-data:/var/lib/mysql
表示将 Docker 数据卷挂载到 MySQL 数据目录。
即使删除并重新创建 MySQL 容器,数据卷仍然可以保留数据库数据。
Redis 使用:
command: redis-server --appendonly yes
启用 Redis AOF 持久化,并将 /data 挂载到独立数据卷。
4. 理解 networks
networks:
- app-network
三个容器加入同一个自定义网络。
因此,FastAPI 可以直接通过服务名称访问:
mysql:3306
redis:6379
不需要填写容器 IP 地址。
5. 理解 depends_on
depends_on:
mysql:
condition: service_healthy
表示 Web 服务等待 MySQL 健康检查通过后再启动。
这里的 service_healthy 比简单的启动顺序依赖更可靠。
不过,健康检查通过不代表服务永远不会断开。正式应用仍然需要处理数据库连接失败和自动重连等情况。
6. 为什么端口绑定到 127.0.0.1?
ports:
- "127.0.0.1:${WEB_PORT:-8000}:8000"
这意味着 FastAPI 只通过宿主机本地地址提供访问。
可以让宿主机上的 Nginx 反向代理访问它,而不会直接向公网开放 8000 端口。
MySQL 和 Redis 没有配置 ports,因此不会发布到宿主机公网端口,只能通过 Docker 网络访问。
八、启动整个项目
1. 检查 Compose 配置
启动前先验证 YAML 配置:
docker compose config
如果配置正确,会显示解析后的完整配置。
2. 构建 Web 应用镜像
docker compose build
3. 启动所有服务
docker compose up -d
其中:
-
up:创建并启动服务。 -
-d:后台运行。
首次启动时,Docker 可能需要下载 MySQL、Redis 和 Python 基础镜像。
4. 查看服务状态
docker compose ps
正常情况下,应该看到:
NAME SERVICE STATUS
demo-mysql mysql Up (healthy)
demo-redis redis Up (healthy)
demo-web web Up
如果 Web 服务启动失败,可以查看日志:
docker compose logs web
查看所有服务日志:
docker compose logs
实时查看:
docker compose logs -f
九、测试 Web 应用
由于本教程将 FastAPI 绑定到宿主机的 127.0.0.1:8000,可以在服务器本机执行测试。
1. 测试首页
curl http://127.0.0.1:8000/
预期返回:
{
"message": "Docker Compose Web Application",
"status": "running"
}
2. 测试 MySQL
curl http://127.0.0.1:8000/db
预期返回类似:
{
"mysql": "connected",
"version": "8.4.x"
}
3. 测试 Redis
curl http://127.0.0.1:8000/redis
预期返回:
{
"redis": "connected"
}
4. 写入一条数据
curl -X POST http://127.0.0.1:8000/items \
-H "Content-Type: application/json" \
-d '{"name":"Docker Compose Test"}'
预期返回:
{
"id": 1,
"name": "Docker Compose Test"
}
5. 查询数据
curl http://127.0.0.1:8000/items
预期返回:
{
"items": [
{
"id": 1,
"name": "Docker Compose Test"
}
]
}
如果上述接口全部正常,说明 FastAPI 已经成功连接 MySQL 和 Redis。
十、使用 Docker Compose 管理服务
Docker Compose 的优势之一,就是可以统一管理整个项目。
1. 查看服务
docker compose ps
2. 停止服务
docker compose stop
停止容器,但保留容器和数据卷。
3. 启动服务
docker compose start
4. 重启服务
docker compose restart
5. 停止并删除容器和网络
docker compose down
注意:默认情况下,down 不会删除命名数据卷。
6. 停止服务并删除数据卷
docker compose down -v
危险提示: -v 会删除 Compose 项目声明的命名数据卷。本教程中的 MySQL 和 Redis 数据都保存在这些数据卷中,执行后可能造成数据库数据永久丢失。生产环境不要随意使用。
7. 修改代码后重新构建
docker compose up -d --build
该命令会重新构建需要构建的镜像,并重新创建发生变化的服务容器。
8. 只重启 Web 服务
docker compose restart web
9. 进入 MySQL 容器
docker compose exec mysql bash
进入 MySQL 命令行:
mysql -u root -p
输入 .env 中配置的 Root 密码。
10. 进入 Redis 容器
docker compose exec redis redis-cli
测试:
PING
正常返回:
PONG
十一、MySQL 数据备份与恢复
数据库容器虽然使用了数据卷,但数据卷不等于备份。
如果服务器磁盘损坏、误删数据卷或发生安全事故,仍然可能丢失全部数据库数据。
因此,生产环境应建立独立备份机制。
1. 创建 MySQL 备份目录
mkdir -p ~/compose-backups
2. 导出数据库
在项目目录中执行:
cd ~/compose-demo
执行:
docker compose exec -T mysql sh -c \
'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers "$MYSQL_DATABASE"' \
> ~/compose-backups/app_db.sql
该命令会将数据库导出到宿主机的备份目录。
--single-transaction 适用于 InnoDB 表的一致性导出;如果数据库还包含非事务表或其他特殊对象,需要另行制定备份方案。
3. 查看备份文件
ls -lh ~/compose-backups/
4. 恢复数据库
恢复前,建议先备份当前数据库。
执行:
docker compose exec -T mysql sh -c \
'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE"' \
< ~/compose-backups/app_db.sql
恢复操作会向数据库导入 SQL 内容。生产环境应先在测试环境验证备份文件,确认目标数据库和恢复策略后再执行。
5. 备份文件保存建议
不要只把备份文件保存在运行数据库的同一块磁盘上。
建议:
-
定期备份数据库。
-
将备份同步到另一台服务器或对象存储。
-
对备份文件设置合理的访问权限。
-
定期进行恢复测试。
-
根据业务需要设置保留周期。
十二、Docker Compose 常见问题
1. MySQL 容器一直处于 Starting 状态
查看日志:
docker compose logs mysql
常见原因:
-
数据库首次初始化时间较长。
-
数据目录权限异常。
-
配置文件错误。
-
磁盘空间不足。
-
已存在的数据目录与当前初始化配置不兼容。
首次初始化数据库可能需要一定时间,应先查看日志,不要直接删除数据卷。
2. Web 应用无法连接 MySQL
检查服务状态:
docker compose ps
查看 MySQL 日志:
docker compose logs mysql
检查应用配置:
docker compose exec web sh -c 'echo "$DATABASE_URL"'
常见错误是使用了:
localhost
在 Web 容器中,localhost 指向 Web 容器自身,而不是 MySQL 容器。
正确的连接主机名应该是:
mysql
3. Redis 连接失败
检查 Redis 服务:
docker compose ps redis
查看 Redis 日志:
docker compose logs redis
测试 Redis:
docker compose exec redis redis-cli ping
如果返回 PONG,说明 Redis 服务正常响应。
4. 修改 .env 后配置没有生效
检查当前解析配置:
docker compose config
修改环境变量后,需要重新创建相关容器:
docker compose up -d --force-recreate
注意:MySQL 官方镜像的初始化环境变量主要在数据目录首次初始化时生效。修改 .env 中的数据库密码,并不会自动修改已经存在的 MySQL 用户密码。
需要通过 MySQL SQL 命令修改实际数据库账号密码,并同步更新 Web 应用连接配置。
5. Docker 磁盘空间不足
查看 Docker 磁盘占用:
docker system df
查看服务器磁盘空间:
df -h
查看数据卷:
docker volume ls
不要直接删除 /var/lib/docker,也不要在没有备份的情况下删除数据库数据卷。
十三、生产环境部署建议
本教程的项目是用于理解 Docker Compose 的教学示例。如果准备将其用于正式业务,还需要进一步完善。
1. 使用 Nginx 反向代理
生产环境通常采用:
用户浏览器
|
v
Nginx(HTTPS)
|
v
FastAPI 容器
|
+--+--+
| |
v v
MySQL Redis
Nginx 可以负责 HTTPS、域名绑定、访问日志、静态文件和反向代理。
2. 不要将数据库暴露到公网
MySQL 和 Redis 一般只需要在内部 Docker 网络中通信。
除非有明确需求,否则不要为它们配置公网端口映射。
3. 做好环境变量管理
.env 适合开发和简单部署,但生产环境应进一步考虑:
-
文件权限。
-
密钥轮换。
-
备份和泄露风险。
-
专门的 Secret 管理方案。
不要将真实数据库密码提交到 GitHub。
4. 设置资源限制
可以根据服务器实际资源为容器设置 CPU 和内存限制。
同时注意 MySQL、Redis 和 Web 应用的资源分配,避免单个服务耗尽服务器资源。
5. 配置日志轮转
长期运行的容器可能产生大量日志。
Docker 默认的 json-file 日志驱动如果没有配置轮转,日志文件可能持续增长。
可以在 Docker 守护进程配置中设置:
{
"log-driver": "local"
}
修改后需要谨慎重启 Docker 服务,并评估对现有容器的影响。新日志驱动通常需要重新创建容器才能应用到容器级配置。
6. 做好应用健康检查
健康检查应根据实际业务设计,区分:
-
进程是否运行。
-
应用是否能响应请求。
-
数据库是否可用。
-
Redis 是否可用。
不要简单地把容器处于 Running 状态等同于整个业务正常。
7. 固定镜像版本
生产环境应使用经过测试的镜像版本,必要时使用镜像 Digest 固定具体内容,并定期更新和修复安全漏洞。
8. 谨慎修改 Docker 防火墙规则
Docker 会自动管理容器网络和端口转发规则。
如果服务器同时使用 UFW、nftables、Docker 网络和其他网络管理工具,需要统一规划防火墙规则。
不要为了排查端口问题而直接执行:
nft flush ruleset
这可能删除 Docker 和其他服务依赖的网络规则。
十四、总结
通过本教程,我们完成了 Docker Compose 的基本学习,并搭建了一个包含 MySQL、Redis 和 FastAPI 的多容器应用。
本篇重点内容:
-
Docker Compose 的基本概念。
-
Debian 安装 Docker Compose 插件。
-
创建多容器项目目录。
-
编写 FastAPI Web 应用。
-
使用 Dockerfile 构建应用镜像。
-
使用
.env管理环境变量。 -
编写
compose.yaml配置文件。 -
使用 Compose 启动和管理多个容器。
-
配置服务依赖、健康检查和网络。
-
使用数据卷实现数据库持久化。
-
MySQL 数据备份与恢复。
-
生产环境部署和安全注意事项。
Docker Compose 适合管理单台服务器上的多容器应用。掌握它之后,可以进一步学习 Docker 网络、镜像构建优化、容器安全和自动化部署。
- 点赞
- 收藏
- 关注作者
评论(0)