很多早期部署的 Nextcloud 都采用子目录模式。

例如:

https://<MAIN_DOMAIN>/nextcloud/

这种部署方式简单,而且可以和其他服务共用同一个主域名。

但随着 Nextcloud 和现代浏览器安全机制不断演进,子目录架构会逐渐暴露一些限制。

这次迁移最终把正式地址从:

https://<MAIN_DOMAIN>/nextcloud/

迁移为:

https://<CLOUD_DOMAIN>/

也就是独立子域名根路径部署。

整个过程涉及:

DNS
TLS
Nginx
Apache
Nextcloud
Cookie
WebDAV
CalDAV
CardDAV
.well-known
客户端
兼容跳转
Security Scan

并不是简单地修改一个域名字符串。


一、为什么要从子目录迁移到子域名

最直接的原因之一是:

__Host- Cookie

现代浏览器支持:

__Host-

Cookie 前缀。

它要求:

Secure
Path=/
不能设置 Domain

而如果 Nextcloud 长期运行在:

/nextcloud/

路径下,就很难完全满足:

Path=/

这一要求。

安全扫描因此一直存在一个:

__Host-Prefix

相关提示。

这意味着即使 HTTPS、HSTS、Content-Security-Policy 等其他配置全部正常,子路径本身仍然会限制部分 Cookie Hardening。

因此最终决定把 Nextcloud 放到独立子域名根目录。


二、DNS:先创建新的正式入口

首先创建:

<CLOUD_DOMAIN>

对应 DNS A 记录:

<CLOUD_DOMAIN> → <PUBLIC_IP>

TTL 采用普通值,例如:

3600

在真正修改 Web Server 之前,先确认:

DNS 已经能够正确解析

这样后续 TLS 和反向代理才能正常验证。


三、TLS:把新域名加入现有多域名证书

原系统使用一张包含多个服务域名的证书。

因此没有单独创建一套证书,而是把:

<CLOUD_DOMAIN>

加入现有 SAN 列表。

Certbot 命令形式类似:

certbot certonly --standalone \
  -d <MAIN_DOMAIN> \
  -d <SERVICE_1_DOMAIN> \
  -d <SERVICE_2_DOMAIN> \
  -d <CLOUD_DOMAIN> \
  --agree-tos \
  --email <ADMIN_EMAIL> \
  --cert-name <CERT_NAME> \
  --expand

证书成功签发后,不能只看 Certbot 输出。

还需要检查:

Certbot live 目录中的证书

和:

Nginx 实际加载的证书

是否已经是同一个新证书。

最终确认:

Serial 一致
SAN 中包含 <CLOUD_DOMAIN>

四、反向代理主机与应用主机保持职责分离

整个架构中存在两个角色。

前端:

<REVERSE_PROXY_HOST>

运行:

Nginx
Certbot

后端:

<NEXTCLOUD_HOST>

运行:

Apache
PHP-FPM
Nextcloud
Database

这两个角色在迁移中必须严格区分。

新域名首先在反向代理主机建立独立 Nginx Server Block。


五、没有把新域名永久塞进旧 Server Block

最开始可以临时让:

server_name <MAIN_DOMAIN> <CLOUD_DOMAIN>;

共同命中同一个 Server Block。

但最终没有保留这种结构。

而是为:

<CLOUD_DOMAIN>

建立独立配置。

形式类似:

server {
    listen 443 ssl http2;
    server_name <CLOUD_DOMAIN>;

    client_max_body_size 1G;

    access_log /var/log/nginx/cloud.access.log;
    error_log  /var/log/nginx/cloud.error.log;

    ssl_certificate     <FULLCHAIN_PATH>;
    ssl_certificate_key <PRIVATE_KEY_PATH>;

    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://<NEXTCLOUD_LAN_IP>;

        proxy_http_version 1.1;

        proxy_hide_header Upgrade;
        proxy_hide_header Connection;

        proxy_set_header Host              $http_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 https;
        proxy_set_header X-Forwarded-Host  $http_host;
        proxy_set_header X-Forwarded-Port  443;
        proxy_set_header X-Nginx-Proxy     true;

        proxy_redirect off;

        proxy_connect_timeout 3600s;
        proxy_send_timeout    3600s;
        proxy_read_timeout    3600s;
    }
}

这种结构和其他独立服务保持一致。

也避免以后维护:

主域名

时意外影响:

Nextcloud 子域名

六、没有额外覆盖 Nextcloud 自己的 Referrer-Policy

旧主域名 Nginx 配置中曾经存在类似:

add_header Referrer-Policy strict-origin-when-cross-origin always;

但新的 Nextcloud Server Block 没有继续加。

原因是 Nextcloud 自己已经发送:

Referrer-Policy: no-referrer

如果 Nginx 再加一层,就可能产生重复 Header。

因此这里选择让 Nextcloud 自己控制应用级 Security Header。


七、Apache 从 Alias 架构转为独立 VirtualHost

后端最初采用:

Alias /nextcloud "<NEXTCLOUD_ROOT>/"

也就是说:

http://<BACKEND>/nextcloud/

才对应 Nextcloud。

迁移时,在原有 Apache 配置基础上增加独立 VirtualHost:

<VirtualHost *:80>
    ServerName <CLOUD_DOMAIN>
    DocumentRoot <NEXTCLOUD_ROOT>

    SetEnvIf X-Forwarded-Proto https HTTPS=on

    <Directory <NEXTCLOUD_ROOT>/>
        Options +FollowSymlinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog <APACHE_CLOUD_ERROR_LOG>
    CustomLog <APACHE_CLOUD_ACCESS_LOG> combined
</VirtualHost>

随后执行:

apache2ctl configtest

确认:

Syntax OK

再通过:

apache2ctl -S

确认:

<CLOUD_DOMAIN>

已经作为独立 NameVirtualHost 生效。


八、Nextcloud 增加新 Trusted Domain

Nextcloud 自己也必须允许新的 Host。

例如:

trusted_domains:
  <BACKEND_LAN_IP>
  <MAIN_DOMAIN>
  <CLOUD_DOMAIN>

这里没有立即删除旧域名。

因为旧地址仍然需要保留一段时间作为兼容入口。


九、Nextcloud 正式 Canonical URL 改为新域名

随后更新:

sudo -E -u <WEB_USER> php occ \
config:system:set overwrite.cli.url \
--value='https://<CLOUD_DOMAIN>/'

再设置:

sudo -E -u <WEB_USER> php occ \
config:system:set htaccess.RewriteBase \
--value='/'

然后:

sudo -E -u <WEB_USER> php occ maintenance:update:htaccess

最终:

overwrite.cli.url = https://<CLOUD_DOMAIN>/
htaccess.RewriteBase = /

十、没有设置 overwritehost / overwriteprotocol / overwritewebroot

这次迁移并没有额外设置:

overwritehost
overwriteprotocol
overwritewebroot

原因是前端 Nginx 已经正确提供:

Host
X-Forwarded-Proto
X-Forwarded-Host
X-Forwarded-Port

Nextcloud 可以正确识别真实请求。

如果反向代理本身已经配置正确,再强制写:

overwritehost

反而会增加以后配置复杂度。


十一、.well-known 全部迁移到新地址

原有:

CalDAV
CardDAV
WebFinger
NodeInfo

也需要同步调整。

Apache 中最终类似:

Redirect 301 /.well-known/carddav \
https://<CLOUD_DOMAIN>/remote.php/dav

Redirect 301 /.well-known/caldav \
https://<CLOUD_DOMAIN>/remote.php/dav

Redirect 301 /.well-known/webfinger \
https://<CLOUD_DOMAIN>/index.php/.well-known/webfinger

Redirect 301 /.well-known/nodeinfo \
https://<CLOUD_DOMAIN>/index.php/.well-known/nodeinfo

之后从外部验证:

/.well-known/carddav
→ 301
→ https://<CLOUD_DOMAIN>/remote.php/dav

其他几个入口也得到正确跳转。


十二、旧 Alias 最终从后端彻底删除

迁移初期,为了避免一次变化过多,旧:

Alias /nextcloud "<NEXTCLOUD_ROOT>/"

仍然暂时存在。

当新域名经过完整验证之后,这条 Alias 被删除。

此后:

https://<CLOUD_DOMAIN>/nextcloud/

应该直接返回:

404

这一步很重要。

它意味着:

新域名只有一个正式根路径

而不是:

/
和
/nextcloud/

两套入口同时存在。


十三、旧公网地址没有立即删除,而是由 Nginx 保留 301

虽然后端已经删除旧 Alias,但前端反向代理仍然保留兼容跳转。

在:

<MAIN_DOMAIN>

对应 Server Block 中加入:

location = /nextcloud {
    return 301 https://<CLOUD_DOMAIN>/;
}

location ^~ /nextcloud/ {
    rewrite ^/nextcloud/(.*)$ \
        https://<CLOUD_DOMAIN>/$1 permanent;
}

这样:

https://<MAIN_DOMAIN>/nextcloud/

会变成:

301
https://<CLOUD_DOMAIN>/

而:

https://<MAIN_DOMAIN>/nextcloud/status.php

则变成:

301
https://<CLOUD_DOMAIN>/status.php

也就是保留后续路径。


十四、为什么兼容跳转只放在反向代理

兼容逻辑没有继续保留在后端 Apache。

这是有意设计。

最终结构变成:

公网旧 URL
    ↓
前端 Nginx 301
    ↓
新 URL
    ↓
后端 Apache
    ↓
Nextcloud

而不是:

前端跳一次
后端再判断一次旧路径
Nextcloud 再处理一次

这样职责更清晰。


十五、客户端迁移期间出现 Cookie 和 Session 错误

正式切换后,需要在很多位置更新地址:

浏览器书签
密码管理器
桌面客户端
手机客户端
WebDAV
CalDAV
CardDAV
自动化脚本
旧登录 Session

因此迁移后的短时间内,Nextcloud 日志出现:

Request does not pass strict cookie check

来自多个桌面客户端。

也出现:

Could not find the session token to renew

异常类型:

OC\Authentication\Exceptions\ExpiredTokenException

主要发生在:

Tasks
Calendar
/
login

几个请求上。

这些错误高度集中在很短时间窗口内。

随着浏览器重新登录、客户端服务器地址更新,这些错误停止继续产生。

因此最终确认它们属于客户端迁移阶段的旧 Cookie / Session 残留,而不是服务器配置错误。


十六、Cookie 变化是这次架构迁移最关键的验证之一

新域名根路径部署生效后,返回的 Cookie 类似:

__Host-nc_sameSiteCookielax=true
Path=/
Secure
HttpOnly

以及:

__Host-nc_sameSiteCookiestrict=true
Path=/
Secure
HttpOnly

这说明:

__Host-

Cookie 前缀已经真正满足规范要求。

此前因为:

/nextcloud/

子路径结构无法完全满足的条件,现在已经消失。


十七、Security Scan 最终从 A 变为 A+

旧地址进行 Nextcloud Security Scan 时,整体已经达到:

A

但仍存在:

__Host-Prefix

红色提示。

迁移后曾经一度错误地扫描:

https://<CLOUD_DOMAIN>/nextcloud/

因此仍然看到旧问题。

后来改为真正的:

https://<CLOUD_DOMAIN>/

重新扫描。

最终结果为:

A+

所有主要 Hardening 项全部绿色,包括:

__Host-Prefix

这也从外部验证了新的根域名架构已经正确生效。


十八、最终架构

迁移结束后的结构可以概括为:

https://<CLOUD_DOMAIN>/
        ↓
正式 Nextcloud

https://<CLOUD_DOMAIN>/nextcloud/
        ↓
404

https://<MAIN_DOMAIN>/nextcloud/
        ↓
301
        ↓
https://<CLOUD_DOMAIN>/

也就是说:

新域名根路径 = 唯一正式入口
旧域名子路径 = 临时兼容入口
新域名旧子路径 = 不存在

这是一个非常干净的状态。


十九、旧兼容地址不会永久存在

301 兼容跳转只是迁移期措施。

需要给:

浏览器
Desktop Client
Mobile Client
WebDAV
CalDAV
CardDAV
历史文档
自动化脚本

留出更新时间。

等迁移窗口结束之后,再从:

<REVERSE_PROXY_HOST>

删除:

location = /nextcloud

和:

location ^~ /nextcloud/

两段规则。

到那时:

旧公网地址

才算真正退役。


二十、这不是一次简单的“换域名”

整个过程实际上完成了:

子路径部署
        ↓
独立子域名根路径部署

旧 Cookie 结构
        ↓
__Host- Cookie

Apache Alias
        ↓
独立 VirtualHost

多入口
        ↓
单一 Canonical URL

后端兼容逻辑
        ↓
前端统一 301

Security Scan A
        ↓
Security Scan A+

因此它更准确的描述应该是:

一次 Nextcloud URL、反向代理、安全 Cookie 和 Web 服务架构的整体重构。

而不仅仅是“把地址换了一个域名”。

Leave a Reply

Your email address will not be published. Required fields are marked *