用 Docker Compose 在 VPS 上堆服务:从零到一个可用栈

不想一个个配环境?用 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 一把梭。稳妥流程:

  1. 先备份数据库(见第三节),这是回滚的命根子;
  2. 拉新镜像:docker compose pull app;
  3. 滚动替换:docker compose up -d app,Compose 会用新镜像重建该服务,旧卷保留;
  4. 出问题就回退:把镜像 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。

八、上线前的自检清单

  1. compose 文件用具体版本号,不用 latest;
  2. 只有反向代理 publish 80/443,数据库、Redis 不暴露宿主机端口;
  3. 敏感配置(密码、密钥)走 .env 或 secrets,不写进 git;
  4. 每个服务设 restart 策略和资源 limits/reservations;
  5. 配好反向代理 + 免费 SSL,并实测续期;
  6. 数据库用命名卷持久化,且有定期异地备份 + 恢复演练;
  7. 小内存机器开 Alpine + swap/zram;
  8. 宿主机本身先按上篇做安全基线(密钥登录、防火墙、fail2ban)。

把这套跑顺,你的 VPS 就从“一堆手配的服务”变成“一份可复现的文件”。以后换机器、加服务、回滚版本,都是改 yaml 和敲命令的事,而不是熬夜救火。Compose 不是银弹,但把持久化、隔离和限额这三件事做对,它足够撑起一台 VPS 上的绝大多数小项目。