Appearance
NAS 自建 Nextcloud(LinuxServer.io)
在 Synology NAS 上以 LSIO(LinuxServer.io)映像部署 Nextcloud,搭配 MariaDB + Redis + Cloudflare Tunnel,含 External Storage 定時掃描排程 SOP。
概述
LinuxServer.io(LSIO)的 Nextcloud 映像是 NAS 上部署 Nextcloud 的熱門選擇:Alpine-based,體積小,且內建 nginx + php-fpm + crond,不需要額外的 web server 容器。相較於官方 FPM 映像需要自行組合多個容器,LSIO 版適合不需要 Memories 等進階相片功能、追求簡單架構與維護的場景。
核心架構決策:
External Storage 而非 data directory:NAS 共享資料夾(影片庫、備份等)透過 External Storage (Local) 掛載,讓 Nextcloud 明確知道這些是「外部管理的目錄」。直接放在 /data/ 下的話,其他服務同時寫入時 oc_filecache 與實際檔案容易不一致,風險較高。
定時全量掃描:External Storage 的外部新增/刪除/修改不會自動更新 Nextcloud 的檔案索引。--unscanned flag 只處理 filecache 中已有記錄但未完成掃描的條目,無法發現全新的外部檔案,必須使用不帶此 flag 的全量掃描。
Cloudflare Tunnel:不開任何 inbound port,透過 Tunnel 安全暴露,詳見 Cloudflare Tunnel x Synology NAS 架構指南。
核心內容
LSIO 映像特點
LSIO 的 Nextcloud 映像(lscr.io/linuxserver/nextcloud)使用 s6-overlay v3 管理程序。與官方映像的主要差異:
| 特性 | LSIO | 官方 FPM(Memories 方案) |
|---|---|---|
| 包含 nginx | ✅ 內建 | ❌ 需單獨容器 |
| 包含 crond | ✅ 內建(abc 使用者) | ❌(需另配) |
| PUID/PGID 支援 | ✅(適合 NAS 使用者映射) | — |
| occ wrapper | ✅ 已處理使用者切換 | ❌ 需 --user www-data |
| 支援 Memories + go-vod | ❌ | ✅ |
| 自訂初始化腳本 | /custom-cont-init.d/(容器根目錄) | — |
occ 反斜線加倍問題
LSIO 的 occ wrapper 內部再經一層 shell 解析,設定 memcache class 時反斜線必須加倍:
bash
# 正確(在 LSIO occ wrapper 環境中)
occ config:system:set memcache.local --value='\\OC\\Memcache\\APCu'
# 錯誤(反斜線被 shell 吃掉,config.php 寫入 OCMemcacheAPCu)
occ config:system:set memcache.local --value='\OC\Memcache\APCu'若誤用單反斜線,config.php 寫入 OCMemcacheAPCu(無效 class),後續所有 occ 指令連鎖失敗,需手動編輯 config.php 修復。
Chunked Upload 與 Cloudflare Free Plan 限制
Cloudflare Free plan 對單一 HTTP request body 上限 100MB。Nextcloud 預設 chunk 大小 100 MiB(≈ 104.86 MB),加上 headers 後超限,導致上傳失敗。需調整為 95 MiB(99614720 bytes)留出緩衝。Nextcloud 官方客戶端(Desktop/Mobile)使用應用層 chunked upload(每個 chunk 一個獨立 HTTP request),設定此值後任意大小檔案均可上傳。
External Storage 掃描排程架構
LSIO 的自訂初始化腳本放在容器根目錄的 /custom-cont-init.d/(不是 /config/custom-cont-init.d/)。s6-overlay v3 在每次容器啟動時執行這些腳本,時機在自身初始化之後、crond 啟動之前,確保排程在 crond 啟動時已就緒。
採「先刪後寫」策略:每次容器啟動都先移除舊排程再寫入最新版本,確保設定更新後立即生效,不會因為 marker 標記已存在而跳過。
關鍵要點
- LSIO occ wrapper 中反斜線需加倍(
\\OC\\Memcache\\APCu) - External Storage 只能用全量
files:scan(不帶--unscanned)偵測外部變更 custom-cont-init.d腳本放在容器根目錄/custom-cont-init.d/,不是/config/下- Cloudflare Free plan 100MB 限制需調整 chunk 大小為 95 MiB(99614720 bytes)
occ app:install只會回報 "already installed" 但不啟用,必須用occ app:enabletrusted_domains只需包含外部域名 + localhost + NAS IP:port(不需包含web)
實際應用
此部署適用於:
- Synology NAS 想運行個人雲端儲存,不需 AI 相片管理(純檔案同步)
- 需要將現有 NAS 共享資料夾(影片庫、備份等)透過 Nextcloud Web 存取
- 追求架構簡單、容器數量最少的方案
如需完整的相片管理(時間軸、地圖、AI 標籤、影片轉碼),請改用 Nextcloud + Memories 完整部署。
部署設定參考
以下為實際部署時使用的完整設定,供日後查詢與複製使用。
環境參數
| 項目 | 值 |
|---|---|
| 映像 | lscr.io/linuxserver/nextcloud:33.0.0 |
| 宿主機 | Synology NAS |
| NAS port | 1113:443(LSIO 內部 HTTPS,自簽憑證) |
| Cloudflare Tunnel | 獨立運行,連入 NAS IP:1113,SSL 模式 Full + No TLS Verify |
| External Storage 掃描頻率 | 每 15 分鐘全量掃描 |
docker-compose.yml(完整)
yaml
services:
# ============================================================
# Nextcloud (LSIO — 內建 nginx + php-fpm + cron)
# ============================================================
nextcloud:
image: lscr.io/linuxserver/nextcloud:33.0.0
container_name: nextcloud
environment:
- PUID=1026 # Synology 使用者 UID(用 id <username> 查詢)
- PGID=101 # Synology 使用者 GID
- TZ=Asia/Taipei
volumes:
- /volume1/docker/nextcloud/config:/config
- /volume1/docker/nextcloud/data:/data
# s6-overlay v3 custom init(容器啟動時自動執行)
- /volume1/docker/nextcloud/custom-cont-init.d:/custom-cont-init.d:ro
# External Storage — 掛到 /mnt,不放在 /data 下
- /volume1/backup:/mnt/backup
- /volume1/video:/mnt/video
- /volume1/public:/mnt/public
ports:
- "1113:443"
depends_on:
- db
- redis
restart: unless-stopped
networks:
- backend
# ============================================================
# MariaDB
# ============================================================
db:
image: mariadb:lts
container_name: nextcloud-db
environment:
- MYSQL_ROOT_PASSWORD=<你的root密碼>
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=<你的nextcloud密碼>
- TZ=Asia/Taipei
volumes:
- /volume1/docker/nextcloud/mariadb/mysql:/var/lib/mysql
# 單機環境不需要 binlog(--log-bin / --binlog-format 已移除)
# innodb_file_per_table 已是 MariaDB 預設值
command: >
--transaction-isolation=READ-COMMITTED
--log-bin-trust-function-creators=1
--innodb-buffer-pool-size=512M
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
restart: unless-stopped
networks:
- backend
# ============================================================
# Redis(無密碼)
# ============================================================
redis:
image: redis:alpine
container_name: nextcloud-redis
command: redis-server
volumes:
- /volume1/docker/nextcloud/redis/data:/data
restart: unless-stopped
networks:
- backend
networks:
backend:
driver: bridgeExternal Storage 掃描腳本
路徑: /volume1/docker/nextcloud/config/custom-scripts/scan-external-storages.sh
bash
#!/bin/bash
# 掃描 External Storage,讓 Nextcloud 發現外部新增/修改/刪除的檔案
# 由 abc 使用者的 crontab 呼叫(LSIO 內建 crond 驅動)
#
# ⚠ 一律使用全量掃描(不帶 --unscanned)。
# --unscanned 只處理 filecache 中「已有記錄但標記為未完成」的條目,
# 不會走訪檔案系統發現「完全未知」的新檔案。對 External Storage 的
# 外部新增、修改、刪除皆無效。全量掃描開銷極低(數千檔案約數秒)。
# === External Storage 掛載路徑 ===
# 格式:<Nextcloud 使用者>/files/<掛載點名稱>
# 這是 Nextcloud 虛擬路徑,不是容器內檔案系統路徑(如 /mnt/backup)。
# 掛載點名稱可用 occ files_external:list 確認。
PATHS=(
"admin/files/Backup"
"admin/files/Video"
"admin/files/Public"
)
cd /app/www/public || exit 1
for p in "${PATHS[@]}"; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Scanning: $p (full)"
php occ files:scan --path="$p"
done掃描排程自動設定(custom-cont-init.d)
路徑: /volume1/docker/nextcloud/custom-cont-init.d/50-setup-scan-cron
bash
#!/bin/bash
# 容器啟動時自動將掃描排程加入 abc 使用者的 crontab。
# LSIO 的 custom-cont-init.d 在自身初始化之後、crond 啟動之前執行,
# 確保 crond 啟動時已包含掃描排程。
# 採用「先刪舊、再寫入」策略,確保每次容器啟動都套用最新排程設定。
CRONTAB="/etc/crontabs/abc"
MARKER="scan-external-storages"
# 移除舊的掃描排程與相關註解(若有),再重新寫入
sed -i "/$MARKER/d" "$CRONTAB" 2>/dev/null
sed -i '/External Storage.*掃描/d' "$CRONTAB" 2>/dev/null
cat >> "$CRONTAB" <<'CRON'
# External Storage 全量掃描(每 15 分鐘)
*/15 * * * * /config/custom-scripts/scan-external-storages.sh >> /config/log/nextcloud/scan-external.log 2>&1
CRON
echo "[custom-init] External Storage scan cron entries updated"
chmod +x /config/custom-scripts/scan-external-storages.sh 2>/dev/null
mkdir -p /config/log/nextcloud部署流程(Step by Step)
Step 1:建立目錄
bash
mkdir -p /volume1/docker/nextcloud/{config,data,mariadb/mysql,redis/data}
mkdir -p /volume1/docker/nextcloud/{custom-cont-init.d,config/custom-scripts}
chmod +x /volume1/docker/nextcloud/config/custom-scripts/scan-external-storages.sh
chmod +x /volume1/docker/nextcloud/custom-cont-init.d/50-setup-scan-cronStep 2:啟動
bash
cd /volume1/docker/nextcloud
docker compose up -d
docker compose logs -f # 等待初始化(約 30-60 秒),Ctrl+C 退出
docker compose ps # 確認三個服務皆 running瀏覽器開啟 https://<NAS-IP>:1113(接受自簽憑證警告),填入安裝精靈:
| 欄位 | 值 |
|---|---|
| 資料庫類型 | MySQL/MariaDB |
| 資料庫使用者 | nextcloud |
| 資料庫密碼 | compose 中的 MYSQL_PASSWORD |
| 資料庫名稱 | nextcloud |
| 資料庫主機 | db:3306 |
Step 3:occ 設定(進入容器)
bash
docker exec -it nextcloud bash
# 以下在容器內執行(LSIO 的 occ wrapper 已處理使用者切換,不需 --user)Trusted Domains & Reverse Proxy
bash
occ config:system:set trusted_domains 0 --value='cloud.yourdomain.com'
occ config:system:set trusted_domains 1 --value='localhost'
occ config:system:set trusted_domains 2 --value='<NAS-IP>:1113'
occ config:system:set overwriteprotocol --value='https'
occ config:system:set overwrite.cli.url --value='https://cloud.yourdomain.com'
occ config:system:set trusted_proxies 0 --value='172.16.0.0/12'
occ config:system:set forwarded_for_headers 0 --value='HTTP_CF_CONNECTING_IP'Redis(反斜線必須加倍!)
bash
occ config:system:set memcache.local --value='\\OC\\Memcache\\APCu'
occ config:system:set memcache.distributed --value='\\OC\\Memcache\\Redis'
occ config:system:set memcache.locking --value='\\OC\\Memcache\\Redis'
occ config:system:set redis host --value='redis'
occ config:system:set redis port --value=6379 --type=integerCloudflare Free Plan chunk 限制
bash
# Cloudflare Free plan 單一 request body 上限 100MB。
# Nextcloud 預設 chunk 100 MiB ≈ 104.86 MB,加 headers 後超限。
# 降至 95 MiB,留 ~5 MiB headroom。
occ config:system:set files.chunked_upload.max_size --value=99614720 --type=integer地區、時區、Cron、維護
bash
occ config:system:set default_phone_region --value='TW'
occ config:system:set default_timezone --value='Asia/Taipei'
occ background:cron
# 維護時段(UTC 19 = 台灣凌晨 3:00)
occ config:system:set maintenance_window_start --value=19 --type=integer
# 一次性維護指令
occ db:add-missing-indices
occ maintenance:repair --include-expensiveStep 4:設定 External Storage
bash
# 允許建立 Local 類型 External Storage
occ config:system:set files_external_allow_create_new_local --value=true --type=boolean
# 啟用功能(必須用 app:enable,不能用 app:install)
occ app:enable files_external
# 建立掛載點(每個指令回傳一個 mount ID)
occ files_external:create Backup local null::null --config datadir=/mnt/backup
occ files_external:create Video local null::null --config datadir=/mnt/video
occ files_external:create Public local null::null --config datadir=/mnt/public
# 確認掛載(記下 ID,後續管理用)
occ files_external:list
exitbash
# 首次全量掃描(讓 Nextcloud 發現既有檔案)
docker exec nextcloud occ files:scan --allStep 5:確認掃描排程
bash
docker exec nextcloud grep scan /etc/crontabs/abc
docker exec nextcloud /config/custom-scripts/scan-external-storages.sh # 手動測試
docker exec nextcloud cat /config/log/nextcloud/scan-external.logconfig.php 關鍵設定參考
php
<?php
$CONFIG = array (
'trusted_domains' => array (
0 => 'cloud.yourdomain.com',
1 => 'localhost',
2 => '<NAS-IP>:1113',
),
'trusted_proxies' => array (
0 => '172.16.0.0/12',
),
'forwarded_for_headers' => array (
0 => 'HTTP_CF_CONNECTING_IP',
),
'overwriteprotocol' => 'https',
'overwrite.cli.url' => 'https://cloud.yourdomain.com',
'default_phone_region' => 'TW',
'default_timezone' => 'Asia/Taipei',
'maintenance_window_start' => 19, // UTC 19 = 台灣 03:00
'files.chunked_upload.max_size' => 99614720,
'files_external_allow_create_new_local' => true,
'memcache.local' => '\\OC\\Memcache\\APCu',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' => array (
'host' => 'redis',
'port' => 6379,
),
);常用維運指令
bash
# 服務狀態
docker compose ps
# Logs
docker compose logs -f nextcloud
# occ(LSIO 已處理使用者切換,不需 --user)
docker exec -it nextcloud occ [command]
# 掃描特定路徑 / 全部
docker exec nextcloud occ files:scan --path="admin/files/Backup"
docker exec nextcloud occ files:scan --all
# 查看掃描 log
docker exec nextcloud cat /config/log/nextcloud/scan-external.log
# 修復 / 更新 App
docker exec nextcloud occ maintenance:repair
docker exec nextcloud occ app:update --all
# 備份
docker exec nextcloud occ maintenance:mode --on
docker exec nextcloud-db bash -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" nextcloud' \
| gzip > backup_$(date +%Y%m%d).sql.gz
cp /volume1/docker/nextcloud/config/www/nextcloud/config/config.php ./config-backup.php
docker exec nextcloud occ maintenance:mode --off
# 升級(只能逐一大版本,如 32→33,不可跳版)
docker compose pull && docker compose up -d
# 臨時啟動 phpmyadmin(勿加入 Cloudflare Tunnel)
docker run --rm -p 8087:80 -e PMA_HOST=nextcloud-db \
--network nextcloud_backend phpmyadmin常見問題排查
| 問題 | 原因 | 解法 |
|---|---|---|
Access through untrusted domain | trusted_domains 缺少存取域名 | 加入外部域名 + localhost + NAS IP:port |
Memcache OCMemcacheAPCu not available(所有 occ 指令失敗) | occ 反斜線沒加倍 | 手動編輯 config.php 刪除壞值,重新以加倍反斜線設定 |
| 大檔上傳失敗(413) | Cloudflare 100MB 限制 | 確認 files.chunked_upload.max_size 為 99614720 |
| 外部檔案在 Nextcloud 看不到 | 需等掃描排程(15 分鐘) | docker exec nextcloud occ files:scan --path="..." 手動觸發 |
| HSTS 標頭警告 | Cloudflare 邊緣已加 HSTS,Nextcloud nginx 未加 | 在 nginx site-conf 加 HSTS header,docker compose restart nextcloud |
@eaDir / #recycle Permission denied | Synology 系統目錄 ACL 保護 | 噪音,不影響其他檔案;已用 --path 精確掃描避開 |
相關概念
- Cloudflare Tunnel x Synology NAS 架構指南 — 外部安全存取的基礎架構
- Nextcloud + Memories 完整部署 — 需要 AI 相片管理時的進階部署方案(官方 FPM + go-vod)
- MariaDB Helm 部署(Kubernetes) — MariaDB 在 K8s 環境的部署參考
- Redis on Kubernetes 部署與維運 — Redis 在 K8s 環境的部署參考