痛点与背景
在搭建私有相册部署服务(如 Immich)时,远程访问通常面临两个抉择:
- Cloudflare Tunnel(内网穿透):免去公网 IP 的烦恼,且带有安全防护和标准 HTTPS。但免费版存在单个文件上传最大 100MB 的硬性限制,在备份动辄数 GB 的手机原画视频时极易断开。
- 公网 IPv6 直连:随着家宽 IPv6 的普及,端到端的直连速度极快,且完全没有上传大小限制。但 IPv6 地址是动态变化的,且直接暴露端口存在安全隐患。
为了兼顾“大文件无限制传输(IPv6直连)”与“全网无缝容灾(CF Tunnel)”,本文实践了一套双链路高可用方案。当处于良好 IPv6 网络下,客户端优先走高带宽直连;在纯 IPv4 环境下,自动降级走 Tunnel 容灾。
第一部分:方案架构设计
本方案的核心在于通过 Nginx 精确分流,将现有的 Web 服务(如 WordPress)与相册服务隔离,同时利用 acme.sh 的 DNS API 模式自动为 IPv6 直连申请原生 SSL 证书。
- 第一顺位(主力):
https://your-ipv6-subdomain.domain.com $\rightarrow$ Nginx (443) $\rightarrow$ Immich (2283)【灰云直连,无限制】 - 第二顺位(容灾):
https://your-tunnel-subdomain.domain.com $\rightarrow$ Cloudflare Tunnel $\rightarrow$ Immich (2283)【黄云穿透,100MB限制】
第二部分:核心配置步骤
1. Nginx 宿主机反向代理与大文件优化
在 /etc/nginx/sites-available/ 下为 IPv6 直连域名创建独立的虚拟主机配置,利用精确的 server_name 匹配,避免被已有的默认重定向规则(如 server_name _)拦截。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-ipv6-subdomain.domain.com;
# SSL 证书路径
ssl_certificate /etc/nginx/ssl/immich.crt;
ssl_certificate_key /etc/nginx/ssl/immich.key;
# 核心:解除 Nginx 自身的上传限制(0 代表无限)
client_max_body_size 0;
# 关闭临时文件缓冲,大文件直接透传给后端容器,避免磁盘 I/O 瓶颈
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_pass http://127.0.0.1:2283;
# 传递必要的 Header
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;
# 开启 WebSocket 支持(Immich 实时同步必须)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 延长超时时间到 10 分钟以上,防止大视频切片传输时间过长
proxy_read_timeout 600s;
proxy_send_timeout 600s;
keepalive_timeout 620s;
}
}
|
2. 轻量化 SSL 证书自动化(acme.sh)
避免引入沉重的可视化证书管理容器,直接在宿主机利用 acme.sh 配合 Cloudflare DNS API 验证申请 ECC 证书:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 导入 Cloudflare 凭证
export CF_Token="your_cloudflare_dns_edit_token"
export CF_Account_ID="your_cloudflare_account_id"
# 申请证书
acme.sh --set-default-ca --server letsencrypt
acme.sh --issue --dns dns_cf -d your-ipv6-subdomain.domain.com --ecc
# 安全安装证书至 Nginx 目录并配置自动重载
acme.sh --install-cert -d your-ipv6-subdomain.domain.com --ecc \
--key-file /etc/nginx/ssl/immich.key \
--fullchain-file /etc/nginx/ssl/immich.crt \
--reloadcmd "sudo systemctl reload nginx"
|
3. 稳健的 Shell + Crontab IPv6 DDNS 脚本
家宽 IPv6 变化频繁,且 Cloudflare API 对于 Token 鉴权较为严格。当使用局部的 DNS:Edit 权限 Token 时,必须通过特定的 ZONE_ID 精确发起 PATCH 请求,否则极易触发误导性的 10000 Authentication error。
编写自动化更新脚本 ~/scripts/cf-ddns.sh:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| #!/bin/bash
# --- 配置区域 ---
CF_TOKEN="your_cloudflare_dns_edit_token"
ZONE_ID="your_cloudflare_zone_id"
SUBDOMAIN="your-ipv6-subdomain.domain.com"
# ----------------
# 1. 获取服务器当前的公网 IPv6 地址(排除本地链路和临时地址)
CURRENT_IPV6=$(ip -6 addr show scope global | grep -v "deprecated" | grep -oE '([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}' | head -n 1)
if [ -z "$CURRENT_IPV6" ]; then
echo "$(date): 未检测到公网 IPv6 地址"
exit 1
fi
# 2. 查询 Cloudflare 当前的 AAAA 记录
DNS_RECORD_INFO=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=AAAA&name=$SUBDOMAIN" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json")
if [[ "$DNS_RECORD_INFO" == *"\"success\":false"* ]]; then
echo "$(date): Cloudflare API 鉴权失败!详细报错: $DNS_RECORD_INFO"
exit 1
fi
RECORD_ID=$(echo "$DNS_RECORD_INFO" | awk -F'"id":"' '{print $2}' | awk -F'"' '{print $1}')
CLOUDFLARE_IPV6=$(echo "$DNS_RECORD_INFO" | awk -F'"content":"' '{print $2}' | awk -F'"' '{print $1}')
if [ -z "$RECORD_ID" ]; then
echo "$(date): 未在 Cloudflare 中找到 $SUBDOMAIN 的 AAAA 记录,请确认后台已手动添加过该记录。"
exit 1
fi
# 3. 比对并执行轻量更新
if [ "$CURRENT_IPV6" = "$CLOUDFLARE_IPV6" ]; then
echo "$(date): IP 未发生改变 ($CURRENT_IPV6),无需更新。"
else
echo "$(date): IP 发生改变,正在将 [$CLOUDFLARE_IPV6] 更新为 [$CURRENT_IPV6]..."
UPDATE_RESULT=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"content\":\"$CURRENT_IPV6\"}")
if [[ "$UPDATE_RESULT" == *"\"success\":true"* ]]; then
echo "$(date): Cloudflare DNS 更新成功!"
else
echo "$(date): Cloudflare DNS 更新失败!错误信息: $UPDATE_RESULT"
fi
fi
|
通过 crontab -e 设置每 5 分钟定时对齐:
1
| */5 * * * * /bin/bash /home/username/scripts/cf-ddns.sh >> /home/username/scripts/ddns.log 2>&1
|
第三部分:避坑指南总结
Cloudflare 10000 报错陷阱:CF API Token 在进行 zones 查询或修改时,如果传入的 ZONE_ID 实际上是 Account ID,接口不会返回“未找到该区域”,而是直接抛出权限不足的混淆报错。可通过 curl -s -X GET "https://api.cloudflare.com/client/v4/zones" -H "Authorization: Bearer <TOKEN>" 准确核对专属的 32 位 Zone ID。
安全性防探测兜底:灰云直连意味着向公网暴露了 443 端口。建议修改全局 nginx.conf 引入 server_tokens off; 隐藏版本。同时配置未匹配域名的默认 443 虚拟主机,对直接扫描公网 IPv6 地址的请求直接返回 444(断开连接),确保只有通过专属合法域名访问的客户端才能安全握手。