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

Nextcloud VPS Docker Compose、TLS 與備份設定

學會在 VPS 以 Docker Compose 部署 Nextcloud,搭配 Postgres、Redis 與 nginx TLS reverse proxy,並掌握可還原備份、升級及記憶體規劃。

實際上要建置的內容

本指南會在 VPS 上使用 Docker Compose 執行 Nextcloud,在前方設定 Let's Encrypt TLS,並建立確實能還原的備份。架構包含 4 個容器和 1 個 proxy:官方 nextcloud 映像檔在 loopback 上監聽;Postgres 儲存所有檔案中繼資料;Redis 儲存檔案鎖定資訊;第 2 份 Nextcloud 映像檔只執行 cron 迴圈;nginx 則在主機上終止 TLS,位於所有元件之前。安裝本身只需 20 分鐘,但這不是重點。第 1 小時內做出的 2 個決定,會決定 1 年後是否仍保有檔案:使用真正的資料庫而非 SQLite,以及將資料目錄、資料庫與 config.php 以一致的集合進行備份。

本指南假設使用 Ubuntu 24.04 LTS 或 Debian 13,並已從 Docker 自有的 repository 安裝 Docker Engine 與 Compose v2 plugin。DNS A 記錄(若使用 IPv6,還需 AAAA)也必須已將 cloud.example.com 指向 VPS。所有操作都需要由您控制的伺服器;若使用他人的 SaaS,就無法自行進行 TLS termination 和資料庫傾印。

記憶體用量:實際消耗記憶體的項目

Nextcloud 的記憶體用量主要由 3 個項目決定,而且這些項目嚴格來說都不是「Nextcloud」本身。

PHP worker。 -apache 映像會由 worker 程序處理每個並行請求,而每個 worker 都會載入 PHP 直譯器。每個 worker 的記憶體用量可能成長到 PHP_MEMORY_LIMIT,之後 PHP 才會終止該請求。最壞情況下,常駐記憶體大致等於「並行請求數 × 記憶體限制」;而桌面同步用戶端會為每位使用者開啟數個平行連線。決定上限的是並行量,而不是使用者數量。

資料庫。 Postgres 會為每個連線 fork 一個後端程序,並讓 shared buffer 常駐記憶體。其工作集大小取決於檔案數量,而不是位元組數:oc_filecache 會為每位使用者的每個檔案保存一列資料。十萬個小檔案所需的資料庫資源,比一百個大型檔案更高。

預覽產生。 產生縮圖時,系統會以完整解析度將來源影像解碼至記憶體。影片預覽會呼叫 ffmpeg。執行 occ preview:generate-all 會連續反覆產生這項記憶體尖峰,是讓小型 VPS 觸發 OOM killer 最常見的原因。

Redis 的資源用量相對較低。之後額外加入的元件,例如 Collabora、全文搜尋或防毒掃描器,都是具有獨立記憶體需求的常駐服務。啟用前,應先將它們納入容量規劃。

如果 RAM 不足,可調整以下設定:降低 PHP_MEMORY_LIMIT,限制 preview_max_xpreview_max_ypreview_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,並將 secret 存放在同層的 .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:

固定 major tag。複製 31 前,先在 Docker Hub 確認目前使用的 tag。未來某次 docker compose pull 中,latest 可能會讓版本跨越 major boundary,而 Nextcloud 不支援這種升級。

資料目錄刻意使用 bind mount,而非 named volume。能直接提供路徑給備份工具,比整潔更重要。使用 image 的 www-data UID 建立目錄,並設定 Nextcloud 要求的權限:

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

請注意 port publish:127.0.0.1:8080:80。Docker 會寫入 DNAT 規則,而這些規則會在封包進入 ufw 的 INPUT chain 前先行套用。直接使用 8080:80,會讓未加密的 Nextcloud 暴露在公用網際網路上,不論 ufw 的設定為何。繫結至 loopback 可避免服務暴露在公用介面上。如此一來,防火牆只需允許 proxy。若不希望 SSH 對整個網際網路開放,透過自架的 WireGuard VPN 連線至 VPS,即可將 port 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。首次啟動時,系統會將完整的應用程式樹複製到 volume,並執行安裝程式;在此程序完成前,container 不會回應任何請求。

TLS 與反向代理

從發行版套件庫安裝 nginx 和 certbot,建立包含正確 server_name 的純 port-80 server block,然後讓 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 重新導向,並安裝 systemd timer,讓 90-day 憑證自動續期。使用 systemctl list-timers | grep certbot 確認該 timer 存在。從未啟用的續期 timer 等同於一個 90-day fuse。

反向代理區塊本身如下:

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 和較長的 read timeout 可避免大型上傳在中途失敗。proxy_request_buffering off 會直接串流上傳內容,不會先將整個檔案暫存到 proxy 的磁碟。

對單一應用程式而言,在主機上執行 nginx 是最簡單可行的做法。如果 Nextcloud 要與其他容器共用 VPS,以 Docker Compose 執行 Traefik,作為多個應用程式的反向代理,可將路由與憑證發行移至容器 labels;相同的 client_max_body_size 與 timeout 注意事項也會以 middleware 和 transport 設定的形式再次出現。

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 gateway,也就是 172.x 中的某個位址。請找出實際的子網路:

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

將該 CIDR,或涵蓋它的 172.16.0.0/12,加入 TRUSTED_PROXIES。範圍設得過大,任何用戶端都可能偽造 X-Forwarded-For;設定錯誤時,每次登入看起來都會來自 gateway 位址,暴力破解防護會一次封鎖整個實例,而管理員總覽則會顯示 「反向代理標頭設定不正確,或您正從受信任的代理伺服器存取 Nextcloud。」

OVERWRITECLIURL 對 cron 容器很重要,因為它沒有傳入請求可用來推斷主機名稱。未設定時,背景工作會產生指向 localhost 的連結,電子郵件通知也會寄出無法使用的 URL。

背景工作:使用 cron,而不是 AJAX

Nextcloud 預設的工作執行器是 AJAX:只有有人載入頁面時,工作才會作為副作用執行。凌晨 04:00 沒有人瀏覽網站,因此垃圾桶到期清理、版本清理、預覽圖產生及聯盟重試都會停滯。最先出現的徵兆通常是資料目錄持續增長。上方的 cron 服務會針對相同的 volume 執行官方 /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>。建議為它建立 alias。

備份:三者都要,否則等於沒有備份

只備份檔案系統,還原後會得到損壞的執行個體。資料目錄保存檔案內容;Postgres 保存檔案快取、分享、使用者與應用程式狀態;config.php 保存資料庫認證、執行個體 ID 與密碼 salt。只還原檔案而沒有資料庫時,Nextcloud 無法看見這些檔案。只還原資料庫而沒有 config.php 時,Nextcloud 無法開啟資料庫。以較新的資料目錄搭配舊資料庫還原時,會得到指向已移動檔案的分享。

請在暫停變更的執行個體上,同時備份這三者:

#!/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 連接至物件儲存或第二台主機,是通常的做法;其重複資料刪除功能也比每晚建立 tarball 更適合處理資料目錄。從初始化儲存庫,到設定每晚計時器與進行還原演練,完整設定請參閱 使用 restic 進行 VPS 主機外備份

還原並不是單純反向執行備份流程。剛啟動的服務堆疊會執行安裝程式,並寫入全新的 config.php、新的執行個體 ID 與密碼 salt。若直接將傾印匯入這個新身分,會留下損壞的工作階段與分享權杖。請先依下列順序還原舊的身分:

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。映像的 entrypoint 會偵測到相較於現有資料較新的程式碼,並自行執行 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,且 connect() failed (111: Connection refused) while connecting to upstream 出現在 /var/log/nginx/error.log 中。nginx 在 127.0.0.1:8080 上沒有連線到任何服務。可能是容器仍在初始化(檢查 docker compose logs app)、容器已結束(docker compose ps),或 publish 行與 proxy_pass 埠不相符。使用 ss -ltnp | grep 8080 確認。

發生重新導向迴圈,或管理員總覽中出現「不安全」警告。 缺少 OVERWRITEPROTOCOL: https,或 TRUSTED_PROXIES 未包含 Docker gateway 子網路。請參閱上方的 proxy 章節。

LockedException: "files/..." is locked 設定 REDIS_HOST 後,映像檔會將 Redis 設為鎖定後端,因此很少會留下過期鎖定。未設定時,鎖定會儲存在資料庫資料表 oc_file_locks 中;若請求在寫入期間遭終止,資料列就會留在資料表中。先確認實際使用的是 Redis;occ config:system:get memcache.locking 應回傳 Redis 類別,再手動清除鎖定資料列。

「PHP memory limit 低於建議的 512MB。」 提高 PHP_MEMORY_LIMIT,然後重新建立容器。請記住,這會如何影響最壞情況下的上限。

大規模化後會出現的問題

第一個瓶頸是資料目錄超過磁碟區容量。在 VPS 上擴大磁碟區需要調整磁碟區大小,再擴充檔案系統。排定在磁碟使用率達到 100% 之前處理,會比事後處理容易得多。現在就設定磁碟使用量警示,不要等到之後才處理。

第二個瓶頸是 oc_filecache。檔案清單與同步掃描會隨資料列數增加而變慢。解決方式是處理資料庫:讓 Postgres 使用快速儲存,提供足夠的 shared memory,並透過保留設定清理垃圾桶與版本,避免資料無限累積。

第三個瓶頸是預覽產生工作與其他工作競爭資源。在小型主機上,應限制預覽提供者的範圍,且絕不要在工作時間執行 occ preview:generate-all。如果儲存內容主要是手機相機膠卷,這些縮圖工作應改由專用相片伺服器處理;PhotoPrism 與 Immich 在 RAM、手機應用程式及備份指令上的比較說明兩者與 Nextcloud 主機搭配時各自需要多少資源。

除此之外,實際情況是,額外功能需要使用獨立主機。Collabora 與全文搜尋都是常駐服務,各自有不同的記憶體需求。將它們放在同一台也存放檔案唯一副本的主機上,只會擴大故障影響範圍,沒有其他好處。如果你需要的額外功能是在瀏覽器中編輯文件,區分 OnlyOffice 與 Collabora 的供應商 RAM 最低需求及連線限制會決定 2 到 4 GB 的 VPS 是否能夠執行其中一個。當磁碟區不再適合目前的規模時,將檔案儲存移至相容 S3 的主要儲存體。但請注意,這會讓備份更困難,而不是更簡單:資料庫仍然保存中繼資料,必須與 bucket 同步傾印。

當執行個體開始為實際使用者提供服務後,將 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 shared buffers、每個連線各自的後端程序,以及預覽產生可能造成的尖峰用量。2 GB 的主機在限制預覽並加入 swap 的情況下,可以執行小型家庭環境;加入 Collabora 或全文搜尋後,還需要為另一組常駐服務預留資源。

為什麼大型上傳會在 nginx 反向代理後方失敗?

代理伺服器上的兩項設定通常就是原因:client_max_body_size 保持 1 MB 預設值時會截斷請求,而過短的 proxy_read_timeoutproxy_send_timeout 值會在傳輸中途終止長時間傳輸。將兩者都設為寬裕的值,將 proxy_request_buffering off 設為串流而不是先暫存,並提高應用程式容器中的 PHP_UPLOAD_LIMIT 以保持一致。

為什麼 Nextcloud 會在重新導向中形成迴圈,或警告反向代理設定?

容器在 127.0.0.1 看不到 nginx,而是看到 Docker bridge gateway,位置大約在 172.x。如果該位址未加入 TRUSTED_PROXIES,就會忽略 X-Forwarded-Proto: https 標頭,Nextcloud 會產生 http:// URL,代理伺服器便會將這些 URL 重新導回。將 TRUSTED_PROXIES 設為實際的 bridge subnet,並固定 OVERWRITEPROTOCOL: https

我可以將 Nextcloud 從 29 直接升級到 31 嗎?

不行。Nextcloud 每次升級只支援一個 major version。略過版本會因 Updates between multiple major versions and downgrades are unsupported. 而停止,並使執行個體進入維護模式。先備份,將 appcron 服務的 tag 各提升一個 major version,執行 docker compose pull && docker compose up -d,使用 occ status 驗證,然後重複此流程。