Docker Compose 从零搭建完整服务栈
说实话,现在回想一年前刚拿到这台 VPS 时的操作记录,我自己都觉得好笑。那时候我手动装了 Docker,然后一个个敲 docker run 启容器,端口从 3000 一路排到 8108,环境变量直接写在命令里——结果某天重启服务器之后,对着满屏的启动参数发呆,我自己都忘了当初给 FileCodeBox 设置的是哪个密码了。
后来我痛定思痛,把所有服务全部迁移到 Docker Compose。现在 22 个容器,一条命令全部启停,配置文件进 Git 做版本管理。今天就把完整经验整理出来。
环境准备
Ubuntu 22.04 的 VPS(我的是 KVM 架构),装 Docker 官方脚本一步到位:
curl -fsSL https://get.docker.com | bash
docker --version
新版 Docker 已经把 Compose 集成进 CLI 了,直接 docker compose(没有横杠):
docker compose version
Docker Compose version v2.23.x
第一个 Compose:博客 + 反代
从最简单的开始——Halo 博客 + OpenResty 反代:
version: "3.8"
services:
halo:
image: halohub/halo:2.23
container_name: halo
restart: unless-stopped
ports:
- "8090:8090"
volumes:
- ./halo-data:/root/.halo2
environment:
- TZ=Asia/Shanghai
networks:
- app
openresty:
image: openresty/openresty:alpine
container_name: openresty
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
networks:
- app
networks:
app:
driver: bridge
两个服务放在同一个自定义网络 app 里。OpenResty 反代 Halo 时直接用 http://halo:8090——容器名就是主机名,这个设计真的很优雅。
反代配置:
upstream halo_backend {
server halo:8090;
keepalive 32;
}
server {
listen 80;
server_name soulwrite.xyz;
location / {
proxy_pass http://halo_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_http_version 1.1;
proxy_set_header Connection "";
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
proxy_pass http://halo_backend;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
启动就一条命令:
cd /opt/services/blog/
docker compose up -d
数据库 + 环境变量分离
后来加了 MySQL,Compose 文件变成多服务。这里有个坑:Halo 启动时 MySQL 还没准备好,直接报错退出。
我的解决方案是给 Halo 加 restart: unless-stopped,让它自动重试。MySQL 实际启动时间很短,Halo 重试两三次就连上了。比搞 healthcheck 简单多了。
环境变量全部抽到 .env 文件:
MYSQL_ROOT_PASSWORD=mysupersecretpassword
MYSQL_DATABASE=halo_db
MYSQL_USER=halo_user
MYSQL_PASSWORD=halo_pass
Compose 里写 ${MYSQL_ROOT_PASSWORD} 引用。这样 Compose 文件可以公开,密码单独管理。
踩坑记录
坑 1:数据没挂载,一删全没
刚开始忘了挂载 volume,容器重建后数据全丢。后来才知道 ./halo-data:/root/.halo2 这种显式挂载是必须的。现在我每周写 cron 定时 tar czf 压缩整个数据目录,再通过云存储做冷备。
坑 2:端口冲突
22 个容器全部用 ports 导出宿主机很快撞端口。后来发现很多容器根本不需要对外暴露,放在 Docker 内部网络里通过服务名访问就行。现在只有 OpenResty(80/443)和个别需要直连的服务映射端口,其余 MySQL、Redis 等全部内网访问。
坑 3:容器 OOM 被杀
有段时间内存吃紧,Linux 的 OOM killer 开始随机杀容器,Uptime Kuma 和 IT-Tools 首当其冲。解决办法:
- 给每个服务设置 deploy.resources.limits.memory
- 加了 2GB swap 作为安全缓冲
我的最终目录结构
/opt/services/
├── blog/ # Halo + OpenResty + MySQL
├── ai/ # Ollama
├── media/ # Navidrome + Alist
├── tools/ # IT-Tools + ConvertX + Uptime Kuma
├── storage/ # Lsky Pro + FileCodeBox
└── shared.env # 共享环境变量
每个子目录一个 docker-compose.yml,独立管理。
总结
Docker Compose 的核心三件事:
1. 服务定义清晰——每个容器做什么、依赖什么
2. 数据持久化——volume 一定要挂,备份一定要做
3. 网络隔离——别把所有端口都暴露给外界
从手动 docker run 到 Compose 管理,这是我在服务器运维上投入产出比最高的一次升级。花一下午整理 Compose 文件,省下来的时间是按年算的。
Docker Compose 从零搭建完整服务栈
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法