Docker Compose 很适合 VPS:一台机器、几个服务、一个 compose.yml,比手动装 Nginx、数据库、Redis 清爽很多。
但很多人的 Compose 文件只做到“能启动”:
services:
app:
image: myapp:latest
restart: always
ports:
- "3000:3000"
这在测试环境够用,到了生产环境就很危险。容器挂了没人知道,日志把系统盘写满,数据库没健康检查,更新时直接拉 latest,.env 里塞满密钥,出了问题也不知道怎么回滚。
这篇不讲 Docker 怎么安装。基础安装可以看:
本文只解决一个问题:Docker Compose 项目已经能跑了,怎样改成更适合 VPS 生产环境的配置。
一份比较靠谱的 VPS Docker Compose 生产配置,至少应该考虑:
- 固定镜像版本,不要盲用
latest; - 每个关键服务有
healthcheck; - 合理的
restart策略; - CPU、内存、进程数或 ulimit 限制;
- 日志轮转,防止磁盘被打满;
- 密钥不要直接写进 compose 文件;
- 数据卷、数据库、上传目录有备份;
- 更新前有回滚方案。
如果你只想记住一句话:Compose 不是问题,问题是把开发环境的 Compose 原封不动搬到生产环境。
很多教程喜欢写:
image: nginx:latest
image: postgres:latest
image: redis:latest
这在本地测试方便,但在 VPS 生产环境很容易出事故。你今天 docker compose pull,明天上游镜像更新,大版本变了、配置行为变了、数据库兼容性变了,网站可能直接起不来。
更稳的写法是固定版本:
services:
web:
image: nginx:1.26-alpine
db:
image: postgres:16-alpine
redis:
image: redis:7.2-alpine
应用镜像也建议使用明确 tag:
image: ghcr.io/example/myapp:2026-06-14-1
不要把生产环境变成“每次更新都抽盲盒”。
如果你必须用 latest,至少要做到:
- 更新前备份数据卷;
- 先在测试机拉起;
- 确认迁移脚本;
- 保留上一版镜像;
- 有明确回滚命令。
Compose 里的 depends_on 只能解决“启动顺序”,不能证明服务真的可用。
比如数据库容器进程启动了,但还没接受连接;应用容器启动了,但数据库迁移失败;Nginx 启动了,但反代后端不通。没有健康检查,你只会看到容器状态是 running,但用户访问已经炸了。
一个 Web 应用可以这样写:
services:
app:
image: ghcr.io/example/myapp:2026-06-14-1
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
PostgreSQL:
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
MySQL / MariaDB:
services:
db:
image: mariadb:11
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root -p$${MARIADB_ROOT_PASSWORD} --silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Redis:
services:
redis:
image: redis:7.2-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
然后再让应用等数据库健康:
services:
app:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
注意:condition: service_healthy 依赖的是 Compose V2。老旧教程可能不适用。
很多模板统一写:
restart: always
它不是错,但不是所有服务都适合。
常见选择:
| 策略 | 适合场景 | 风险 |
|---|---|---|
no | 一次性任务、迁移脚本 | 崩了不会自动恢复 |
on-failure | 任务型服务、worker | 非异常退出不会重启 |
unless-stopped | 大多数 Web 服务 | 手动停掉后不会随 Docker 重启自动启动 |
always | 必须常驻的基础服务 | 手动停掉也可能重启,容易误会 |
对 VPS 上的小项目,通常推荐:
restart: unless-stopped
对于迁移任务、初始化任务,不要写自动重启。否则迁移失败后可能反复执行,越修越乱。
如果你的问题是 VPS 重启后服务没恢复,可以看这篇:
VPS 上最怕一个容器失控,把整台机器内存吃完,最后 OOM Killer 随机杀进程。
Compose 可以限制资源。不同 Docker/Compose 场景对 deploy.resources 支持曾经有差异,现在 Compose V2 已经更常见,但你仍然要在自己的环境里验证。
示例:
services:
app:
image: ghcr.io/example/myapp:2026-06-14-1
deploy:
resources:
limits:
cpus: "1.0"
memory: 768M
reservations:
memory: 256M
如果你的环境不生效,可以使用兼容写法:
services:
app:
mem_limit: 768m
mem_reservation: 256m
cpus: 1.0
还可以限制进程数:
services:
app:
pids_limit: 256
以及打开文件数:
services:
nginx:
ulimits:
nofile:
soft: 65535
hard: 65535
怎么设置才合理?先看业务类型:
| 服务 | 建议限制思路 |
|---|---|
| Nginx/Caddy | CPU 不用太高,nofile 可以放宽 |
| Node/Python 应用 | 重点限制内存,防止泄漏拖垮整机 |
| MySQL/PostgreSQL | 不建议过小,数据库需要稳定内存 |
| Redis | 必须设置 Redis 自身 maxmemory 策略 |
| Worker/队列 | 限 CPU/内存,避免批任务打满 VPS |
如果你的 VPS 已经 OOM,可以参考:
Docker 默认 json-file 日志如果不限制,长期运行会把磁盘写满。
这类故障非常常见:
- 应用报错循环;
- Nginx access log 暴涨;
- 爬虫或 CC 攻击刷接口;
- 容器 stdout 打印 SQL 或 debug 日志;
- 一个月后系统盘突然 100%。
给服务加日志限制:
services:
app:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
Nginx、应用、worker 都建议加。数据库日志要更谨慎,避免关键诊断信息被过早覆盖,但也不能无限增长。
查看 Docker 日志占用:
docker system df
sudo du -h /var/lib/docker/containers | sort -h | tail -20
如果 Docker 已经占满磁盘,可以看:
不要这样:
environment:
DATABASE_PASSWORD: my-secret-password
API_KEY: sk_live_xxx
compose.yml 很容易进 Git,甚至被复制到工单、群聊、文档里。
更好的做法是:
services:
app:
env_file:
- .env.production
.env.production 不要提交到 Git:
.env
.env.*
!.env.example
提供一个 .env.example:
DATABASE_URL=postgres://user:password@db:5432/app
REDIS_URL=redis://redis:6379/0
APP_SECRET=change-me
如果是 Docker Swarm 或更复杂的环境,可以考虑 Docker secrets。但对单机 VPS 来说,先把 .env 管好、权限设好、备份加密,已经能避开大多数事故。
设置权限:
chmod 600 .env.production
不要在镜像构建阶段把密钥写进镜像。构建参数和运行时环境变量要分清。
很多 Compose 文件喜欢这样:
ports:
- "5432:5432"
- "6379:6379"
- "3000:3000"
这会把数据库、Redis、应用端口直接暴露到公网。生产环境通常不需要这样。
更安全的做法是:
- 只把 Nginx/Caddy 暴露到 80/443;
- 应用服务只暴露在 Docker 内部网络;
- 数据库和 Redis 不映射到宿主机公网端口;
- 管理后台放 VPN、SSH 隧道或白名单。
示例:
services:
proxy:
image: caddy:2
ports:
- "80:80"
- "443:443"
networks:
- frontend
app:
image: ghcr.io/example/myapp:2026-06-14-1
expose:
- "3000"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend
networks:
frontend:
backend:
internal: true
internal: true 可以让 backend 网络不直接通外部网络,适合数据库层隔离。但要注意:如果某些服务需要访问外网更新数据,就不要放到完全 internal 的网络里。
数据库不要开公网,这篇已经讲过:
容器可以删,镜像可以重拉,Volume 丢了就是真事故。
先列出数据在哪里:
docker volume ls
docker volume inspect volume_name
Compose 里建议显式命名:
volumes:
postgres_data:
redis_data:
uploads:
服务里挂载:
services:
db:
volumes:
- postgres_data:/var/lib/postgresql/data
app:
volumes:
- uploads:/app/uploads
更新前至少做三件事:
- 数据库 dump;
- Volume 备份;
- 保存当前
compose.yml和.env。
如果要做恢复演练,可以看:
不要以为有 VPS 快照就够。快照适合短期回滚,长期备份还得靠独立备份仓库。
生产环境不要直接:
docker compose pull
docker compose up -d
更稳的流程:
# 1. 记录当前版本
docker compose ps
docker images | grep myapp
# 2. 备份 compose 和 env
cp compose.yml compose.yml.bak.$(date +%F-%H%M)
cp .env.production .env.production.bak.$(date +%F-%H%M)
# 3. 备份数据库和关键 volume
# 这里按你的数据库类型执行 dump
# 4. 拉取新镜像
docker compose pull
# 5. 启动
docker compose up -d
# 6. 看健康状态
docker compose ps
docker compose logs --tail=100 app
如果失败,回滚:
# 改回旧镜像 tag
vim compose.yml
docker compose up -d
所以镜像 tag 一定要固定。如果你用的是 latest,回滚时你甚至不知道上一版是什么。
下面是一个简化模板,不是万能配置,但包含了生产环境必须考虑的几类东西:
services:
proxy:
image: caddy:2.8
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
networks:
- frontend
app:
image: ghcr.io/example/myapp:2026-06-14-1
restart: unless-stopped
env_file:
- .env.production
expose:
- "3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
mem_limit: 768m
cpus: 1.0
pids_limit: 256
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env.production
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
networks:
- backend
redis:
image: redis:7.2-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
networks:
frontend:
backend:
internal: true
volumes:
caddy_data:
caddy_config:
postgres_data:
redis_data:
上线前检查:
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=100
docker compose config 很有用,它能把最终合并后的配置展开,帮你提前发现语法和变量问题。
把 Compose 项目放到 VPS 生产环境前,至少确认:
- 镜像 tag 固定,不依赖
latest; - Web、数据库、Redis 都有健康检查;
- 关键服务使用
restart: unless-stopped; - 应用容器有 CPU/内存/PID 限制;
- Docker 日志设置 max-size/max-file;
-
.env.production不进 Git,权限为 600; - 数据库和 Redis 没有暴露公网端口;
- Volume 名称明确,备份策略清楚;
- 更新前会备份数据库、Volume、compose 文件和 env;
- 能用旧镜像 tag 回滚;
- Nginx/Caddy 反代和 HTTPS 正常;
-
docker compose ps没有 unhealthy 服务。
适合中小项目、个人 SaaS、工具站、博客、内部系统和低复杂度多服务应用。它不适合复杂多节点调度,但一台 VPS 上跑几个服务,Compose 反而比 Kubernetes 更简单、更可控。
多数 VPS 生产服务选 unless-stopped 更直观。你手动停掉服务后,它不会在 Docker 重启时又自动起来。必须常驻且不希望被手动停掉影响的服务,可以考虑 always。
不一定。healthcheck 会标记健康状态,是否重启还要看服务行为和外部编排。它至少能让你用 docker compose ps、监控系统和部署脚本判断服务是否真的可用。
限制太小会影响性能,但完全不限制更危险。VPS 上建议先设置保守上限,防止单个容器拖垮整机,再根据监控逐步调整。
不是。.env 只是避免把密钥写进 compose 文件。你还要确保它不进 Git、权限足够严格、备份时加密、不要在日志里打印密钥。
Docker Compose 在 VPS 上跑生产环境没有问题,前提是你不要把“能启动”的配置当成“能长期稳定运行”的配置。
真正需要补齐的是:固定版本、健康检查、资源限制、日志轮转、密钥管理、端口隔离、Volume 备份和可回滚更新。把这些做完,一台普通 VPS 上的 Compose 项目就会从“玩具部署”变成“可维护的生产部署”。
