适用场景:一台有公网 IP 的云服务器,不花钱实现 Zotero 全平台(包括 iOS)WebDAV 同步


总体实现思路:服务器部署Alist(或者任意WebDAV),由于iOS端Zotero仅支持有CA证书的https,所以需要nginx反代,然后通过DuckDNS申请免费域名,再通过域名获取Let's Encrypt签发的CA证书

目录

  1. 前提与概览
  2. 安装 Docker
  3. 部署 AList(WebDAV 服务端)
  4. 获取免费域名(DuckDNS)
  5. nginx 反向代理 + HTTPS
  6. Let's Encrypt 证书(acme.sh)
  7. 自动续签
  8. Zotero 客户端配置

1. 前提与概览

前提条件

项目 说明
云服务器 任意云厂商,有公网 IP(本文以 Ubuntu 24.04 为例)
SSH 访问 能通过 SSH 登录服务器
域名 不需要购买的付费域名,用 DuckDNS 免费方案

架构

Zotero 客户端
    │
    ▼  HTTPS (443)
nginx(反向代理 + TLS 终止)
    │
    ▼  HTTP (20000)
AList(WebDAV 服务端)
    │
    ▼  Local storage
/data/(文件存储)

2. 安装 Docker

2.1 安装 Docker

# 更新包索引
sudo apt update && sudo apt upgrade -y

# 安装依赖
sudo apt install -y ca-certificates curl gnupg lsb-release

# 添加 Docker 官方 GPG 密钥
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 添加 Docker 仓库
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装 Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

# 启动 Docker 并设置开机自启
sudo systemctl enable docker --now

# 将当前用户加入 docker 组(免 sudo 执行 docker)
sudo usermod -aG docker $USER

# 退出重新登录使组生效,或者运行:
newgrp docker

验证docker --version 应显示版本号。

2.2 安装 Docker Compose(可选但推荐)

sudo apt install -y docker-compose-plugin
docker compose version

3. 部署 AList(WebDAV 服务端)

3.1 创建数据目录

mkdir -p ~/alist-data

3.2 启动 AList 容器

docker run -d \
  --restart unless-stopped \
  -v ~/alist-data:/opt/alist/data \
  -p 20000:5245 \
  --name alist \
  xhofe/alist:latest

注意:将容器的 5245(AList 默认端口)映射到宿主机的 20000。后续 nginx 会代理 20000。

3.3 一键配置(无需打开浏览器)

AList 初始化后,以下脚本通过 CLI + API 一步完成密码设置、存储挂载和专用用户创建:

# 自行修改下面的变量值
ALIST_ADMIN_PASSWORD="<你的管理员密码>"  # 管理员密码
WEBDAV_USERNAME="zotero"                   # WebDAV 专用用户名
WEBDAV_PASSWORD="<你的WebDAV用户密码>"     # WebDAV 专用用户密码

# 1. 设置管理员密码
docker exec alist ./alist admin set "$ALIST_ADMIN_PASSWORD"

# 2. 获取管理员 token(用于 API 鉴权)
ALIST_TOKEN=$(docker exec alist ./alist admin token 2>/dev/null | \
  grep "Admin token:" | sed 's/.*Admin token: //')

ALIST_URL="http://localhost:20000"

# 3. 创建存储挂载(/dav → /opt/alist/data)
curl -s -X POST "$ALIST_URL/api/admin/storage/create" \
  -H "Content-Type: application/json" \
  -H "Authorization: $ALIST_TOKEN" \
  -d '{
    "mount_path": "/dav",
    "driver": "Local",
    "order": 0,
    "remark": "Zotero WebDAV",
    "addition": "{\"root_folder_path\":\"/opt/alist/data\",\"thumbnail\":false,\"show_hidden\":false}"
  }'

# 4. 创建 WebDAV 专用用户(role: [0] = 普通用户)
curl -s -X POST "$ALIST_URL/api/admin/user/create" \
  -H "Content-Type: application/json" \
  -H "Authorization: $ALIST_TOKEN" \
  -d "$(cat <<EOF
{
  "username": "$WEBDAV_USERNAME",
  "password": "$WEBDAV_PASSWORD",
  "base_path": "/dav",
  "role": [0],
  "disabled": false
}
EOF
)"

echo "AList 配置完成!"
echo "  WebDAV 地址: http://<你的服务器IP>:20000/dav"
echo "  用户名: $WEBDAV_USERNAME"
echo "  密码: $WEBDAV_PASSWORD"

说明: - role: [0] 表示普通用户,限制其只能访问 base_path 指定的目录 - base_path: "/dav" 限制该用户只能看到 WebDAV 挂载点下的文件 - addition 中的 root_folder_path 对应容器内的物理路径,可根据实际存储位置修改 - 如果 alist admin 相关命令失败,确保容器已完全启动(docker logs alist 查看日志)

3.4 验证 AList 正常工作

# 用 WebDAV 专用用户测试 PROPFIND
curl -s -u "<你的WebDAV用户名>:<你的WebDAV密码>" \
  -X PROPFIND -H "Depth: 0" \
  http://localhost:20000/dav/ | head -10

返回 XML 格式的目录信息即表示 AList 配置正确。


4. 获取免费域名(DuckDNS)

Let's Encrypt 要求拥有域名才能签发受信任的证书。DuckDNS 提供免费的 yourname.duckdns.org 域名。

4.1 注册 DuckDNS

  1. 打开 duckdns.org
  2. 用 GitHub / Google / Twitter 账号登录
  3. Domains 中输入你想要的子域名(如 jhy-zotero),点击 Add domain
  4. 记下显示的 token(用于后续更新 IP)

4.2 解析到服务器

方法 A:手动设置(IP 不变的场景)

DuckDNS 默认将你的子域名解析到你登录时的 IP。如果你的服务器 IP 和登录 DuckDNS 时的 IP 不同,需要:在 DuckDNS 的 current ip 输入框中填写你服务器的公网 IP,点击 Update IP

方法 B:配置自动更新(IP 可能变动的场景)

# 创建更新脚本
mkdir -p ~/duckdns
cat > ~/duckdns/update.sh << 'EOF'
#!/bin/bash
curl -s "https://www.duckdns.org/update?domains=你的子域名&token=你的token&ip="
EOF
chmod +x ~/duckdns/update.sh

# 添加到 crontab(每 5 分钟更新一次)
(crontab -l 2>/dev/null; echo "*/5 * * * * ~/duckdns/update.sh > ~/duckdns/duck.log 2>&1") | crontab -

4.3 验证域名解析

ping 你的子域名.duckdns.org
# 或
dig +short 你的子域名.duckdns.org

输出应显示你的服务器公网 IP。


5. nginx 反向代理 + HTTPS

5.1 安装 nginx

sudo apt install -y nginx
sudo systemctl enable nginx --now

5.2 配置反向代理

创建 nginx 站点配置:

sudo tee /etc/nginx/sites-enabled/alist << 'EOF'
server {
    listen 80;
    server_name 你的子域名.duckdns.org;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name 你的子域名.duckdns.org;

    # 先使用自签名证书占位,后续会被 Let's Encrypt 替换
    ssl_certificate /etc/nginx/ssl/alist.crt;
    ssl_certificate_key /etc/nginx/ssl/alist.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:WEBSSL:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://localhost:20000;
        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_http_version 1.1;
        proxy_set_header Connection "";
    }
}
EOF

5.3 生成临时自签名证书(占位用)

sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/alist.key \
  -out /etc/nginx/ssl/alist.crt \
  -subj "/CN=你的子域名.duckdns.org"

5.4 检查并重载 nginx

sudo nginx -t && sudo systemctl reload nginx

此时 HTTPS 已经可用,但使用的是自签名证书(浏览器会报不安全)。下一步用 Let's Encrypt 替换。


6. Let's Encrypt 证书(acme.sh)

6.1 安装 acme.sh

curl https://get.acme.sh | sh

这会安装到 ~/.acme.sh/,并自动添加 cron 任务。

使 acme.sh 命令可用:

source ~/.bashrc
# 或
alias acme.sh=~/.acme.sh/acme.sh

6.2 签发证书

acme.sh 支持多种验证方式。standalone 模式最简单——它会临时监听 80 端口响应 Let's Encrypt 的挑战。

# 确保 80 端口空闲
sudo lsof -i :80 || echo "80 端口空闲"

# 签发证书
~/.acme.sh/acme.sh --issue -d 你的子域名.duckdns.org --standalone --server letsencrypt

签发成功后,证书文件在:~/.acme.sh/你的子域名.duckdns.org_ecc/

6.3 安装证书到 nginx

# 复制证书
sudo cp ~/.acme.sh/你的子域名.duckdns.org_ecc/fullchain.cer /etc/nginx/ssl/alist.crt
sudo cp ~/.acme.sh/你的子域名.duckdns.org_ecc/你的子域名.duckdns.org.key /etc/nginx/ssl/alist.key

# 设置权限
sudo chmod 644 /etc/nginx/ssl/alist.crt
sudo chmod 600 /etc/nginx/ssl/alist.key

# 重载 nginx
sudo nginx -t && sudo systemctl reload nginx

6.4 验证证书

echo | openssl s_client -connect 你的子域名.duckdns.org:443 \
  -servername 你的子域名.duckdns.org 2>&1 | \
  openssl x509 -noout -subject -issuer -dates

输出应显示: - subject=CN = 你的子域名.duckdns.org - issuer=... Let's Encrypt ... - 有效日期为签发日 + 90 天


7. 自动续签

acme.sh 安装时已自动添加了 cron 任务。验证:

crontab -l | grep acme

输出类似:

58 5,11,17,23 * * * "/home/你的用户名/.acme.sh"/acme.sh --cron --home "/home/你的用户名/.acme.sh" > /dev/null

acme.sh 还支持 ARI(ACME Renewal Information) 功能,会在服务器推荐的续签窗口(通常是到期前 30 天左右)自动续签,无需手动干预。

续签后证书文件内容会更新,acme.sh 会检测到变化。如果证书路径没变,需要手动添加 --reloadcmd "sudo nginx -s reload" 到安装命令,或在 cron 中额外执行 nginx 重载。

建议添加证书更新后自动重载 nginx 的钩子:

~/.acme.sh/acme.sh --install-cert -d 你的子域名.duckdns.org \
  --reloadcmd "sudo nginx -s reload"

8. Zotero 客户端配置

8.1 Windows / macOS / Linux

Zotero 设置: 1. 打开 Zotero → 编辑 → 首选项 → 同步 2. 文件同步 部分: - 同步类型:WebDAV - URL:https://你的子域名.duckdns.org/dav/zotero - 用户名:AList 中创建的 WebDAV 专用账号 - 密码:对应密码 3. 点击 验证服务器,应显示连接成功

8.2 iOS(iPhone / iPad)

iOS 不需要额外配置证书——Let's Encrypt 是全球受信任的根 CA,iOS 原生信任。

  1. 在 Zotero App 中进入 设置 → 同步
  2. 文件同步: - 类型:WebDAV - URL:https://你的子域名.duckdns.org/dav/zotero - 账号密码同上
  3. 点击验证,应直接通过

8.3 故障排查

现象 可能原因 解决方法
Could not connect to server 防火墙/安全组未放行 443 端口 检查云厂商安全组规则
SSL_ERROR_BAD_CERT_DOMAIN 证书域名不匹配 确认证书的 CN 包含 duckdns.org 域名
验证成功但同步失败 AList WebDAV 目录不存在 在 AList 后台创建 /dav/zotero 目录
浏览器 HTTPS 正常但 Zotero 报错 Zotero 内置证书存储过期 重启 Zotero 或重新导入证书