用 Docker Compose 在 VPS 上一键拉起全家桶:博客+数据库+监控
2026-08-15 · DevCraft Studio
一份 docker-compose.yml 把 WordPress/静态博客、MySQL、Redis、Uptime Kuma 全拉起来,配 Traefik 自动 HTTPS,再讲清持久化、备份、更新回滚与小内存优化,省心又好玩。
买了台 VPS,第一件事往往是想在上面塞点东西:一个博客、一个数据库、再加个监控看板。手动一步步装 Nginx、MySQL、PHP,版本对着对着就乱了,哪天要换机器更是头大。Docker Compose 的思路是:把"要跑什么、怎么连、数据存哪"全写进一份 docker-compose.yml,一条 docker compose up -d 全部拉起。本文给你一份可直接照抄的全家桶模板,包含博客、MySQL、Redis、Uptime Kuma,再用 Traefik 统一做反向代理和自动 HTTPS,最后把持久化、备份、更新回滚、小内存优化这些"省心"细节一次说透。先说结论:Compose 适合单机几服务的场景,一旦你要多节点高可用,就该上 Swarm 或 K8s,别硬用 Compose 撑集群。现在 Docker 官方主推的是 docker compose v2 插件(Go 重写),老旧的 python 版 docker-compose 已不再维护,装的时候认准 v2。
延伸阅读
更多相关攻略推荐:用几台便宜 VPS 组 k3s / Docker Swarm 容器集、2026 存储型/大硬盘 VPS:备份、网盘与冷数据的性价比之选、AI 算力荒如何推高你的 VPS 账单:内存、电力与连锁涨价、大厂价格战(AWS/GCP/阿里云)打响,小 VPS 厂商会被卷死吗、2026 独立站 PCI-DSS 自查清单:SAQ A 还是 A-E。
一、为什么用 Compose 管全家桶
传统装法的最大问题是"环境即文档丢失"。你在终端敲了一堆 apt、改了一堆配置文件,半年后机器要迁移,这些步骤早忘光了。Compose 把部署过程固化成一份文本文件:它既是部署脚本,也是当前运行态的说明书。新人接手只要读这份 yml,就能知道跑了哪些服务、暴露了哪些端口、数据挂在哪。配合版本管理,回滚也只是一行命令的事。
对预算型 VPS(常见 1 vCPU、1GB 内存)来说,Compose 还有个隐形好处:隔离。每个服务跑在自己的容器里,版本互不污染;博客用的 PHP 版本、监控用的 Node 版本各管各的,不会再出现"升级一个把另一个搞挂"的惨剧。对比一下:裸 docker run 适合跑单个临时容器;Docker Swarm / Kubernetes 适合多节点集群。对个人站、小 SaaS、内部面板这种"单机几服务"的场景,Compose 是甜点区——简单、开销低、够用。注意 Compose 本身不做跨节点自愈,容器挂了靠的是 restart 策略拉起,不是自动漂移到别的机器。
二、compose 文件的结构:服务、网络、卷
一份 Compose 文件有三个顶层级。services 定义要跑的容器;networks 定义容器间如何互通;volumes 定义需要持久化的数据盘。三者在文件里平级声明,服务里再引用。下面是一份可运行的骨架要点:
version: "3.8"
# 外部网络,先手动创建:docker network create web
services:
blog:
image: wordpress:6.6-apache
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppass
WORDPRESS_DB_NAME: wpdb
volumes:
- wp_data:/var/www/html
networks:
- web
- internal
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: wpdb
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppass
MYSQL_ROOT_PASSWORD: rootpass
volumes:
- db_data:/var/lib/mysql
networks:
- internal
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
restart: unless-stopped
networks:
- internal
monitor:
image: uptime-kuma:1.23.13
restart: unless-stopped
volumes:
- kuma_data:/app/data
networks:
- web
volumes:
wp_data:
db_data:
kuma_data:
networks:
web:
external: true
internal:
internal: true几个关键点。第一,容器之间用"服务名"互访,博客里填 WORDPRESS_DB_HOST: db,Docker 内网 DNS 会自动解析到数据库容器,不用写 IP;同理博客连 Redis 填 redis://cache:6379。第二,网络分两面:web 是挂了 Traefik、对外暴露的网络;internal 标了 internal: true,这个网络里的容器不能主动访问外网,数据库和缓存放这里最安全。第三,数据用命名卷(wp_data、db_data、kuma_data)而不是塞进容器可写层,这样即使容器被删,文章、表结构和监控记录都还在。第四,depends_on 配 condition: service_healthy 能保证博客等数据库真正可连通后才启动,而不是只等容器"起来了"就莽撞连接。
密码和密钥不要明文写死在 yml 里。更规范的做法是用 env_file 引入一个 .env 文件,并把 .env 加进 .gitignore,这样既方便协作又不泄露凭证。Compose 会把这个文件里的变量注入环境变量,供各个服务引用;本文示例为便于直接复制把值写死了,你上线时建议改回外部文件管理,尤其是数据库密码和 Traefik 的通知邮箱。
三、Traefik 标签自动申请证书
如果每个服务都自己配 Nginx 加证书,维护成本很高。Traefik 作为边缘代理统一收口:它监听 Docker 套接字,发现带标签的容器就自动接管路由,并向 Let's Encrypt 申请证书。两个设计要点。一是 --providers.docker.exposedbydefault=false,默认不暴露任何容器,只有显式打标签的服务才会被代理,避免误暴露。二是靠四个标签完成接入:
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host('blog.example.com')"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=le"rule=Host('blog.example.com') 告诉 Traefik 这个域名归这个服务;entrypoints=websecure 走 443;tls.certresolver=le 触发证书申请。证书会写进 acme.json,记得创建时设权限 chmod 600,否则 Traefik 拒绝启动。再配一条全局 HTTP 到 HTTPS 的重定向,用户访问 http 会自动跳 https。如果容器内部端口不是 80,要补一行 traefik.http.services.blog.loadbalancer.server.port=80 指明真实端口。想换成纯静态博客也很简单:把 blog 服务换成 nginx:alpine,把本地构建好的 HTML 目录挂进 /usr/share/nginx/html,再打上同样的 Traefik 标签即可,证书逻辑完全复用。一个 Traefik 实例可以同时代理十几个域名,每个域名各自拿一张证书,扩容几乎零成本。
首次部署建议先用 Let's Encrypt 的 staging 环境验证流程,避免反复申请触发限流。在 Traefik 命令里加一行 --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory,就能拿到可签发但浏览器暂不信任的测试证书;确认路由与域名解析都无误后,再去掉这行切回正式环境。注意域名 A 记录必须提前指向这台 VPS 的 IP,否则 HTTP 或 TLS 校验会因为访问不到而失败,证书也就申请不下来。
四、Watchtower 自动更新镜像
容器跑起来后,镜像会慢慢过时,安全补丁也跟不上。Watchtower 盯着运行的容器,发现新镜像就拉取并重启。但"全自动更新"对数据库是危险动作,所以生产环境更推荐监控模式:
watchtower:
image: containrrr/watchtower:1.7.1
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_MONITOR_ONLY=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
- TZ=Asia/Shanghai
labels:
- "com.centurylinklabs.watchtower.enable=false"WATCHTOWER_MONITOR_ONLY=true 让 Watchtower 只发"有新版本可用"的通知,不真去重启容器,把更新时机留给你在维护窗口手动操作,最稳。WATCHTOWER_SCHEDULE 是六段 cron,这里表示每天凌晨 4 点检查一次;WATCHTOWER_CLEANUP=true 更新成功后清掉旧镜像,省磁盘。想给某些关键容器(如数据库)彻底关掉自动更新,打标签 com.centurylinklabs.watchtower.enable=false 即可,不管监控模式还是自动模式都生效。若你确实想要全自动,去掉 MONITOR_ONLY 那行,但务必把数据库排除掉,避免一次镜像升级把表搞坏。
五、资源限制与容器日志
预算 VPS 资源紧,最怕某个容器内存泄漏把整机拖垮。用 deploy.resources.limits 给每个服务封顶:
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256Mlimits 是硬上限,超了容器会被 OOM 杀掉并由 restart: unless-stopped 拉起;reservations 是保底预留,保证关键服务有最低资源。小机器上建议给 MySQL 多一点内存、给博客和监控各限一点,整体留几百 MB 给宿主系统。还有一处容易被忽略:容器日志。默认的 json-file 驱动不限制体积,长期运行会悄悄吃满磁盘。在 compose 里给每个服务加上 logging 配置,例如 max-size: 10m 配合 max-file: 3,日志超过 10MB 就滚动,并最多保留 3 份,能有效防止磁盘被日志写满。最后提醒一句:任何修改过 yml 之后,用 docker compose up -d 重新应用即可,Compose 会智能地只重建有变动的服务,不会把无关容器全掀翻。
六、数据持久化:命名卷、绑定挂载与备份
容器本身是易失的——你 docker compose down 再 up,容器是重建的,里面写的东西全没。持久化靠"卷(volume)"把数据映射到宿主机。两种主流方式要分清楚:
- 命名卷(named volume):Docker 托管,写到 /var/log/volumes 下(实际在 /var/lib/docker/volumes/),按名字引用。数据库、监控数据这类"容器自己会初始化"的状态数据首选,因为容器首次启动会自己建好数据目录。
- 绑定挂载(bind mount):直接把宿主机某个目录挂进容器,你能用 ls 看到、用 cp 备份。上传文件、配置文件适合用它,方便直接在宿主机操作。
两者可混用,不必二选一:数据库用命名卷,配置文件用绑定挂载都很常见。备份铁律:定期、异地、且真机恢复过一次。快照不等于备份,备份不等于能恢复——只有演练过的恢复才算数。针对上面的全家桶,最稳的备份是用引擎自带工具导出,而不是拷文件:
# MySQL / MariaDB
docker compose exec db mysqldump -u root -p --all-databases > backup_$(date +%F).sql
# 若用 Postgres,换成:
docker compose exec db pg_dump -U app app > backup_$(date +%F).sql也可以用临时容器把命名卷整体打包:
docker run --rm -v db_data:/source:ro -v $(pwd):/backup alpine tar czf /backup/db_data_$(date +%F).tar.gz -C /source .一个常见坑是权限:bind mount 时宿主机 UID 和容器内运行用户 UID 不一致,会导致容器写不进挂载目录,遇到就 chown 或在 compose 里指定 user。命名卷由 Docker 在首次挂载时按镜像内用户初始化,一般更省心。把备份传到对象存储(如 Backblaze B2 / Cloudflare R2)做异地留存,比只留在同一台机器靠谱得多。
七、更新与回滚:升级不慌
更新镜像别直接 latest 一把梭。稳妥流程:
- 先备份数据库(见第六节),这是回滚的命根子;
- 拉新镜像:docker compose pull blog;
- 滚动替换:docker compose up -d blog,Compose 会用新镜像重建该服务,旧卷保留;
- 出问题就回退:把镜像 tag 改回上一个版本号(比如 wordpress:6.5-apache)再 up -d;数据库 schema 变更则用备份恢复。
关键纪律:镜像用具体版本号而不是 latest,这样你知道自己在跑什么、回退到哪。docker compose down 默认不删命名卷,所以数据不会随重启丢失;只有加 -v 才会连卷一起删,慎用。大版本升级前务必读变更日志(changelog),尤其是数据库的主版本跳跃(如 MySQL 8.3 → 8.4),往往要先用官方迁移命令处理 schema,不能硬升。WordPress 这类应用升级前也建议先拍个快照或导出,避免插件不兼容把站点搞白屏。
八、小内存 VPS 优化:Alpine、swap 与 zram
在 1–2G 内存的便宜 VPS 上堆服务,内存是最紧的资源。如果你把上面的 WordPress 换成更轻量的静态站 / Node 服务,再配合下面三招,1G 内存跑 nginx + 应用 + 数据库 + 监控完全可行:
- 用 Alpine 镜像:Alpine 基础镜像 20–50MB,而 Debian/Ubuntu 系 200–600MB。同样一套栈,Alpine 能省下 60–70% 基线内存。不过 WordPress 官方没有好用的 Alpine 版,轻量场景可优先选 nginx:alpine、redis:7-alpine 这类官方 Alpine 镜像。
- 开 swap 或 zram:swap 用磁盘当内存,防止 OOM;zram 是在内存里建一个压缩块设备,把不常用内存页压缩后存回 RAM,几乎不占磁盘又显著降低"内存见底"的概率,对便宜 VPS 很友好。Debian/Ubuntu 可用 zram-tools,或开个 1–2G 的 swap 文件。
- 合理 reservation:给每个服务设 memory reservation(软下限),让 Docker 在内存紧张时按优先级保住关键服务。
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 写入 /etc/fstab 让它开机自动挂载注意 MySQL/Postgres 官方不建议把它的数据放 swap(随机写会很慢),所以给 db 留够 reservation,别让它被挤到 swap 上。上线后建议用 docker stats 或装个 Netdata 看各容器真实占用,再回头收紧 limits,别拍脑袋设太大浪费内存或太小频繁 OOM。
九、上线前的自检清单
- compose 文件用具体版本号(如 wordpress:6.6-apache),不用 latest;
- 只有反向代理 / 需要对外暴露的服务 publish 80/443,数据库、Redis、监控不暴露宿主机端口;
- 敏感配置(密码、密钥)走 .env 或 secrets,不写进 git;
- 每个服务设 restart 策略和资源 limits/reservations,并配 logging 上限;
- 配好 Traefik + 免费 SSL,并实测证书续期;
- 数据库用命名卷持久化,且有定期异地备份 + 恢复演练;
- 小内存机器开 Alpine 镜像 + swap/zram;
- 宿主机本身先按安全基线加固(密钥登录、防火墙、fail2ban)。
把这套跑顺,你的 VPS 就从"一堆手配的服务"变成"一份可复现的文件"。以后换机器、加服务、回滚版本,都是改 yaml 和敲命令的事,而不是熬夜救火。Compose 不是银弹,但把持久化、隔离、限额、备份这四件事做对,它足够撑起一台 VPS 上的绝大多数小项目。
延伸阅读:把监控真正跑起来可参考 用 Uptime Kuma 给 VPS 做监控;容器和数据库都要备份,见 VPS 快照与备份策略;想看更多自托管玩法合集,读 VPS 自托管全家桶速查。