稍候片刻,月出文自明。
# CDN_AUTH_KEY:原图 CDN 的 URL 鉴权密钥,签名函数也要用
openssl rand -hex 24
# WEBHOOK_TOKEN:OSS 事件转发函数调用 webhook 时携带的口令
openssl rand -hex 32# 原图签名函数(manifest.photos.json 目前是空清单,首次构建后会自动更新)
(cd fc/photo-auth && zip -r ../../photo-auth.zip .)
# OSS 事件转发函数
(cd deploy/aliyun-static/oss-event-forwarder && zip -r ../../../oss-event-forwarder.zip .)
ls -l photo-auth.zip oss-event-forwarder.zip{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:ListObjects", "oss:GetObject", "oss:GetBucketInfo"],
"Resource": [
"acs:oss:*:*:example-photos-private",
"acs:oss:*:*:example-photos-private/*"
]
}
]
}{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:ListObjects",
"oss:GetObject",
"oss:PutObject",
"oss:DeleteObject",
"oss:GetBucketInfo",
"oss:AbortMultipartUpload",
"oss:ListParts",
"oss:ListMultipartUploads"
],
"Resource": ["acs:oss:*:*:example-site", "acs:oss:*:*:example-site/*"]
}
]
}{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:ListBuckets"],
"Resource": ["acs:oss:*:*:*"]
},
{
"Effect": "Allow",
"Action": ["oss:ListObjects", "oss:GetBucketInfo", "oss:ListMultipartUploads"],
"Resource": ["acs:oss:*:*:example-photos-private"]
},
{
"Effect": "Allow",
"Action": [
"oss:GetObject",
"oss:PutObject",
"oss:DeleteObject",
"oss:AbortMultipartUpload",
"oss:ListParts"
],
"Resource": ["acs:oss:*:*:example-photos-private/photos/*"]
}
]
}{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["fc:GetFunction", "fc:UpdateFunction"],
"Resource": ["acs:fc:cn-chengdu:<你的账号ID>:functions/afilmory-photo-auth"]
}
]
}{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"alidns:AddDomainRecord",
"alidns:DeleteDomainRecord",
"alidns:DescribeDomainRecords",
"alidns:DescribeDomains"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["cdn:SetCdnDomainSSLCertificate"],
"Resource": "*"
}
]
}# 只允许你的主站使用这个评论服务
SECURE_DOMAINS=example.com
# 站点信息(通知邮件里会用到)
SITE_NAME=我的相册
SITE_URL=https://example.comapt update
apt install -y git curl zip perl
# 内存为 2G 时建议加 2G swap(4G 可以跳过)
fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabcurl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
# 国内服务器使用 npm 镜像加速
npm config set registry https://registry.npmmirror.com
export COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
echo 'export COREPACK_NPM_REGISTRY=https://registry.npmmirror.com' >> ~/.bashrc
corepack enable
npm install -g pm2
node -v # v22.x
pm2 -vcd /tmp
curl -o ossutil.zip https://gosspublic.alicdn.com/ossutil/v2/2.4.0/ossutil-2.4.0-linux-amd64.zip
unzip -o ossutil.zip
chmod 755 ossutil-2.4.0-linux-amd64/ossutil
mv ossutil-2.4.0-linux-amd64/ossutil /usr/local/bin/ossutil
ossutil version # 输出 2.4.0 即安装成功mkdir -p /root/projects && cd /root/projects
git clone https://github.com/<你的GitHub用户名>/afilmory.git
cd afilmory
git checkout private-oss
pnpm installsystem: {
processing: {
// 照片 ID 直接用文件名,不加哈希后缀
digestSuffixLength: 0,
},
observability: {
performance: {
worker: {
// 并发数等于 CPU 核数
workerCount: os.cpus().length,
useClusterMode: false,
workerConcurrency: 1,
},
},
},
},cat > .env <<'EOF'
S3_BUCKET_NAME=example-photos-private
S3_REGION=cn-chengdu
S3_ENDPOINT=https://oss-cn-chengdu.aliyuncs.com
S3_ACCESS_KEY_ID=afilmory-builder的AccessKeyID
S3_SECRET_ACCESS_KEY=afilmory-builder的AccessKeySecret
S3_PREFIX=photos
S3_CUSTOM_DOMAIN=
PHOTO_PROXY_BASE_URL=https://media-auth.example.com
EOF
chmod 600 .envWEBHOOK_TOKEN=配置清单中的WEBHOOK_TOKEN
WEBHOOK_ALLOWED_BUCKETS=example-photos-private
OSS_SITE_BUCKET=oss://example-site
ALIYUN_PROFILE=afilmory-fc
ALIYUN_REGION=cn-chengdu
FC_FUNCTION_NAME=afilmory-photo-authnode -e 'const m=require("./apps/web/src/data/photos-manifest.json");console.log(m.data.length, m.data[0]?.originalUrl)'
# 输出类似:3 https://media-auth.example.com/original/%E6%B5%8B%E8%AF%95cd /root/projects/afilmory
pm2 start deploy/aliyun-static/webhook/ecosystem.config.cjs
pm2 save
pm2 startup # 按输出提示执行命令,设置开机自启
curl -s https://hook.example.com/health
# 返回 {"status":"ok"}curl https://get.acme.sh | sh -s email=你的邮箱
source ~/.bashrc
# 使用 afilmory-acme 子账号
export Ali_Key="afilmory-acme的AccessKeyID"
export Ali_Secret="afilmory-acme的AccessKeySecret"
# 签发证书(DNS 验证)
acme.sh --issue --dns dns_ali -d example.com -d img.example.com --server letsencrypt
# 部署到 CDN,之后 acme.sh 每次自动续期都会重新部署
export DEPLOY_ALI_CDN_DOMAIN="example.com img.example.com"
acme.sh --deploy -d example.com --ecc --deploy-hook ali_cdncd afilmory
pnpm install
# 从服务器复制 config.json、builder.config.ts、.env 到本机对应位置后:
pnpm build:manifest # 生成照片清单和缩略图
pnpm --filter @afilmory/web dev # 启动本地预览cd /root/projects/afilmory
git restore apps/web/public # 每次构建都会重新生成 favicon,先还原
git pull
pnpm install --frozen-lockfile
pm2 restart afilmory-webhook # 只有 webhook 代码有变化时才需要"Resource": [
"acs:fc:cn-chengdu:<你的账号ID>:functions/afilmory-photo-auth",
"acs:fc:cn-chengdu:<你的账号ID>:functions/afilmory-manifest-api"
]
写在前面
这是一篇从零开始的完整部署教程。跟着做完,你会得到一个这样的摄影相册:
项目基于开源相册 Afilmory,我在它的基础上做了原图私有化、Waline 评论和自动构建,整理在我的 fork 仓库里:specialhua/afilmory(private-oss 分支)。我的相册:afilm.site。
步骤比较多,涉及本机、阿里云控制台和服务器三个地方。为了少来回切换,文章按「在哪里操作」来组织:先在本机准备好代码和密钥,再在阿里云控制台一次性把云资源配完,最后登录服务器完成部署。每一步用到的东西,都会在前面的步骤里准备好。
文中所有域名都用 example.com 示意,存储桶名、函数名也都是示例,请全部替换为你自己的。建议先完成第二章的「配置清单」,后面照着清单填,不容易出错。
一、方案概览
日常使用是什么样的
1.2 架构
整套方案由这几部分组成:
访问一张原图的完整过程:
发布一张新照片的完整过程:
1.3 和官方版本有什么不同
官方仓库目前开发的方向大概是 SaaS、iOS App 和一键部署,作者 Innei 源码已经支持S3存储了,Cloudflare、阿里云等都是支持的。我最初找类似项目的时候,考虑了以下几点:
如果你不想自己运维,推荐直接用 官方 SaaS。我的 fork 只维护阿里云静态部署这一套方案:
方面
官方仓库
本方案
部署方式
SaaS、Docker、SSR / 后端服务、iOS App
OSS 静态托管 + CDN
原图访问
存储桶公开地址
私有桶 + CDN URL 鉴权,函数计算签发临时地址
评论
依赖官方后端
Waline,静态部署也能用
发布流程
手动构建或后台管理
上传 OSS 后自动构建发布,附构建状态页
遥测
默认开启 VibeLoft 统计
默认关闭
1.4 费用参考
以我的相册为例(私有桶约 700MB、公开桶约 50MB,访问量不大),不含服务器和域名:
项目
月费用
说明
OSS
0.2 ~ 1 元
存储费 + 构建时从 OSS 下载新原图的外网流量
函数计算
7 ~ 17 元
主要是签名函数保留 1 个常驻实例的费用,不保留常驻实例几乎免费,但首次打开原图会慢一点
CDN
几毛钱
取决于访问流量
服务器我用的是轻量应用服务器 2 核 4G。构建时的并发数等于 CPU 核数,2 核 2G 理论上也能跑,但我没有实测,如果用 2G 内存,建议加 2G swap(第四章有命令)。
1.5 开始前需要准备
二、本机:规划配置、拉取代码、生成密钥
这一章全部在你自己的电脑上完成。
2.1 规划配置清单
整个部署会用到很多名称和密钥。先在本机新建一个文本文件(不要放进任何 Git 仓库),复制下面的清单,把示例值改成你自己的,后面每一步都照着它填。
项目
示例值
说明
主站域名
example.com
也可以用子域名,比如 photo.example.com
原图域名
img.example.com
CDN,回源私有桶
签名函数域名
media-auth.example.com
函数计算自定义域名
webhook 域名
hook.example.com
解析到服务器
评论域名
comment.example.com
解析到服务器
地域
cn-chengdu (西南1 成都)
OSS、函数计算必须在同一地域
OSS 外网 Endpoint
oss-cn-chengdu.aliyuncs.com
按地域替换
公开桶
example-site
存放静态网站,桶名全局唯一
私有桶
example-photos-private
存放原图
签名函数名
afilmory-photo-auth
转发函数名
oss-event-forwarder
阿里云账号 ID
待填
控制台右上角头像里可以看到
服务器公网 IP
待填
第三章购买后填
CDN_AUTH_KEY
待填
2.3 节生成
WEBHOOK_TOKEN
待填
2.3 节生成
5 组 AccessKey
待填
3.2 节创建
2.2 Fork 并拉取代码
本机暂时不需要安装依赖,这一章只用到仓库里的函数代码。以后想修改代码、本地预览,看第八章。
本章的命令用到 git、openssl 和 zip,macOS 和 Linux 自带;Windows 用户可以在 WSL 里执行,或者用 Git Bash 执行 openssl、用资源管理器把函数目录里的文件压缩成 zip(注意压缩的是目录里的文件,而不是目录本身)。
想换网站图标,替换仓库根目录的 logo.jpg 即可,每次构建都会用它自动生成各尺寸的 favicon。改完提交并推送到你的 fork。
2.3 生成两个密钥
在本机终端执行两次,把输出分别记到配置清单里:
2.4 打包两个函数的代码
在仓库根目录执行,生成两个 zip 包,第三章上传到函数计算:
三、阿里云控制台:一次配完所有云资源
这一章全部在 阿里云控制台 完成。顺序是有依赖关系的,请按顺序做。
3.1 购买轻量应用服务器
3.2 创建 RAM 子账号与最小权限策略
不要在任何地方使用主账号的 AccessKey。按用途创建 5 个子账号,每个只给必需的权限,某个密钥泄露时影响范围也最小。
进入 RAM 访问控制 →「权限管理」→「权限策略」→「创建权限策略」→「脚本编辑」,依次创建下面 5 个策略。把其中的桶名、地域、账号 ID、函数名替换成配置清单里的值。
策略一:afilmory-builder-policy(服务器构建时读取私有桶)
策略二:afilmory-publisher-policy(服务器用 ossutil 发布网站到公开桶)
策略三:afilmory-uploader-policy(手机 App 上传照片,只能写 photos/ 目录)
策略四:afilmory-fc-deployer-policy(构建完成后更新签名函数的代码)
策略五:afilmory-acme-policy(申请和续期 SSL 证书:通过 DNS 验证域名,并把证书部署到 CDN)
然后进入「身份管理」→「用户」→「创建用户」,创建 5 个用户,访问方式都只勾选 OpenAPI 调用访问(不需要控制台登录),并分别授予对应的策略:
用户名
授权策略
在哪里使用
afilmory-builder
afilmory-builder-policy
服务器项目根目录 .env
afilmory-publisher
afilmory-publisher-policy
服务器 ossutil
afilmory-uploader
afilmory-uploader-policy
手机 OSS App
afilmory-fc-deployer
afilmory-fc-deployer-policy
服务器阿里云 CLI
afilmory-acme
afilmory-acme-policy
1Panel 证书申请、证书自动续期
每创建一个用户,立即把 AccessKey ID 和 AccessKey Secret 记到配置清单。Secret 只在创建时显示一次。
AccessKey 相当于账号密码:不要发给别人,不要提交到 Git 仓库。一旦泄露,立即在 RAM 控制台禁用并删除,再重新创建。 我自己早期用的是 AliyunOSSFullAccess 这类系统策略,能看到账号下所有桶,权限明显过大,建议您直接按上面的最小权限来。如果某一步提示权限不足,可以临时换成对应的系统策略排查。
3.3 创建两个 OSS 存储桶
进入 对象存储 OSS →「Bucket 列表」→「创建 Bucket」。
私有桶(存放原图):
公开桶(存放静态网站):
注意,阿里云这里创建公开桶时,也就是上图的第5点默认是灰色的,不允许直接创建公开桶,先按私有桶创建完再到桶设置中修改类型,关闭阻止公共访问,并修改读写权限为:公共读
创建完公开桶后,进入该桶 →「数据管理」→「静态页面」,按下面设置并保存:
这样访问 /photos/xxx 这种前端路由时,OSS 会返回 index.html,由前端页面自己处理路由,刷新页面也不会 404。
最后,进入私有桶,新建目录 photos,先上传几张测试照片(JPG 即可),第四章首次构建时要用。
照片 ID 就是去掉扩展名的文件名,也会出现在网址里,所以同一个桶里不要有重名文件。文件名可以用中文。
3.4 申请 SSL 证书
CDN 和函数计算自定义域名都需要 HTTPS 证书。先用阿里云的免费证书把流程跑通,第四章最后再介绍自动续期。
进入 数字证书管理服务 →「SSL 证书管理」→「个人测试证书(原免费证书)」,分别为以下 3 个域名申请证书:
域名验证方式选「自动 DNS 验证」(DNS 在同一账号的云解析下会自动添加验证记录),等待签发完成。
免费证书有效期较短(目前为 3 个月),到期前需要续期,否则网站会打不开。建议部署完成后配置 4.9 节的自动续期。
3.5 CDN:主站 example.com
进入 CDN →「域名管理」→「添加域名」:
添加完成后,按下面依次修改配置:
地址
类型
过期时间
权重
其他
/index.html
目录
1 分钟
98
开启「优先遵循源站缓存策略」
/sw.js
目录
1 分钟
97
开启「优先遵循源站缓存策略」
/registerSW.js
目录
1 分钟
96
开启「优先遵循源站缓存策略」
/manifest.webmanifest
目录
1 分钟
95
开启「优先遵循源站缓存策略」
jpg,jpeg,png,webp,svg,ico,gif,avif
文件后缀名
1 个月
90
开启「忽略源站不缓存标头」
js,css,woff,woff2,ttf,eot,otf
文件后缀名
7 天
80
开启「忽略源站不缓存标头」
html
文件后缀名
1 分钟
70
json,xml,webmanifest
文件后缀名
1 分钟
60
index.html、Service Worker 这几个文件缓存时间很短,发布新照片后用户能尽快看到;带版本号的 js、css 和图片可以长期缓存。
最后复制该域名的 CNAME 地址,去 云解析 DNS 为主域名添加一条记录:主机记录 @,记录类型 CNAME,记录值为 CDN 给的 CNAME。
如果你的主域名配置了邮箱(有 MX 记录),根域名无法再添加 CNAME 记录。这种情况建议用子域名作为主站,比如 photo.example.com,后文所有 example.com 主站地址都相应替换。
3.6 CDN:原图 img.example.com
再添加一个加速域名:
添加完成后修改配置:
网站需要用 JavaScript 读取原图(解析 EXIF、在 WebGL 查看器里显示),所以原图域名必须允许主站跨域访问。Access-Control-Allow-Origin 填你的主站地址即可,不建议填 *。
最后去云解析添加记录:主机记录 img,类型 CNAME,记录值为 CDN 给的 CNAME。
3.7 函数计算:原图签名函数
进入 函数计算 FC 3.0 控制台,确认左上角地域与配置清单一致。
1. 创建函数
「函数」→「创建函数」→「事件函数」:
2. 配置环境变量
「配置」→「环境变量」,添加:
变量名
值
说明
CDN_MEDIA_BASE_URL
https://img.example.com
原图 CDN 地址
CDN_AUTH_KEY
配置清单中的值
必须与 3.6 节的主 KEY 完全一致
CDN_AUTH_VALID_SECONDS
600
签名时间窗口,同一窗口内同一张图的地址不变
CDN_AUTH_OVERLAP_SECONDS
300
额外延长的有效时间,避免窗口切换时地址恰好过期
ALLOW_ORIGIN
https://example.com
许跨域调用签名函数的来源,逗号分隔可填多个,必填,漏配时函数会直接报错
按上面的配置,签名地址每 10 分钟更换一次,每个地址从生成起至少可用约 35 分钟。同一个时间窗口内,同一张照片的地址不变,浏览器可以直接命中缓存。建议 CDN_AUTH_VALID_SECONDS 不超过 3.6 节设置的有效时长。
3. 创建 HTTP 触发器
「配置」→「触发器」→「创建触发器」:
4. 保留一个常驻实例(可选)
「配置」→「弹性配置」→ 最小实例数设为 1。这样打开原图时不会遇到冷启动,但会产生每月几元到十几元的费用;设为 0 则几乎免费,只是冷启动时首张原图会慢一两秒。
5. 绑定自定义域名
函数计算要求先把域名解析过去,才能添加自定义域名:
验证一下(在任意终端执行):
不能直接用函数的默认公网地址访问:函数计算会给默认域名的响应强制加上下载头,浏览器会把返回内容当成文件下载,所以必须绑定自定义域名。
3.8 函数计算:OSS 事件转发函数
1. 创建函数
「函数」→「创建函数」→「事件函数」:
2. 配置环境变量
变量名
值
WEBHOOK_HOST
hook.example.com
WEBHOOK_TOKEN
配置清单中的值
3. 创建 OSS 触发器
「配置」→「触发器」→「创建触发器」:
此时服务器还没部署好,如果现在往 photos/ 上传照片,转发函数会因为连不上 webhook 而报错重试,不影响后续使用。建议先不要上传,等第四章做完再说。
3.9 核对 DNS 解析
最后在 云解析 DNS 补上指向服务器的两条记录,并核对前面添加的记录,一共 5 条:
主机记录
记录类型
记录值
@
CNAME
主站 CDN 的 CNAME
img
CNAME
原图 CDN 的 CNAME
media-auth
CNAME
<你的账号ID>.cn-chengdu.fc.aliyuncs.com
hook
A
服务器公网 IP
comment
A
服务器公网 IP
到这里,阿里云控制台的配置全部完成。
四、服务器:安装 1Panel、Waline 和构建环境
这一章通过 SSH 登录服务器操作,部分步骤在 1Panel 网页面板上完成。以下命令都以 root 用户执行。
4.1 安装 1Panel 和 OpenResty
4.2 在 1Panel 中配置证书申请
1Panel 会自动续期这两张证书。
4.3 安装 Waline 评论
Waline我想的是2个作用,一个是供别人评论,而且他具有免登陆的特点,原仓库的评论应该是用Oauth登陆github、google账号进行评论,对境内用户不是很友好;二是自己也可以发表些关于照片拍摄的想法、创意,值得记念的一句话等等。
配置环境变量
在 1Panel「应用商店」→「已安装」→ Waline →「参数」(或编辑 compose 文件的 environment),按需添加以下变量,保存后重建应用:
配置邮件通知(可选)
阿里云服务器默认禁止 25 端口,需要使用 465 端口(SSL)的 SMTP 服务。以 QQ 邮箱为例:
网易 163 邮箱把 SMTP_SERVICE 换成 163,在 163 邮箱设置里开启 SMTP 并获取授权码即可。Gmail 等境外邮箱在国内服务器上通常连不上,不建议使用。
如果不想用邮件,也可以设置 WEBHOOK=https://你的接收地址,Waline 会在收到新评论时向这个地址发送 POST 请求,可以对接自己的通知服务。
4.4 创建 webhook 反向代理网站
「网站」→「创建网站」→「反向代理」:
创建后进入该网站 →「HTTPS」→ 启用,选择 hook.example.com 的证书。webhook 服务在 4.8 节启动,现在访问会返回 502,属于正常现象。
4.5 安装构建工具
回到 SSH 终端。
1. 基础软件与 swap
2. Node.js 22、pnpm、pm2
pnpm 的版本由仓库的 package.json 锁定,进入项目目录后第一次执行 pnpm 时会自动下载对应版本。
3. ossutil(发布网站用)
构建脚本用的是 ossutil 2.x 的命令格式。1.x 和 2.x 的配置文件、命令参数都不兼容,用 1.x 跑脚本会在开头直接报错退出。
从阿里云下载 2.x 的安装包,解压后放到 PATH 里:
ARM 服务器把链接里的 amd64 换成 arm64。最新版本号可以在阿里云文档「安装 ossutil 2.0」里查。
然后配置访问凭据:
按提示依次填写:
提示
填写
config file name
直接回车,用默认的 /root/.ossutilconfig
AccessKey ID / Secret
3.2 节里给发布用户创建的 AK/SK
Region
站点 bucket 所在地域,例如成都填 cn-chengdu
Endpoint
回车留空,会按 Region 自动推导
配置文件路径一定要用默认值,构建脚本调用 ossutil 时不会指定配置文件。
最后验证能否访问站点 bucket:
能列出目录就说明配置好了。不要用不带 bucket 名的 ossutil ls 验证:发布用户只有这一个桶的权限,列出账号下所有 bucket 会报 AccessDenied,这是正常的。
4. 阿里云 CLI(更新签名函数用)
aliyun configure 时填入 afilmory-fc-deployer 的 AccessKey,Region 填 cn-chengdu。
4.6 拉取代码并填写配置
1. 拉取代码并安装依赖
pnpm install 会自动创建 config.json 和 builder.config.ts(从模板复制),这两个文件和 .env 都不会被提交到 Git。
2. 站点配置 config.json
主要修改这些字段:
3. 构建配置 builder.config.ts
找到并修改下面几项,降低构建时的内存占用,并让照片 ID 直接使用文件名:
只改这几项,其他配置保持默认。
4. 构建环境变量:项目根目录 .env
S3_REGION 填地域 ID(如 cn-chengdu),S3_ENDPOINT 填带 oss- 前缀的外网 Endpoint,两者写法不同,注意区分。PHOTO_PROXY_BASE_URL 是原图私有化的开关:设置后,照片清单里的原图地址会变成签名函数的地址,而不是 OSS 的公开地址。
5. 发布与 webhook 配置:deploy/aliyun-static/.env
需要修改的项:
其余项保持默认。注意这个文件同时被 Node.js 和 bash 读取,值里不要有空格,也不要在行尾写注释。
4.7 首次构建并发布
建议在 tmux 里执行,SSH 断开也不影响:
脚本会依次执行:处理照片 → 构建网站 → 打包签名函数 → 发布到公开桶 → 更新签名函数。最后一行显示「构建并发布完成」即为成功。某一步失败时,详细日志在 /tmp/afilmory-build-logs/ 目录下。
检查照片清单里的原图地址是否已经是签名函数的地址:
然后打开 https://example.com,应该能看到 3.3 节上传的测试照片,点开大图也能正常加载。
4.8 启动 webhook
浏览器打开 https://hook.example.com,可以看到构建状态页:当前状态、最近构建记录,以及每 10 秒自动刷新的日志。状态页和日志是公开的,但日志只展示照片文件名和构建步骤,报错原文、路径、密钥等信息都会被隐藏;会触发构建的接口必须携带 WEBHOOK_TOKEN。
4.9 证书自动续期(推荐)
hook 和 comment 两张证书由 1Panel 自动续期。CDN 上的 example.com、img.example.com 使用的是 3 个月的免费证书,可以用 acme.sh 自动续期并部署到 CDN:
acme.sh 安装时会自动添加定时任务,到期前自动续期并部署到 CDN。
media-auth.example.com 是函数计算的自定义域名,不走 CDN,而且函数计算要求使用 RSA 证书。目前需要在证书到期前手动续期,或者自己写脚本调用阿里云 CLI 更新;我自己用的自动化脚本整理好后,后续会放到仓库里。
五、手机上传照片
5.1 配置手机 App
iOS 上我用的是 App Store 里的 OSS Browser(第三方 App),Android 上也有类似的 OSS 客户端。以 OSS Browser 为例:
afilmory-uploader 只能在私有桶的 photos/ 目录下读写,即使手机丢了或者 App 出了问题,也不会影响其他资源。如下图,从文件上传而别从相册选择。
5.2 导出照片的注意事项
5.3 上传并观察构建
六、验证清单
部署完成后,建议逐项确认:
七、常见问题
7.1 刷新子页面显示 404 或空白
检查公开桶的「静态页面」配置(3.3 节),默认 404 页必须是 index.html。
7.2 用 OSS 默认域名访问时浏览器直接下载 index.html
这是 OSS 的安全限制,默认域名不能直接浏览网页,必须通过绑定的自定义域名(CDN)访问。
7.3 原图打不开,返回 403
7.4 原图打不开,签名函数返回 Photo Not Found
签名函数里的照片清单没有更新。检查构建状态页里「自动部署函数 photo-auth」这一步是否成功;如果失败,在服务器上查看 /tmp/afilmory-build-logs/deploy-fc-photo-auth.log,通常是阿里云 CLI 的 profile 名称或 afilmory-fc-deployer 的权限不对。
7.5 浏览器控制台报跨域错误
站外应用(如博客)引用原图时,签名函数的ALLOW_ORIGIN里也要有那个站点的地址。只用<img>显示不受影响,但用javascript读取图片字节(例如显示下载进度)会被浏览器拦下
7.6 上传照片后没有自动构建
按顺序排查:
7.7 构建失败
状态页会显示失败的步骤,详细原因在服务器上查看:
内存不足时进程会被系统杀掉,表现为构建突然中断,加 swap 或降低 workerCount 即可。
7.8 构建成功,但网站上看不到新照片
index.html 在 CDN 上有 1 分钟缓存,网站还注册了 Service Worker,稍等一会儿再强制刷新即可。
八、二次开发与同步更新
8.1 在本机预览和修改
本地预览时,原图域名的跨域限制只允许主站访问,大图可能加载不出来;需要调试原图时,可以临时把 CDN 的 Access-Control-Allow-Origin 改为 *,调试完记得改回去。
改完代码后提交并推送到你的 fork:
8.2 更新服务器上的代码
下一次上传照片触发构建时,就会使用新代码;也可以手动触发一次构建:
8.3 同步官方仓库的更新
官方仓库更新很频繁,合并时可能遇到冲突,建议在新分支上合并、测试通过后再合回 private-oss。
九、可选:在博客中引用相册
如果你的博客(比如 Mix Space / Yohaku)编辑器支持插入 Afilmory 相册区块,它需要从相册读取照片清单。官方版本由后端提供这些接口,静态部署没有后端,所以这里用一个函数计算补上:路由和官方后端一致,博客那边不用改任何代码。
这个函数只返回照片清单,缩略图地址会补全为主站地址,私有桶里的文件路径不会出现在响应里,原图仍然只能通过签名函数访问。
9.1 给部署账号加权限
RAM → 权限策略 → afilmory-fc-deployer-policy,在 Resource 里加一行:
9.2 创建函数
「函数」→「创建函数」→「事件函数」:
环境变量:
变量名
值
说明
ALLOW_ORIGINS
https://blog.example.com,https://api.blog.example.com
博客前台和后台的地址,逗号分隔,要带 `https://`,必填
SITE_BASE_URL
https://example.com
相册主站地址,必填
博客后台和前台不在同一个域名时,两个都要填。否则前台能显示照片,但后台编辑器里选不了片。
9.3 挂到签名函数的域名上
不需要新域名。「域名管理」→ 点击 media-auth.example.com →「路由配置」,新增两条指向 afilmory-manifest-api:
路径
函数
`/api/*`
afilmory-manifest-api
`/photos/*`
afilmory-manifest-api
/(精确路径,不带 *)
afilmory-manifest-api
原来的 /* → afilmory-photo-auth 保留不动。这几条路径互不重叠,顺序无所谓。
第三条是精确路径 /,不是 /*。站外应用的「查看全部」和标签筛选链接都是直接拼在相册地址上的(https://media-auth.example.com/?tags=xxx),没有这条路由就会落到 /* → 签名函数,返回一段 {"message":"Not Found"} 的 JSON。函数收到后会 302 跳回主站首页,查询串原样带过去。
9.4 让构建自动更新它
在服务器的 deploy/aliyun-static/.env 加一行:
然后手动触发一次构建(见 8.2 节)。之后每次上传照片,构建都会把最新的照片清单同时更新到两个函数。
9.5 验证
注意是 /api/health:/health 仍然由签名函数响应。
9.6 在博客里使用
编辑器插入 Afilmory 区块时,相册地址填 https://media-auth.example.com,不是主站地址。博客会用这个地址请求照片清单,图片直接从主站加载,点击照片会跳回主站的详情页。
选片面板一直加载不出来时,打开浏览器控制台:报跨域错误,就检查 ALLOW_ORIGINS 有没有包含后台的地址。
本章只负责把只读接口准备好。照片在文章里长什么样、点击有什么行为,取决于你博客端的渲染实现,和这套部署无关。
9.10 站外应用里点「查看全部」,打开的是一段 JSON
自定义域名少配了精确路径 / 的路由,见 9.3。
写在最后
整套流程第一次配置确实繁琐,但配好之后几乎不用维护:修图、导出、上传,剩下的全自动完成。
最后感谢 Innei 和 Afilmory 团队开源了这么好的相册项目。如果这篇文章对你有帮助,欢迎给 我的仓库 和 官方仓库 点个 Star。部署中遇到问题,欢迎在评论区留言。