用 Docker Compose 在 VPS 上堆服务:从零到一个可用栈
2026-08-15 · DevCraft Studio
不想一个个配环境?用 Docker Compose 在 VPS 上一键拉起博客、数据库、反向代理。讲清 compose 文件结构、网络隔离、volumes 持久化与备份、免费 SSL、资源限制和小内存注意点。
在一台 VPS 上同时跑博客、数据库、缓存和反向代理,传统做法是逐个装环境、改配置、记依赖,迁机时全得重来。Docker Compose 把这堆东西写进一个 yaml 文件,一条 docker compose up -d 全拉起来,一条命令也能整体停。本文带你从零搭出一个“能长期用”的栈,重点是持久化、网络隔离和资源限制这三件新手最容易翻车的事。先说结论:Compose 适合单机几服务的场景,一旦你要多节点高可用,就该上 Swarm 或 K8s,别硬用 Compose 撑集群。现在 Docker 官方主推的是 docker compose v2 插件(Go 重写),老旧的 python 版 docker-compose 已不再维护,装的时候认准 v2。
延伸阅读
更多相关攻略推荐:泛域名 SSL 证书自动化避坑:Acme.sh + DNS API 、BGP 是互联网的"高德地图":打开网页时,数据是这样找路的、【优化线路 02】BGP / AS4837 常规线路年付 10 美元、高级玩家玩出花:如何在 VPS 上广播自己的 IP 地址?BYOIP、计算机科学两大难题之一:缓存失效与 CDN 瞬时刷新奥秘。
一、为什么用 Compose:环境隔离 + 一键启停
Docker 把每个服务封进独立的容器,自带运行环境,不再污染宿主机,也不会“我本地能跑你线上跑不了”。Compose 在 Docker 之上做编排:用一份声明式文件定义多个服务、网络和卷,依赖关系、启动顺序、端口映射一目了然。
- 环境隔离:博客用 Node、数据库用 Postgres、缓存用 Redis,各跑各的镜像,版本互不干扰。
- 一键启停:docker compose up -d 起全部,docker compose down 停全部,docker compose ps 看状态。
- 可复现:同一份 compose 文件换台机器、换个人,跑出来一模一样,迁移和协作不再靠记忆。
对比一下:裸 docker run 适合跑单个临时容器;Docker Swarm / Kubernetes 适合多节点集群。对个人站、小 SaaS、内部面板这种“单机几服务”的场景,Compose 是甜点区——简单、开销低、够用。注意 Compose 本身不做跨节点自愈,容器挂了靠的是 restart 策略拉起,不是自动漂移到别的机器。
二、一个最小可用栈:nginx + 应用 + 数据库
下面是一份可直接用的 compose 文件骨架。重点是:只有反向代理对外暴露 80/443,应用和数据库都在内部网络,数据库绝不 publish 端口到宿主机。
services:
proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
networks:
- web
- internal
restart: unless-stopped
app:
image: your-app:latest
environment:
- DATABASE_URL=postgres://app:changeme@db:5432/app
networks:
- internal
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=app
- POSTGRES_USER=app
- POSTGRES_PASSWORD=changeme
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- internal
restart: unless-stopped
networks:
web:
internal:
volumes:
pgdata:注意数据库那一段没有 ports 映射,意味着它只在 internal 网络里被 app 通过主机名 db 访问,外网根本连不到 5432。这就是“最小暴露”原则在容器世界的落地。depends_on 只保证启动顺序,不等 db 真正就绪,所以应用侧最好加重试或 wait-for-it 脚本,避免启动瞬间的连接失败。
三、volumes 持久化与备份:数据不能随容器灰飞烟灭
容器本身是易失的——你 docker compose down 再 up,容器是重建的,里面写的东西全没。持久化靠“卷(volume)”把数据映射到宿主机。两种主流方式:
- 命名卷(named volume):Docker 托管,写到 /var/lib/docker/volumes/ 下,按名字引用。数据库首选,因为容器首次启动会自己初始化数据目录。
- 绑定挂载(bind mount):直接把宿主机某个目录挂进容器,你能用 ls 看到、用 cp 备份。上传文件、配置文件适合用它。
上面例子里 pgdata 就是命名卷。备份数据库最稳的是用应用自带工具导出,而不是拷文件:
docker compose exec db pg_dump -U app app > backup_$(date +%F).sql
docker compose exec -T db psql -U app app < backup_2026-08-15.sql也可以用临时容器把命名卷打包:
docker run --rm -v pgdata:/source:ro -v $(pwd):/backup alpine \
tar czf /backup/pgdata_$(date +%F).tar.gz -C /source .备份铁律:定期、异地、且真机恢复过一次。快照不等于备份,备份不等于能恢复——只有演练过的恢复才算数。一个常见坑是权限:bind mount 时宿主机 UID 和容器内运行用户 UID 不一致,会导致容器写不进挂载目录,遇到就 chown 或在 compose 里指定 user。命名卷由 Docker 在首次挂载时按镜像内用户初始化,一般更省心。
四、反向代理与免费 SSL:让流量安全进门
反向代理是栈的正门:它终结 TLS、把请求路由到对应容器、还能加安全头。两个经得起生产的方案:
- Nginx + Certbot:适合域名少、喜欢显式配置文件的场景。Certbot 申请 Let's Encrypt 免费证书后挂进容器,crontab 里设自动续期。
- Traefik:靠 Docker 标签(labels)动态发现服务,内置 ACME 自动签发 Let's Encrypt,加个服务自动配好路由和证书,适合服务多的栈。
Traefik 的最小示意(注意它挂载 docker.sock 只读来发现容器):
traefik:
image: traefik:v3.0
command:
- --providers.docker=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- web无论选哪个,记得防火墙只开 80/443,并且真实测一次证书续期,别等过期才发现问题。Let's Encrypt 证书 90 天一换,Certbot 的 renew 钩子或 Traefik 的 ACME 会自动处理,但 DNS 解析要先指向这台 VPS,否则签发会失败。
五、资源限制:别让一个容器吃满内存
默认情况下容器不限额,一个内存泄漏的 app 或吃内存的 Postgres 能把整台 VPS 拖垮,触发 OOM 把别的服务一起杀掉。Compose 里用 deploy.resources.limits 给每个服务设上限(docker compose v2 直接生效):
services:
db:
image: postgres:16-alpine
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 512M
restart: unless-stopped经验值:Nginx 反向代理 64–128M 就够;Redis 把容器内存上限的约 80% 设给 maxmemory;Postgres 的 shared_buffers 取容器内存上限的约 25%。同时给容器设 restart: unless-stopped,挂了自动拉起。用 docker stats --no-stream 看实时候内存占用,按真实数据收放限额。limits 是硬上限会触发 OOM Kill,reservations 是软下限只影响调度,建议两者都给,给关键服务留出保命内存。
六、更新与回滚:升级不慌
更新镜像别直接 latest 一把梭。稳妥流程:
- 先备份数据库(见第三节),这是回滚的命根子;
- 拉新镜像:docker compose pull app;
- 滚动替换:docker compose up -d app,Compose 会用新镜像重建该服务,旧卷保留;
- 出问题就回退:把镜像 tag 改回上一个版本号(比如 your-app:1.4.2)再 up -d;数据库 schema 变更则用备份恢复。
关键纪律:镜像用具体版本号而不是 latest,这样你知道自己在跑什么、回退到哪。docker compose down 默认不删命名卷,所以数据不会随重启丢失;只有加 -v 才会连卷一起删,慎用。大版本升级前务必读变更日志(changelog),尤其是数据库的主版本跳跃,往往要先用官方迁移命令处理 schema,不能硬升。
七、小内存 VPS 注意:swap/zram 与 Alpine
在 1–2G 内存的便宜 VPS 上堆服务,内存是最紧的资源。三招救场:
- 用 Alpine 镜像:Alpine 基础镜像 20–50MB,而 Debian/Ubuntu 系 200–600MB。同样一套栈,Alpine 能省下 60–70% 基线内存,1G 机器能塞下 nginx+PHP+Postgres+Redis。
- 开 swap 或 zram:swap 用磁盘当内存,防止 OOM;zram 是在内存里建一个压缩块设备,把不常用内存页压缩后存回 RAM,几乎不占磁盘又显著降低“内存见底”的概率,对便宜 VPS 很友好。Debian/Ubuntu 可装 zram-tools,或开个 1–2G 的 swap 文件。
- 合理 reservation:给每个服务设 memory reservation(软下限),让 Docker 在内存紧张时按优先级保住关键服务。
开 swap 文件示例:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 写入 /etc/fstab 让它开机自动挂载注意 Postgres 官方不建议把它的数据放 swap(随机写会很慢),所以给 db 留够 reservation,别让它被挤到 swap 上。zram 在较新内核是默认模块,用 systemd-zram-generator 或 zram-tools 一句命令就能启用,比传统 swap 文件更适合 SSD 小的机器,因为几乎不写盘。上线后建议用 docker stats 或装个 Netdata 看各容器真实占用,再回头收紧 limits,别拍脑袋设太大浪费内存或太小频繁 OOM。
八、上线前的自检清单
- compose 文件用具体版本号,不用 latest;
- 只有反向代理 publish 80/443,数据库、Redis 不暴露宿主机端口;
- 敏感配置(密码、密钥)走 .env 或 secrets,不写进 git;
- 每个服务设 restart 策略和资源 limits/reservations;
- 配好反向代理 + 免费 SSL,并实测续期;
- 数据库用命名卷持久化,且有定期异地备份 + 恢复演练;
- 小内存机器开 Alpine + swap/zram;
- 宿主机本身先按上篇做安全基线(密钥登录、防火墙、fail2ban)。
把这套跑顺,你的 VPS 就从“一堆手配的服务”变成“一份可复现的文件”。以后换机器、加服务、回滚版本,都是改 yaml 和敲命令的事,而不是熬夜救火。Compose 不是银弹,但把持久化、隔离和限额这三件事做对,它足够撑起一台 VPS 上的绝大多数小项目。