VPS 架設 Nextcloud 教學:Docker Compose 與 TLS 設定
本文詳解如何在 VPS 上使用 Docker Compose 部署 Nextcloud,包含 Postgres 資料庫、Redis 快取與 Let's Encrypt TLS 設定。針對常見的 OOM killer 記憶體溢位問題,提供容量規劃建議與完整備份還原流程,確保您的雲端檔案安全無虞。
實際建置內容
本指南使用 Docker Compose 在 VPS 上執行 Nextcloud,並在前端配置 Let's Encrypt TLS,同時建立可成功還原的備份機制。架構包含四個容器與一個代理伺服器:官方 nextcloud 映像檔監聽 loopback;Postgres 儲存所有檔案中繼資料;Redis 處理檔案鎖定;第二個 Nextcloud 映像檔僅執行 cron 迴圈;最後由主機上的 nginx 負責所有前端的 TLS 終止。安裝過程約需 20 分鐘,但重點不在於此。在第一個小時內需做出兩項決定,這將決定一年後您是否還能保有檔案:使用真正的資料庫而非 SQLite,以及建立一個包含 data 目錄、資料庫與 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 workers。 -apache 映像檔透過持有 PHP 解譯器的 worker 處理程序來處理每個並行請求。每個 worker 的記憶體消耗在 PHP 終止請求前,最高可達 PHP_MEMORY_LIMIT。最糟情況下的常駐記憶體大約是 並行請求數 × 記憶體限制,且桌面同步用戶端會為每個使用者開啟數個並行連線。決定上限的是並行數,而非使用者數量。
資料庫。 Postgres 會為每個連線 fork 一個 backend,並保持 shared buffers 常駐。其工作集(working set)隨 檔案數量 增加,而非檔案大小:oc_filecache 為每個使用者的每個檔案存放一列資料。十萬個小檔案對資料庫的負擔,會比一百個大檔案更重。
預覽圖生成。 生成縮圖時,會將原始圖片以完整解析度解碼至記憶體中。影片預覽會呼叫 ffmpeg。執行 occ preview:generate-all 會連續且重複地觸發此記憶體激增,這是導致小型 VPS 觸發 OOM killer 最常見的原因。
Redis 的消耗相對較低。任何後續新增的組件——如 Collabora、全文檢索、防毒掃描器——都是具有獨立記憶體佔用空間的獨立常駐服務,應在啟用前納入容量規劃。
若 RAM 資源有限,可採取以下措施:降低 PHP_MEMORY_LIMIT、限制 preview_max_x / preview_max_y / preview_max_filesize_image、將 enabledPreviewProviders 縮減至實際瀏覽的格式,並設定 trashbin_retention_obligation 與 versions_retention_obligation 以防止資料目錄的體積無預警增長至檔案大小數倍。請建立 swap file。Swap 速度較慢,但升級過程中發生 OOM kill 的後果更嚴重。
Why SQLite breaks
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,並將 secrets 存放在同層級的 .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 之前,請固定主要版本標籤 (major tag),並至 Docker Hub 確認目前的版本。若未來 docker compose pull 發生主要版本變更,latest 會導致版本跳轉,而 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 鏈處理封包前被執行 — 若僅使用 8080:80,無論 ufw 設定為何,都會將未加密的 Nextcloud 暴露於公開網路。綁定至 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 的 plain port-80 server block,接著讓 certbot 進行重寫。HTTP-01 驗證機制、續期計時器(renewal timer)以及失敗模式,已在 issuing Let's Encrypt certificates with certbot and nginx on Ubuntu 24.04 中完整說明:
sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.comCertbot 會新增 ssl_certificate 行與 :80 → :443 重新導向,並安裝一個用於續期 90 天憑證的 systemd timer。使用 systemctl list-timers | grep certbot 確認其存在 —— 若未啟用續期計時器,憑證將在 90 天後失效。
代理區塊內容:
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 與長讀取逾時(long read timeouts)可防止大型上傳在傳輸中斷。proxy_request_buffering off 會直接串流上傳內容,而非先將整個檔案暫存至代理伺服器的磁碟。
若僅運行單一應用程式,使用主機上的 nginx 是最簡單的做法。若 Nextcloud 需要與其他容器共用 VPS,請參考 running Traefik as a Docker Compose reverse proxy for multiple apps,該方案將路由與憑證核發移至 container labels 中,且 client_max_body_size 與逾時相關問題會以 middleware 與 transport 設定的形式再次出現。
trusted_proxies and overwriteprotocol
這是大多數自架 Nextcloud 實例出錯的地方,且症狀與原因看似無關。
只有當請求來自 trusted_proxies 中列出的位址時,X-Forwarded-Proto: https 才會生效。若未生效,Nextcloud 會認為請求為 plain HTTP 並產生 http:// URL;接著 proxy 會將其重新導向至 HTTPS;瀏覽器跟隨導向;Nextcloud 再次產生 http://。這就是重新導向迴圈(redirect loop)。無論如何,OVERWRITEPROTOCOL: https 會固定使用該協定。
TRUSTED_PROXIES 的陷阱在於 Nextcloud 看到的位址 並非 127.0.0.1。nginx 執行於 host 並連接至已發布的 port,因此 container 看到的是 Docker bridge gateway —— 即 172.x 中的內容。請找出實際的 subnet:
docker network inspect nextcloud_default \
-f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'將該 CIDR(或包含它的 172.16.0.0/12)填入 TRUSTED_PROXIES。範圍設定過大會導致任何 client 都能偽造 X-Forwarded-For;設定錯誤則會導致所有登入請求都顯示來自 gateway 位址,進而觸發 brute-force protection 並封鎖整個實例,且 admin overview 會顯示 "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."
OVERWRITECLIURL 對於 cron container 至關重要,因為該 container 沒有傳入請求可用於推斷 hostname。若未設定,背景作業會產生指向 localhost 的連結,且電子郵件通知會發送無法使用的 URL。
Background jobs: cron, not AJAX
Nextcloud 預設的作業執行方式為 AJAX:作業會在使用者載入頁面時作為副作用執行。由於凌晨 04:00 通常沒有使用者瀏覽,導致垃圾回收、版本清理、預覽圖生成及 federated 重試作業停滯,首要徵兆是 data directory 持續增長。上述的 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>。建議設定 alias。
Backups: 三項要素,缺一不可
僅備份檔案系統無法還原至損壞的實例。data directory 存放數據位元組;Postgres 存放檔案快取、分享資訊、使用者與應用程式狀態;config.php 存放資料庫憑證、實例 ID 與密碼鹽值 (password salt)。若僅還原檔案而無資料庫,Nextcloud 將無法讀取檔案。若僅還原資料庫而無 config.php,則無法開啟資料庫。若將舊版資料庫還原至新版 data directory,分享資訊將指向已移動的檔案路徑。
請從靜止 (quiesced) 狀態的實例中備份以下三項:
#!/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/開啟維護模式 (Maintenance mode) 能確保資料庫 dump 與檔案副本同步。若跳過此步驟,可能會導致資料庫紀錄中包含 rsync 尚未處理的檔案。請注意,此指令碼會保留帶有時間戳記的資料庫 dump,但 data directory 僅保留一個滾動鏡像 (rolling mirror) —— rsync --delete 每次執行都會覆蓋它 —— 因此只有最新的 dump 才能與檔案副本對應。
接著將備份移出主機。備份與被備份對象存放在同一個 VPS 僅是副本而非備份。常見的做法是使用 restic 備份至物件儲存 (object storage) 或第二台主機,其重複資料刪除 (deduplication) 功能處理 data directory 的效能遠優於每日製作的 tarball。從儲存庫初始化到每日排程與還原演練的完整設定,請參閱 off-box VPS backups with restic。
還原並非單純的反向操作。全新啟動的堆疊會執行安裝程式並寫入全新的 config.php —— 即新的實例 ID 與密碼鹽值 —— 在此新身份上匯入 dump 會導致 Session 與分享權杖 (share tokens) 失效。請依照下列順序先還原舊身份:
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 --allfiles:scan 會將檔案快取與磁碟上的實際內容進行同步。在實際需要之前,請先在備用 VPS 上進行一次演練。
Upgrades: one major at a time
Nextcloud 僅支援一次升級一個主要版本。若直接從 29 升級至 31 會導致失敗,系統會出現 Exception: Updates between multiple major versions and downgrades are unsupported. 並進入維護模式。
Docker 升級步驟如下:先進行備份,將 app 與 cron 服務中的 tag 從 31 修改為 32,接著執行 docker compose pull && docker compose up -d,然後執行 docker compose logs -f app。Image entrypoint 會針對現有資料偵測新版程式碼並自動執行 occ upgrade。請勿中斷此程序。待 logs 停止輸出後,執行 docker compose exec -u www-data app php occ status 並檢查 versionstring 以及應用程式是否已重新啟用。
兩項關鍵原則:一次僅升級一個主要版本並進行驗證,接著再升級下一個版本。此外,修改 app 服務的 tag 時,必須同步修改 cron 以保持一致;在同一個資料庫上運行兩個不同的 Nextcloud 版本會導致資料毀損。
您實際會看到的錯誤
"Your data directory is readable by other users. Please change the permissions to 0770." 綁定掛載(bind-mounted)的目錄具有群組或全域讀取權限。 sudo chmod 0770 /srv/nextcloud/data 與 sudo chown -R 33:33 /srv/nextcloud/data。
"Your data directory is invalid. Ensure there is a file called .ocdata in the root." 綁定掛載指向 Nextcloud 未曾初始化的位置——可能是路徑拼寫錯誤,或是原本運作中的實例被替換為全新的空目錄。請檢查主機路徑是否與 volume 行一致。
"Access through untrusted domain." 請求中的主機名稱不在 trusted_domains 中。NEXTCLOUD_TRUSTED_DOMAINS 僅適用於首次安裝;之後請設定為 live 模式: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),或是 publish 行與 proxy_pass 埠號不符。請使用 ss -ltnp | grep 8080 確認。
重新導向迴圈(redirect loop),或管理介面出現 "insecure" 警告。 缺少 OVERWRITEPROTOCOL: https,或 TRUSTED_PROXIES 未包含 Docker gateway 子網路。請參閱上方的代理(proxy)章節。
LockedException: "files/..." is locked。 若設定了 REDIS_HOST,映像檔會將 Redis 配置為鎖定(locking)後端,這能減少過期鎖定的發生。若未設定,鎖定資訊會儲存在資料庫表 oc_file_locks 中,且寫入中斷的請求會留下殘餘資料列。在手動清除鎖定資料列之前,請先確認 Redis 確實正在使用中——occ config:system:get memcache.locking 應回傳 Redis 類別。
"The PHP memory limit is below the recommended value of 512MB." 請調高 PHP_MEMORY_LIMIT 並重新建立容器。請注意這會影響您的最高上限。
規模擴張時的瓶頸
第一個瓶頸是資料目錄超過磁碟卷空間。在 VPS 上擴張磁碟卷需要進行 resize 並擴展檔案系統;在空間達到 100% 滿載時才處理會非常痛苦,因此請立即針對磁碟使用率設定警示。
第二個瓶頸是 oc_filecache。檔案列表與同步掃描的速度會隨資料列數量增加而變慢。解決方案是進行資料庫優化:將 Postgres 放在高速儲存裝置上,提供充足的 shared memory,並透過 retention settings 清理垃圾與舊版本,避免資料無限累積。
第三個瓶頸是預覽圖生成與其他程序競爭資源。在小型主機上,應限制預覽提供者(preview providers)的範圍,且絕不要在工作時間執行 occ preview:generate-all。
除此之外,坦白的說,額外功能需要獨立的機器。Collabora 與 full-text search 是獨立的常駐服務,擁有各自的記憶體需求;將它們與存放唯一檔案副本的主機放在一起,會增加故障範圍(failure domain)且無益。當磁碟卷不再適合使用時,請將檔案儲存移至 S3-compatible 主要儲存裝置——請注意,這會增加備份難度而非簡化:資料庫仍保有 metadata,必須與 bucket 同步進行 dump。
當實例開始為真實用戶服務時,請在前端部署 Uptime Kuma,以便在同步用戶端發現問題前收到停機通知。私有雲與 你自己的郵件伺服器 是絕佳組合;若不想手動串接服務,可以比較 Cloudron, CasaOS and Coolify 等自動化平台。
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 與每個連線對應的後端,以及預覽圖生成時的突發需求。若限制預覽圖生成並增加 swap,2 GB 的主機可運行小型家庭實例;若需加入 Collabora 或全文檢索,則需額外計算這些後端服務的常駐記憶體。
為什麼在 nginx 反向代理後,大型上傳會失敗?
通常由代理伺服器的兩項設定引起:client_max_body_size 若維持 1 MB 預設值會導致請求被截斷,而過短的 proxy_read_timeout / proxy_send_timeout 值會導致長時傳輸中途失敗。請將兩者設定為較大的數值,將 proxy_request_buffering off 設定為 stream 而非 spool,並同步調高應用程式容器的 PHP_UPLOAD_LIMIT。
為什麼 Nextcloud 會出現重定向迴圈或反向代理警告?
容器無法從 127.0.0.1 看到 nginx,它看到的是位於 172.x 的 Docker bridge gateway。當 TRUSTED_PROXIES 缺少該位址時,X-Forwarded-Proto: https 標頭會被忽略,導致 Nextcloud 產生 http:// URL,進而引發代理伺服器重定向迴圈。請將 TRUSTED_PROXIES 設定為實際的 bridge 子網路並固定 OVERWRITEPROTOCOL: https。
我可以直接將 Nextcloud 從 29 升級到 31 嗎?
不行。Nextcloud 每次升級僅支援一個主要版本。跳版本會導致 Updates between multiple major versions and downgrades are unsupported. 錯誤,使實例進入維護模式。請先進行備份,將 app 與 cron 服務的標籤各提升一個主要版本,執行 docker compose pull && docker compose up -d,透過 occ status 驗證,然後重複此步驟。