这篇记录一下我在一台 VPS 上用 宿主机 Nginx + Docker Compose 部署 Nextcloud(FPM) 的过程,以及中间踩到的几个典型坑。
0. 选择 Nextcloud 的原因
- 自托管:数据和账号完全自己掌控,比第三方网盘更安心。
- 生态完整:Web / Desktop / Mobile 都有客户端,WebDAV 也可用,适合 Obsidian 等同步场景。
- 可扩展:后续可以加 Redis、对象存储(S3)、外部存储、OnlyOffice(资源够的话)等。
1. Docker Compose 配置
Nginx 跑在宿主机(因为可能还要代理别的服务),Docker 里只跑 Nextcloud 的核心组件:
nextcloud:fpmmariadbrediscron
同时为了安全:
- FPM 只监听
127.0.0.1:9000(外网不可直接访问) - DB/Redis 不暴露端口
首先建立nextcloud目录和权限
mkdir -p /opt/nextcloud/{html,data,db,redis}
chown -R www-data:www-data /opt/nextcloud
docker-compose.yml:
services:
db:
image: mariadb:11
restart: unless-stopped
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
environment:
- MYSQL_ROOT_PASSWORD=...
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=...
volumes:
- /opt/nextcloud/db:/var/lib/mysql
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
app:
image: nextcloud:fpm
restart: unless-stopped
depends_on:
- db
- redis
ports:
- "127.0.0.1:9000:9000"
environment:
- MYSQL_HOST=db
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=...
- REDIS_HOST=redis
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=...
- NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
- PHP_MEMORY_LIMIT=512M
- PHP_UPLOAD_LIMIT=2G
volumes:
- /opt/nextcloud/html:/var/www/html
- /opt/nextcloud/data:/var/www/html/data
cron:
image: nextcloud:fpm
restart: unless-stopped
depends_on:
- app
volumes:
- /opt/nextcloud/html:/var/www/html
- /opt/nextcloud/data:/var/www/html/data
entrypoint: /cron.sh
为什么用 FPM:因为我想用宿主机 Nginx 统一反代/证书/路由,而不是让容器里再跑一层 Apache/Nginx。
部署:
docker compose up -d
docker compose psbash
docker compose logs --tail=120 app
可以检查(一般需要等一会因为这个versioncheck文件要等nextcloud脚本构建完才会显示(不是容器))
ls -la /opt/nextcloud/html/lib/versioncheck.php
2. Nginx 配置
实际上可以直接参考官方给的nginx: https://docs.nextcloud.com/server/stable/admin_manual/installation/nginx.html
- 静态资源(CSS/JS/图片等)优先走 Nginx 直接读磁盘:更快
- 找不到静态资源时 回退到 Nextcloud(index.php):因为 Nextcloud 有些资源是动态生成的(例如 theming 的 CSS)
- 仅允许 Nextcloud 的“入口 PHP 文件”走 FPM(
index.php/remote.php/...),其它.php一律 404 remote.php必须可用:否则 WebDAV(/remote.php/dav/...)会 404,文件列表会出各种“空/未找到文件夹”的表现
一个可用的SSL站点配置:
server {
listen 80;
server_name cloud.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cloud.example.com;
ssl_certificate /etc/nginx/ssl/cloud.example.com.pem;
ssl_certificate_key /etc/nginx/ssl/cloud.example.com.key;
root /opt/nextcloud/html;
index index.php;
client_max_body_size 2G;
location = /.well-known/carddav { return 301 /remote.php/dav; }
location = /.well-known/caldav { return 301 /remote.php/dav; }
# 禁止访问敏感目录/文件(避免直接读到代码/配置)
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { deny all; }
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { deny all; }
# 仅允许 Nextcloud “入口 PHP” 走 FPM(remote.php 对 DAV 很关键)
location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
include fastcgi_params;
# 关键点:FPM 在容器里,SCRIPT_FILENAME 必须是容器内路径
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTPS on;
fastcgi_read_timeout 300;
fastcgi_pass 127.0.0.1:9000;
}
# 静态资源:存在就直接返回;不存在则交给 Nextcloud(用于 theming CSS / 动态资源)
location ~* \.(?:css|js|mjs|wasm|svg|gif|png|jpg|jpeg|ico|webp|woff2?|ttf|otf|eot|map|webm|mp4)$ {
try_files $uri /index.php$request_uri;
expires 6M;
access_log off;
}
# 页面路由:不要用 $uri/,否则会把 /apps/dashboard/ 当目录,触发 403
location / {
try_files $uri /index.php$request_uri;
}
# 其它 php 禁止(安全)
location ~ \.php(?:$|/) { return 404; }
}
3. 常见Bug
Bug A:CSS/JS 404 或页面只剩一个“Nextcloud”标题
- 现象:HTML 能出来,但大量
/core/css/...、/core/js/...失败 - 根因:宿主机 Nginx 负责静态资源,如果
root不对、或宿主机没有完整的 Nextcloud 代码目录,就会 404 - 解决:确认
/opt/nextcloud/html有完整源码,并且 Nginxroot指向它
Bug B:/index.php/index.php/... 内部重定向循环(500)
- 现象:Nginx 日志出现
rewrite or internal redirection cycle ... /index.php/index.php/... - 根因:配置里把已经带
/index.php/...的请求再次拼接进/index.php$request_uri - 解决:让入口 PHP(
location ~ ^/(index|remote|...)\.php)优先匹配,且try_files/fallback 写法不要重复拼接导致循环
Bug C:WebDAV 404,Files 页面“空/未找到文件夹”
- 现象:
/remote.php/dav/...404(浏览器 Network 里能看到 REPORT/PROPFIND) - 根因:Nginx 没把
remote.php这个入口交给 FPM,直接当静态/目录处理了 - 解决:入口 PHP 的正则里必须包含
remote.php,并正确设置PATH_INFO
Bug D:ESM 模块加载失败:.mjs 返回 application/octet-stream
- 现象:
Failed to load module script ... MIME type application/octet-stream - 根因:老版本 Nginx 的
/etc/nginx/mime.types可能没有mjs映射,导致.mjs默认变成 octet-stream - 解决:在
/etc/nginx/mime.types里把application/javascript扩展补上mjs(并 reload nginx)