返回博客
八月 19, 2026指南

如何在 VPS 上用 Docker Compose 部署应用

Ubuntu 上 Docker Compose v2 的实用生产指南:安装引擎、编写带网络和卷的 Compose 文件、处理密钥、前面放反向代理、不慌不忙地更新,以及备份命名卷。

如何在 VPS 上用 Docker Compose 部署应用

Docker Compose 是在一台 VPS 上跑小型栈的常用方式:一个 Web 应用、一个数据库、一个 Redis 缓存,以及一个反向代理,全部写在一个 YAML 文件里。它不是 Kubernetes。你不会得到多节点故障转移。你会得到可重复、可审查、能在新的 Hiddence VPS 上几分钟内重建的配置——这正是大多数副项目和小产品需要的。

本文假设你想要比周五晚上 docker run 更接近生产的东西。我们将安装 Docker Engine 和 Compose 插件,创建非 root 的部署用户,编写带健康检查和重启策略的 Compose 文件,把密钥留在 Git 之外,只把反向代理端口发布到互联网,并定义更新与备份流程。示例栈是典型的 Web 应用加 PostgreSQL 加 Caddy,但同一模式适用于 Node、PHP、Python,或一堆 worker。

为什么单台 VPS 上的 Compose 仍然有意义

有人因为时髦跳到 Kubernetes,然后把周末花在一个网站的 YAML 上。Compose 仍然好读。你给文件做版本控制,记录环境变量,应用前可以看 diff。隔离足够好:如果你用了网络和最小权限用户,被攻破的应用容器不应该拿着数据库 UNIX 套接字。资源限制能阻止一次内存泄漏冻住整台 VPS。

  • 一个文件描述整个栈
  • 命名卷在容器重建后仍然存在
  • 内部 Docker 网络让 Postgres 远离公共互联网
  • restart: unless-stopped 覆盖大多数重启
  • 第一台不够用时,容易复制到第二台 VPS

要求

使用 RAM 足够容纳应用加 Postgres 的 VPS。应用 + 数据库 + 代理的现实下限是 2 GB 套餐。假定 Ubuntu 24.04。不要在没读它对 cgroups 做什么的情况下从随机 Snap 安装 Docker;下面的步骤使用 Docker 官方 apt 仓库。

  • Ubuntu 22.04/24.04,root 或 sudo
  • 推荐 2 GB RAM(没有 Postgres 的极小栈才用 1 GB)
  • 如果要自动 HTTPS,需要指向 VPS 的域名
  • Git 或其他把项目拷到服务器上的方式

步骤 1:安装 Docker Engine 和 Compose v2

Compose v2 是以 docker compose(带空格)调用的插件,不是旧的 docker-compose Python 二进制。两者都从 Docker 的仓库安装,以便拿到当前安全补丁。

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

步骤 2:部署用户和目录布局

以 root 跑 Compose 能用,但加入 docker 组的专用用户更干净。把项目放在 /opt 或 /srv,而不是 /root,这样备份和权限一目了然。永远不要让包含 .env 的目录对全世界可写。

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

步骤 3:写一份有生产意识的 Compose 文件

下面的文件是模板。把应用镜像换成你的。Postgres 不会发布到 0.0.0.0:5432——只有 Caddy 发布在 80 和 443。健康检查会阻止反向代理把流量发给仍在迁移数据库的应用。钉死镜像标签;latest 是意外损坏发生的方式。

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

步骤 4:密钥、.env 和 Caddyfile

把密码放在服务器上的 .env 里,chmod 600,并且永远不要提交那个文件。用 openssl rand -base64 32 生成密码。Caddyfile 只需要反向代理到 Docker 网络上的应用服务名。

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

步骤 5:防火墙、日志和资源限制

UFW 应该只允许 22、80 和 443。Docker 有时会为已发布端口绕过 UFW;如果你需要严格的主机防火墙,查阅你这版 Ubuntu 上当前的 Docker + UFW 交互,或只在 127.0.0.1 上发布端口并在前面放主机 Caddy。在 Compose 里设置内存限制,以免 Postgres 吃光 VPS。

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

步骤 6:更新和回滚

管用的无聊更新是:拉取新镜像,up -d,盯着健康检查,把上一个标签留在 git 里以便钉回去。除非你打算停止接收流量,否则不要在生产数据库上 docker compose down。down -v 会删除卷——那是擦除,不是重启。

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

备份命名卷

VPS 快照很好。如果你需要还原到另一台机器,逻辑 Postgres 转储更好。用 cron 调度。真正测一次还原,否则你没有备份——你只有希望是备份的文件。

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

故障排除

阅读 docker compose ps 和 docker compose logs service。镜像拉取失败通常是 ghcr/Docker Hub 速率限制,或没有登录的私有镜像。Caddy 的 502 意味着应用不健康,或代理主机名错了(从 Caddy 容器内部使用 Compose 服务名,而不是 localhost)。卷上的权限错误往往意味着镜像以 uid 1000 运行,而主机目录是 root。

  • compose: command not found — 你装的是旧二进制名;使用 docker compose
  • port is already allocated — 别的东西占用了 80/443(Apache、Nginx、另一个 Caddy)
  • database connection refused — 应用在 Postgres 就绪前启动;使用 healthcheck + depends_on condition
  • 磁盘满 — docker system df,然后谨慎地 prune 未使用镜像
  • docker.sock 上 permission denied — 用户不在 docker 组,或需要重新登录会话

安全

不要发布数据库端口。不要运行 privileged: true。除非你故意在写容器管理器,否则不要把 /var/run/docker.sock 挂进应用容器。保持引擎更新。如果自己构建,扫描自己的镜像。磁盘上的 .env 仍是密钥文件——限制谁能 SSH 到这台机器。

  • 没有公开的 5432 / 3306 / 6379
  • 钉死镜像摘要,或至少不可变标签
  • chmod 600 .env
  • 主机 OS 的 unattended-upgrades 仍然重要
  • 每个应用一个 Compose 项目,让爆炸半径更小

提示

  • 把 compose.yaml 放进 git;.env.example 不含真实密钥
  • 用 profiles 做可选 worker(docker compose --profile workers up -d)
  • 给 VPS 定规格时看 docker stats
  • 绑定挂载源代码不是生产模式——烤进镜像
  • 栈长到几台 VPS 时再看编排——之前不必

一台 VPS 上接近生产的 Compose 配置是:官方 Docker 软件包、锁定的 .env、不发布数据库的 YAML 文件、健康检查、80/443 上的反向代理、UFW、可以回滚的镜像标签,以及你真正还原过一次的 Postgres 转储。从本文模板开始,替换应用镜像,并把 docker compose down -v 当成和 rm -rf 同等尊重的破坏性命令。