SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-22

VPS 用 Docker 部署 Nextcloud:TLS 与备份

学习在 VPS 上用 Docker Compose 部署 Nextcloud,配置 Postgres、Redis 和 TLS 反向代理,并通过实际恢复验证备份与升级流程,确保数据可恢复。

实际要构建的内容

本指南使用 Docker Compose 在 VPS 上运行 Nextcloud,在前端配置 Let's Encrypt TLS,并设置一个经过实际恢复验证的备份方案。整体包含 4 个容器和一个代理:官方 nextcloud 镜像仅监听 loopback;Postgres 保存所有文件元数据;Redis 保存文件锁;第二个 Nextcloud 镜像副本只运行 cron 循环;主机上的 nginx 在所有组件前终止 TLS。安装本身只需 20 分钟,但这不是重点。开始后的第一个小时内有两个决定会影响您一年后是否还能保留文件:使用真正的数据库而不是 SQLite;将数据目录、数据库和 config.php 作为一个一致的数据集一起备份。

本指南假设您使用 Ubuntu 24.04 LTS 或 Debian 13,已从 Docker 官方软件源安装 Docker Engine 和 Compose v2 插件,并且 DNS 的 A 记录(如果使用 IPv6,还包括 AAAA)已经将 cloud.example.com 指向 VPS。所有操作都需要一台由您控制的服务器;您无法在他人的 SaaS 上执行 TLS 终止和数据库转储。

内存规划:实际消耗内存的部分

Nextcloud 的内存使用主要由三部分决定,而且这些部分都不能简单称为“Nextcloud”。

PHP 工作进程。 -apache 镜像通过工作进程处理每个并发请求,每个工作进程都包含一个 PHP 解释器。PHP 终止请求前,每个工作进程最多可增长到 PHP_MEMORY_LIMIT。最坏情况下,常驻内存大致为并发请求数 × 内存限制;桌面同步客户端会为每个用户打开多个并行连接。决定上限的是并发数,而不是用户数。

数据库。 Postgres 为每个连接派生一个后端进程,并让共享缓冲区常驻内存。其工作集规模取决于文件数量,而不是字节数:oc_filecache 为每个用户的每个文件保存一行记录。十万个小文件会比一百个大文件产生更重的数据库负载。

预览生成。 生成缩略图时,系统会将源图像按完整分辨率解码到内存中。视频预览会调用 ffmpeg。运行 occ preview:generate-all 会连续重复产生这种内存峰值,这是导致小型 VPS 被 OOM killer 终止的最常见原因。

Redis 的内存开销相对较低。之后添加的任何组件,例如 Collabora、全文搜索或防病毒扫描器,都是具有独立内存占用的常驻服务。启用这些组件前,应将其纳入内存规划。

如果内存紧张,可以调整以下参数:降低 PHP_MEMORY_LIMIT,限制 preview_max_x / preview_max_y / preview_max_filesize_image,将 enabledPreviewProviders 缩减为实际浏览的格式,并设置 trashbin_retention_obligationversions_retention_obligation,避免数据目录在不知不觉中增长到文件总大小的数倍。添加 swap 文件。swap 速度较慢,但升级过程中发生 OOM kill 更糟糕。

SQLite 为什么会导致故障

Nextcloud 支持 SQLite,官方镜像也会直接使用它。不要这样做。SQLite 通过整个数据库文件级别的锁来串行处理写入:整个文件同一时间只能有一个写入者。Nextcloud 会持续写入文件锁、活动记录、缓存条目和作业状态;单个桌面客户端同步目录树时,也会发起许多并行请求。在这种负载模式下,您会遇到 SQLSTATE[HY000]: General error: 5 database is locked 和 HTTP 500 错误,而且故障通常恰好出现在实例开始真正投入使用时。

之后可以使用 occ db:convert-type 进行转换,但这需要在活动数据集上执行一次耗时且不可分阶段的迁移。请一开始就使用 Postgres 或 MariaDB。

Compose 文件

将以下内容保存到 /srv/nextcloud/compose.yaml,并将密钥保存在同级的 .env 文件中,文件权限设置为 600

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}

  app:
    image: nextcloud:31-apache
    restart: unless-stopped
    depends_on: [db, redis]
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
      TRUSTED_PROXIES: 172.16.0.0/12
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://cloud.example.com
      APACHE_DISABLE_REWRITE_IP: "1"
      PHP_MEMORY_LIMIT: 512M
      PHP_UPLOAD_LIMIT: 10G

  cron:
    image: nextcloud:31-apache
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on: [db, redis]
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data

volumes:
  db:
  html:

固定主版本标签。在原样复制 31 前,先在 Docker Hub 上确认当前使用的标签。未来某个 docker compose pull 中,latest 会将你升级到其他主版本,而 Nextcloud 不支持这种升级。

数据目录故意使用绑定挂载,而不是命名卷:备份工具可以直接访问的路径比整洁的目录结构更重要。使用镜像的 www-data UID 创建该目录,并设置 Nextcloud 要求的权限:

sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/data

注意端口发布配置:127.0.0.1:8080:80。Docker 会通过写入 DNAT 规则来发布端口,这些规则在数据包到达 ufw 的 INPUT 链之前就已生效。仅使用 8080:80 会让未加密的 Nextcloud 暴露在公网,无论 ufw 的规则如何配置。绑定到 loopback 可使其无法通过公网接口访问。这样,防火墙只需允许代理访问。如果不想让 SSH 对整个互联网开放,可以通过自托管的 WireGuard VPN 访问 VPS,从而完全将端口 22 从公网规则中移除:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

使用 docker compose up -d 启动,然后监控 docker compose logs -f app。首次启动会将完整的应用程序目录复制到卷中,并运行安装程序;在此过程完成前,容器不会响应任何请求。

TLS 与反向代理

从发行版安装 nginx 和 certbot,创建包含正确 server_name 的普通 80 端口 server 块,然后让 certbot 重写该配置。HTTP-01 challenge 的工作机制、续期计时器和故障模式已在 在 Ubuntu 24.04 上使用 certbot 和 nginx 签发 Let's Encrypt 证书 中完整介绍:

sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.com

Certbot 会添加 ssl_certificate 行和 :80:443 重定向,并安装一个用于续期 90-day 证书的 systemd timer。使用 systemctl list-timers | grep certbot 确认该 timer 存在;从未启用的续期 timer 就像一根 90-day 引线。

反向代理块如下:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name cloud.example.com;

    # certbot manages ssl_certificate / ssl_certificate_key here

    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

    client_max_body_size 10G;
    client_body_timeout 300s;

    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

在 nginx 1.25 及更高版本中,添加 http2 on;。Ubuntu 24.04 提供的版本较旧,对应配置项是 listen 443 ssl http2;nginx -t 会告诉您当前版本接受哪一种配置项。

client_max_body_size 和较长的读取超时可避免大文件上传中途失败。proxy_request_buffering off 会直接流式传输上传内容,而不是先将整个文件暂存到代理服务器的磁盘上。

对于单个应用,在主机上运行 nginx 是最简单的可行方案。如果 Nextcloud 要与其他容器共享 VPS,使用 Docker Compose 运行 Traefik,为多个应用提供反向代理 可将路由和证书签发移入容器标签中;相同的 client_max_body_size 和超时问题会以中间件和传输设置的形式再次出现。

trusted_proxies 和 overwriteprotocol

大多数自托管 Nextcloud 实例的问题都出在这里,而且症状通常看起来与原因无关。

只有当请求来自 trusted_proxies 中列出的地址时,X-Forwarded-Proto: https 才会生效。未生效时,Nextcloud 会认为请求使用的是普通 HTTP,并生成 http:// URL;代理会将这些 URL 重定向到 HTTPS;浏览器跟随重定向;Nextcloud 再次生成 http://。这就形成了重定向循环。OVERWRITEPROTOCOL: https 始终固定使用指定的协议。

TRUSTED_PROXIES 中的陷阱在于,Nextcloud 看到的地址并不是 127.0.0.1。nginx 在主机上运行,并连接到已发布的端口,因此容器看到的是 Docker bridge 网关,通常位于 172.x 中。查找实际子网:

docker network inspect nextcloud_default \
  -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

将该 CIDR(或覆盖它的 172.16.0.0/12)加入 TRUSTED_PROXIES。范围设置过宽,任何客户端都可能伪造 X-Forwarded-For;设置错误,则所有登录都会显示为来自网关地址,暴力破解防护会立即阻止整个实例,管理概览还会显示 “反向代理标头配置不正确,或者您正在从受信任的代理访问 Nextcloud。”

OVERWRITECLIURL 对 cron 容器很重要,因为它没有传入请求可用于推断主机名。没有该设置,后台作业会生成指向 localhost 的链接,电子邮件通知也会发送无法使用的 URL。

后台任务:使用 cron,而不是 AJAX

Nextcloud 默认的任务运行器是 AJAX:用户加载页面时,任务才会作为副作用执行。04:00 通常没有人浏览页面,因此回收站过期清理、版本清理、预览生成和联合云重试都会停滞。最先出现的症状通常是数据目录持续增长。上面的 cron 服务会针对相同的卷运行官方 /cron.sh 循环。告诉 Nextcloud 使用该方式:

docker compose exec -u www-data app php occ background:cron

每条 occ 命令都遵循这一格式:docker compose exec -u www-data app php occ <command>。建议为其设置别名。

备份:三者缺一不可

仅备份文件系统会将数据恢复到损坏的实例。数据目录保存文件内容;Postgres 保存文件缓存、共享、用户和应用状态;config.php 保存数据库凭据、实例 ID 和密码盐。只恢复文件而不恢复数据库,Nextcloud 将无法识别这些文件。只恢复数据库而不恢复 config.php,它将无法打开数据库。使用旧数据库恢复到较新的数据目录上,会得到指向已移动文件的共享。

请在实例处于静默状态时备份这三者:

#!/usr/bin/env bash
set -euo pipefail
cd /srv/nextcloud
DEST="/var/backups/nextcloud/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"

occ() { docker compose exec -T -u www-data app php occ "$@"; }

occ maintenance:mode --on
trap 'occ maintenance:mode --off' EXIT

docker compose exec -T db \
  pg_dump -U nextcloud --clean --if-exists nextcloud | gzip > "$DEST/db.sql.gz"

docker compose exec -T app \
  tar -C /var/www/html -cf - config custom_apps themes > "$DEST/app.tar"

rsync -a --delete /srv/nextcloud/data/ /var/backups/nextcloud/data/

维护模式可确保转储和文件副本彼此一致。跳过这一步,最终可能会捕获一个引用了某个文件的数据库,但 rsync 尚未复制到该文件。请注意,该脚本会保留带时间戳的数据库转储,但数据目录只有一个循环镜像;rsync --delete 每次运行都会覆盖它。因此,只有最新的转储与文件副本匹配。

然后将备份移出服务器。备份与被备份对象位于同一 VPS 上时,它只是副本,不是备份。将 restic 连接到对象存储或第二台主机通常是标准做法,其去重功能处理数据目录的效果远好于每晚生成一个 tar 包。完整配置过程包括初始化存储库、设置每晚定时器和执行恢复演练,详见使用 restic 进行 VPS 异机备份

恢复并不是简单地反向执行。刚启动的堆栈会运行安装程序,并写入全新的 config.php、新的实例 ID 和密码盐。在这个新身份上直接导入转储,会导致会话和共享令牌损坏。请先按以下顺序恢复旧身份:

docker compose up -d && docker compose stop app cron    # create the volumes, then halt the app
sudo rsync -a --delete /var/backups/nextcloud/data/ /srv/nextcloud/data/
docker compose run --rm -T --entrypoint "" app \
  tar -C /var/www/html -xf - < app.tar                  # the original config.php returns
gunzip -c db.sql.gz | docker compose exec -T db psql -U nextcloud -d nextcloud
docker compose start app cron
docker compose exec -T -u www-data app php occ maintenance:mode --off
docker compose exec -T -u www-data app php occ files:scan --all

files:scan 会将文件缓存与磁盘上的实际内容重新同步。在需要执行恢复前,先在备用 VPS 上演练一次。同样的磁盘文件与 Postgres 元数据分离关系适用于所有类似架构的应用。这也是只备份媒体库而未备份数据库的 Immich 备份会恢复为空时间线的原因。

升级:每次只跨一个主版本

Nextcloud 每次只支持升级一个主版本。从 29 直接跳到 31 不会正常失败,而是以 Exception: Updates between multiple major versions and downgrades are unsupported. 失败,并使系统停留在维护模式。

Docker 升级步骤是:先创建备份;在 appcron 服务中,将标签从 31 修改为 32;然后执行 docker compose pull && docker compose up -d,再执行 docker compose logs -f app。镜像入口点会检测现有数据对应的新代码,并自行运行 occ upgrade。不要中断此过程。日志停止输出后,执行 docker compose exec -u www-data app php occ status,检查 versionstring,并确认应用已重新启用。

以下两条规则可以避免问题:每次只升级一个主版本,验证无误后再升级下一个主版本。切勿只修改 app 服务的标签而不同时修改 cron;让两个不同版本的 Nextcloud 连接同一个数据库可能导致数据损坏。

实际会看到的错误

“您的数据目录可被其他用户读取。请将权限修改为 0770。” 绑定挂载的目录设置了组用户或其他用户的读取权限位。sudo chmod 0770 /srv/nextcloud/datasudo chown -R 33:33 /srv/nextcloud/data

“您的数据目录无效。请确保根目录中存在名为 .ocdata 的文件。” 绑定挂载指向了 Nextcloud 从未初始化的位置,路径中存在拼写错误,或者工作实例下方被替换成了新的空目录。检查主机路径是否与 volume 行一致。

“通过不受信任的域访问。” 请求中的主机名不在 trusted_domains 中。NEXTCLOUD_TRUSTED_DOMAINS 仅适用于首次安装;之后应在线设置:occ config:system:set trusted_domains 1 --value=cloud.example.com

502 Bad Gateway,并在 /var/log/nginx/error.log 中显示 connect() failed (111: Connection refused) while connecting to upstream。nginx 未能在 127.0.0.1:8080 上连接到任何服务。可能是容器仍在初始化(检查 docker compose logs app),容器已退出(docker compose ps),或者发布配置行与 proxy_pass 端口不匹配。使用 ss -ltnp | grep 8080 确认。

发生重定向循环,或管理概览中出现“不安全”警告。 缺少 OVERWRITEPROTOCOL: https,或者 TRUSTED_PROXIES 不包含 Docker 网关子网。请参阅上面的代理部分。

LockedException: "files/..." is locked 设置 REDIS_HOST 后,镜像会将 Redis 配置为锁后端,过期锁很少出现。未设置时,锁会存储在数据库表 oc_file_locks 中;如果请求在写入过程中被终止,就会留下记录。清理锁记录前,请先确认实际使用的是 Redis;occ config:system:get memcache.locking 应返回 Redis 类。

“PHP 内存限制低于建议值 512MB。” 提高 PHP_MEMORY_LIMIT,然后重新创建容器。请记住这会如何影响最坏情况下的内存上限。

规模扩大后会出现什么问题

第一个瓶颈是数据目录超出卷容量。在 VPS 上扩容卷需要调整卷大小并扩展文件系统。提前安排操作远比等到磁盘使用率达到 100% 后再处理轻松得多。现在就对磁盘使用率配置告警,不要等到之后。

第二个瓶颈是 oc_filecache。文件列表和同步扫描会随着行数增加而变慢。解决方法是优化数据库:让 Postgres 使用高速存储,并为其提供足够的共享内存;通过保留设置清理回收站和文件版本,不要让它们无限累积。

第三个问题是预览生成会与其他任务争用资源。在小型服务器上,应限制预览提供程序的范围,并且绝不要在工作时间运行 occ preview:generate-all。如果存储内容主要是手机相册,那么缩略图处理应交给专用照片服务器。PhotoPrism 与 Immich 在内存、手机应用和备份命令方面的比较介绍了将任一服务部署在 Nextcloud 服务器旁边所需的成本。

除此之外,实际情况是,这些附加服务需要独立的服务器。Collabora 和全文搜索都是常驻服务,各自有不同的内存需求。将它们与唯一一份文件副本部署在同一台服务器上,只会扩大故障域,没有其他好处。如果你需要在浏览器中编辑文档,将 OnlyOffice 与 Collabora 区分开的厂商内存最低要求和连接数限制会决定一台 2 到 4 GB 的 VPS 是否能够运行其中一个。卷的容量和性能不再适用时,可以将文件存储迁移到兼容 S3 的主存储。但要注意,这会让备份更困难,而不是更简单:数据库仍保存元数据,并且必须与存储桶同步导出。

实例开始为真实用户提供服务后,应在其前方部署 Uptime Kuma,以便在同步客户端发现故障之前收到宕机通知。私有云适合搭配 自建邮件服务器。如果你不想手动连接各项服务,Cloudron、CasaOS 和 Coolify比较了能够自动完成这些工作的相关平台。如果下一步要部署自托管搜索引擎,应预期会遇到与上述问题不同的一类故障:SearXNG 的 429 错误可能由其自身的速率限制器触发,也可能是上游搜索引擎屏蔽了你的 VPS IP。只有日志能确定具体原因。

FAQ

我可以使用 SQLite 代替 Postgres 运行 Nextcloud 吗?

可以,官方镜像也支持这样配置,但单个桌面同步客户端发起并行请求后,会遇到 SQLSTATE[HY000]: General error: 5 database is locked 和 HTTP 500 错误。SQLite 会锁定整个数据库以执行写入,而 Nextcloud 会持续写入文件锁、活动记录和作业状态。建议从 Postgres 或 MariaDB 开始;虽然存在 occ db:convert-type,但在实时数据上执行迁移耗时较长,而且要么全部成功,要么全部回滚。

Nextcloud VPS 实际需要多少 RAM?

应根据并发量而不是用户数量规划资源。最坏情况下,常驻内存大致等于并发请求数乘以 PHP_MEMORY_LIMIT,再加上 Postgres 共享缓冲区、每个连接对应的一个后端进程,以及预览生成产生的内存峰值。如果限制预览生成并添加 swap,2 GB 的服务器可以运行小型家庭实例;如果再添加 Collabora 或全文搜索,就需要为另一组常驻服务规划内存。

为什么通过 nginx 反向代理上传大文件会失败?

通常是代理上的两个设置导致问题:保持默认 1 MB 的 client_max_body_size 会截断请求,而过短的 proxy_read_timeout / proxy_send_timeout 值会在传输过程中终止长连接。将这两个值调大,将 proxy_request_buffering off 设置为流式传输而不是先写入临时文件,并将应用容器中的 PHP_UPLOAD_LIMIT 调高到匹配的值。

为什么 Nextcloud 会陷入重定向循环,或提示反向代理配置有问题?

容器在 127.0.0.1 处看不到 nginx,而是看到 Docker bridge 网关,该地址通常位于 172.x 中。当该地址未加入 TRUSTED_PROXIES 时,X-Forwarded-Proto: https 请求头会被忽略,Nextcloud 会生成 http:// URL,代理随后又将这些 URL 重定向回来。将 TRUSTED_PROXIES 设置为实际的 bridge 子网,并固定 OVERWRITEPROTOCOL: https

我可以将 Nextcloud 从 29 直接升级到 31 吗?

不可以。Nextcloud 每次升级只支持跨越一个主版本;跳过版本会因 Updates between multiple major versions and downgrades are unsupported. 停止,并使实例进入维护模式。先备份,然后在 appcron 服务中将标签提升一个主版本,执行 docker compose pull && docker compose up -d,再使用 occ status 验证,之后重复此过程。