写在前面这是一篇从零开始的完整部署教程。跟着做完,你会得到一个这样的摄影相册:网站是纯静态页面,放在阿里云 OSS 上,由 CDN 加速,国内访问很快原图放在私有存储桶里,访客查看大图时由函数计算临时签发访问地址,原图链接不能被长期盗用在手机上把照片上传到 OSS,大约 1 分钟后网站上就出现新照片,不用登录服务器每张照片有独立的评论区(Waline)除了一台最低配的服务器,每月云服务费用只有十几元项目基于开源相册 Afilmory,我在它的基础上做了原图私有化、Waline 评论和自动构建,整理在我的 fork 仓库里:specialhua/afilmory(private-oss 分支)。我的相册:afilm.site。步骤比较多,涉及本机、阿里云控制台和服务器三个地方。为了少来回切换,文章按「在哪里操作」来组织:先在本机准备好代码和密钥,再在阿里云控制台一次性把云资源配完,最后登录服务器完成部署。每一步用到的东西,都会在前面的步骤里准备好。Tip文中所有域名都用 example.com 示意,存储桶名、函数名也都是示例,请全部替换为你自己的。建议先完成第二章的「配置清单」,后面照着清单填,不容易出错。一、方案概览日常使用是什么样的用 Lightroom 或 Photoshop 修完图,导出到手机的「文件」App(不要存到「相册」,会丢失 EXIF 信息)用手机上的 OSS 客户端、或者在网页端阿里云OSS后台文件管理中,把照片上传到私有桶的 photos/ 目录等待约 1 分钟,刷新相册即可看到新照片;构建进度可以在 https://hook.example.com 实时查看删除 OSS 上的照片,网站上的照片也会自动删掉。1.2 架构整套方案由这几部分组成:私有 OSS 桶:存放原图,不对外公开公开 OSS 桶:存放构建好的静态网站和缩略图,开启静态网站托管CDN example.com:加速主站,回源公开桶CDN img.example.com:加速原图,回源私有桶,开启 URL 鉴权,没有合法签名的请求一律拒绝函数计算 afilmory-photo-auth:绑定 media-auth.example.com,访客点开原图时,它根据照片 ID 生成一个带时效签名的 img.example.com 地址,再 302 跳转过去函数计算 oss-event-forwarder:私有桶有照片上传或删除时被 OSS 触发,把事件转发给服务器轻量服务器:运行 webhook 服务,收到事件后执行构建:读取私有桶生成照片清单和缩略图,构建静态网站,发布到公开桶,并把最新照片清单更新到签名函数;同时运行 Waline 评论服务访问一张原图的完整过程:发布一张新照片的完整过程:1.3 和官方版本有什么不同官方仓库目前的方向是 SaaS、iOS App 和一键部署。如果你不想自己运维,推荐直接用 官方 SaaS。我的 fork 只维护阿里云静态部署这一套方案:方面官方仓库本方案部署方式SaaS、Docker、SSR / 后端服务、iOS AppOSS 静态托管 + CDN原图访问存储桶公开地址私有桶 + CDN URL 鉴权,函数计算签发临时地址评论依赖官方后端Waline,静态部署也能用发布流程手动构建或后台管理上传 OSS 后自动构建发布,附构建状态页遥测默认开启 VibeLoft 统计默认关闭1.4 费用参考以我的相册为例(私有桶约 700MB、公开桶约 50MB,访问量不大),不含服务器和域名:项目月费用说明OSS0.2 ~ 1 元存储费 + 构建时从 OSS 下载新原图的外网流量函数计算7 ~ 17 元主要是签名函数保留 1 个常驻实例的费用,不保留常驻实例几乎免费,但首次打开原图会慢一点CDN几毛钱取决于访问流量服务器我用的是轻量应用服务器 2 核 4G。构建时的并发数等于 CPU 核数,2 核 2G 理论上也能跑,但我没有实测,如果用 2G 内存,建议加 2G swap(第四章有命令)。1.5 开始前需要准备阿里云账号,已实名认证一个域名,已完成 ICP 备案,DNS 解析托管在阿里云云解析。中国大陆的 CDN 和函数计算自定义域名都要求备案一台安装了 Git 的电脑(本机),用于拉取代码、生成密钥和打包函数代码;Node.js 只在第八章二次开发时需要我的iphone用OSS Browser上传,Android应该有更多的应用访问存储桶,用于日常上传照片如果你在 GitHub 上没有账号,先注册一个,后面要 fork 仓库二、本机:规划配置、拉取代码、生成密钥这一章全部在你自己的电脑上完成。2.1 规划配置清单整个部署会用到很多名称和密钥。先在本机新建一个文本文件(不要放进任何 Git 仓库),复制下面的清单,把示例值改成你自己的,后面每一步都照着它填。项目示例值说明主站域名example.com也可以用子域名,比如 photo.example.com原图域名img.example.comCDN,回源私有桶签名函数域名media-auth.example.com函数计算自定义域名webhook 域名hook.example.com解析到服务器评论域名comment.example.com解析到服务器地域cn-chengdu (西南1 成都)OSS、函数计算必须在同一地域OSS 外网 Endpointoss-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 并拉取代码打开 specialhua/afilmory,点右上角 Fork,取消勾选「Copy the main branch only」,这样才会带上 private-oss 分支拉取代码:BASH复制git clone https://github.com/<你的GitHub用户名>/afilmory.git cd afilmory git checkout private-oss本机暂时不需要安装依赖,这一章只用到仓库里的函数代码。以后想修改代码、本地预览,看第八章。Tip本章的命令用到 git、openssl 和 zip,macOS 和 Linux 自带;Windows 用户可以在 WSL 里执行,或者用 Git Bash 执行 openssl、用资源管理器把函数目录里的文件压缩成 zip(注意压缩的是目录里的文件,而不是目录本身)。 Tip想换网站图标,替换仓库根目录的 logo.jpg 即可,每次构建都会用它自动生成各尺寸的 favicon。改完提交并推送到你的 fork。2.3 生成两个密钥在本机终端执行两次,把输出分别记到配置清单里:BASH复制# CDN_AUTH_KEY:原图 CDN 的 URL 鉴权密钥,签名函数也要用 openssl rand -hex 24 # WEBHOOK_TOKEN:OSS 事件转发函数调用 webhook 时携带的口令 openssl rand -hex 322.4 打包两个函数的代码在仓库根目录执行,生成两个 zip 包,第三章上传到函数计算:BASH复制# 原图签名函数(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三、阿里云控制台:一次配完所有云资源这一章全部在 阿里云控制台 完成。顺序是有依赖关系的,请按顺序做。3.1 购买轻量应用服务器进入 轻量应用服务器 控制台,创建实例地域:与配置清单中的地域一致(例如成都)镜像:系统镜像选择 Debian 12套餐:2 核 2G 起步,我用的是 2 核 4G创建完成后,把 公网 IP 记到配置清单进入实例详情 →「防火墙」,确认放行 80、443 端口。1Panel 面板端口在第四章安装后再放行3.2 创建 RAM 子账号与最小权限策略不要在任何地方使用主账号的 AccessKey。按用途创建 5 个子账号,每个只给必需的权限,某个密钥泄露时影响范围也最小。进入 RAM 访问控制 →「权限管理」→「权限策略」→「创建权限策略」→「脚本编辑」,依次创建下面 5 个策略。把其中的桶名、地域、账号 ID、函数名替换成配置清单里的值。策略一:afilmory-builder-policy(服务器构建时读取私有桶)JSON复制{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": ["oss:ListObjects", "oss:GetObject", "oss:GetBucketInfo"], "Resource": [ "acs:oss:*:*:example-photos-private", "acs:oss:*:*:example-photos-private/*" ] } ] }策略二:afilmory-publisher-policy(服务器用 ossutil 发布网站到公开桶)JSON复制{ "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/*"] } ] }策略三:afilmory-uploader-policy(手机 App 上传照片,只能写 photos/ 目录)JSON{ "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/*"] } ] }展开 · 26 行复制策略四:afilmory-fc-deployer-policy(构建完成后更新签名函数的代码)JSON复制{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": ["fc:GetFunction", "fc:UpdateFunction"], "Resource": ["acs:fc:cn-chengdu:<你的账号ID>:functions/afilmory-photo-auth"] } ] }策略五:afilmory-acme-policy(申请和续期 SSL 证书:通过 DNS 验证域名,并把证书部署到 CDN)JSON复制{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "alidns:AddDomainRecord", "alidns:DeleteDomainRecord", "alidns:DescribeDomainRecords", "alidns:DescribeDomains" ], "Resource": "*" }, { "Effect": "Allow", "Action": ["cdn:SetCdnDomainSSLCertificate"], "Resource": "*" } ] }然后进入「身份管理」→「用户」→「创建用户」,创建 5 个用户,访问方式都只勾选 OpenAPI 调用访问(不需要控制台登录),并分别授予对应的策略:用户名授权策略在哪里使用afilmory-builderafilmory-builder-policy服务器项目根目录 .envafilmory-publisherafilmory-publisher-policy服务器 ossutilafilmory-uploaderafilmory-uploader-policy手机 OSS Appafilmory-fc-deployerafilmory-fc-deployer-policy服务器阿里云 CLIafilmory-acmeafilmory-acme-policy1Panel 证书申请、证书自动续期每创建一个用户,立即把 AccessKey ID 和 AccessKey Secret 记到配置清单。Secret 只在创建时显示一次。WarningAccessKey 相当于账号密码:不要发给别人,不要提交到 Git 仓库。一旦泄露,立即在 RAM 控制台禁用并删除,再重新创建。 我自己早期用的是 AliyunOSSFullAccess 这类系统策略,能看到账号下所有桶,权限明显过大,建议您直接按上面的最小权限来。如果某一步提示权限不足,可以临时换成对应的系统策略排查。3.3 创建两个 OSS 存储桶进入 对象存储 OSS →「Bucket 列表」→「创建 Bucket」。私有桶(存放原图):Bucket 名称:example-photos-private地域:与配置清单一致存储类型:标准存储读写权限:私有公开桶(存放静态网站):Bucket 名称:example-site地域:与配置清单一致存储类型:标准存储读写权限:公共读Note注意,阿里云这里创建公开桶时,也就是上图的第5点默认是灰色的,不允许直接创建公开桶,先按私有桶创建完再到桶设置中修改类型,关闭阻止公共访问,并修改读写权限为:公共读创建完公开桶后,进入该桶 →「数据管理」→「静态页面」,按下面设置并保存:默认首页:index.html子目录首页:开通文件 404 规则:Redirect默认 404 页:index.html错误文档响应码:404这样访问 /photos/xxx 这种前端路由时,OSS 会返回 index.html,由前端页面自己处理路由,刷新页面也不会 404。最后,进入私有桶,新建目录 photos,先上传几张测试照片(JPG 即可),第四章首次构建时要用。Tip照片 ID 就是去掉扩展名的文件名,也会出现在网址里,所以同一个桶里不要有重名文件。文件名可以用中文。3.4 申请 SSL 证书CDN 和函数计算自定义域名都需要 HTTPS 证书。先用阿里云的免费证书把流程跑通,第四章最后再介绍自动续期。进入 数字证书管理服务 →「SSL 证书管理」→「个人测试证书(原免费证书)」,分别为以下 3 个域名申请证书:example.comimg.example.commedia-auth.example.com域名验证方式选「自动 DNS 验证」(DNS 在同一账号的云解析下会自动添加验证记录),等待签发完成。Warning免费证书有效期较短(目前为 3 个月),到期前需要续期,否则网站会打不开。建议部署完成后配置 4.9 节的自动续期。3.5 CDN:主站 example.com进入 CDN →「域名管理」→「添加域名」:加速域名:example.com业务类型:图片小文件源站类型:OSS 域名,选择公开桶 example-site端口:443加速区域:仅中国内地(或全球)添加完成后,按下面依次修改配置:回源配置 → 开启「阿里云 OSS 私有 Bucket 回源」,按提示完成授权HTTPS 配置 → 开启 HTTPS,选择 3.4 节申请的 example.com 证书;开启「强制跳转」HTTP → HTTPS缓存配置 →「缓存过期时间」,添加以下规则:地址类型过期时间权重其他/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 分钟70json,xml,webmanifest文件后缀名1 分钟60index.html、Service Worker 这几个文件缓存时间很短,发布新照片后用户能尽快看到;带版本号的 js、css 和图片可以长期缓存。最后复制该域名的 CNAME 地址,去 云解析 DNS 为主域名添加一条记录:主机记录 @,记录类型 CNAME,记录值为 CDN 给的 CNAME。Warning如果你的主域名配置了邮箱(有 MX 记录),根域名无法再添加 CNAME 记录。这种情况建议用子域名作为主站,比如 photo.example.com,后文所有 example.com 主站地址都相应替换。3.6 CDN:原图 img.example.com再添加一个加速域名:加速域名:img.example.com业务类型:图片小文件源站类型:OSS 域名,选择私有桶 example-photos-private端口:443加速区域:仅中国内地(或全球)添加完成后修改配置:回源配置 → 开启「阿里云 OSS 私有 Bucket 回源」并授权;开启「Range 回源」HTTPS 配置 → 开启 HTTPS,选择 img.example.com 证书;开启强制跳转访问控制 →「URL 鉴权」→ 开启: 鉴权类型:A 方式 主 KEY:配置清单里的 CDN_AUTH_KEY 有效时长:1800 秒缓存配置 →「缓存过期时间」:地址 /,类型「目录」,过期时间 1 个月,权重 99缓存配置 →「修改出站响应头」,逐条「增加」:网站需要用 JavaScript 读取原图(解析 EXIF、在 WebGL 查看器里显示),所以原图域名必须允许主站跨域访问。Access-Control-Allow-Origin 填你的主站地址即可,不建议填 *。最后去云解析添加记录:主机记录 img,类型 CNAME,记录值为 CDN 给的 CNAME。3.7 函数计算:原图签名函数进入 函数计算 FC 3.0 控制台,确认左上角地域与配置清单一致。1. 创建函数「函数」→「创建函数」→「事件函数」:函数名称:afilmory-photo-auth运行环境:Node.js 20代码上传方式:上传 ZIP 包,选择 2.4 节打包的 photo-auth.zip请求处理程序:index.handler规格:vCPU 0.1 核、内存 128 MB、单实例并发度 1超时时间:10 秒2. 配置环境变量「配置」→「环境变量」,添加:按上面的配置,签名地址每 10 分钟更换一次,每个地址从生成起至少可用约 35 分钟。同一个时间窗口内,同一张照片的地址不变,浏览器可以直接命中缓存。建议 CDN_AUTH_VALID_SECONDS 不超过 3.6 节设置的有效时长。3. 创建 HTTP 触发器「配置」→「触发器」→「创建触发器」:变量名值说明CDN_MEDIA_BASE_URLhttps://img.example.com原图 CDN 地址CDN_AUTH_KEY配置清单中的值必须与 3.6 节的主 KEY 完全一致CDN_AUTH_VALID_SECONDS600签名时间窗口,同一窗口内同一张图的地址不变CDN_AUTH_OVERLAP_SECONDS300额外延长的有效时间,避免窗口切换时地址恰好过期ALLOW_ORIGINExample Domainexample.com允许调用签名函数的主站地址,必填,漏配时函数会直接报错触发器类型:HTTP 触发器请求方式:GET、HEAD、OPTIONS认证方式:无需认证4. 保留一个常驻实例(可选)「配置」→「弹性配置」→ 最小实例数设为 1。这样打开原图时不会遇到冷启动,但会产生每月几元到十几元的费用;设为 0 则几乎免费,只是冷启动时首张原图会慢一两秒。5. 绑定自定义域名函数计算要求先把域名解析过去,才能添加自定义域名:去云解析添加记录:主机记录 media-auth,类型 CNAME,记录值 <你的账号ID>.cn-chengdu.fc.aliyuncs.com(地域按实际替换)回到函数计算 →「域名管理」→「添加自定义域名」:域名:media-auth.example.com 协议:HTTP,HTTPS HTTPS 证书:选择 3.4 节的 media-auth.example.com 证书 路由:路径 /*,函数 afilmory-photo-auth,版本 LATEST验证一下(在任意终端执行):BASH复制curl https://media-auth.example.com/health # 返回 {"ok":true,...} 即为成功Tip不能直接用函数的默认公网地址访问:函数计算会给默认域名的响应强制加上下载头,浏览器会把返回内容当成文件下载,所以必须绑定自定义域名。3.8 函数计算:OSS 事件转发函数1. 创建函数「函数」→「创建函数」→「事件函数」:函数名称:oss-event-forwarder运行环境:Node.js 20代码上传方式:上传 ZIP 包,选择 oss-event-forwarder.zip请求处理程序:index.handler规格:vCPU 0.1 核、内存 128 MB超时时间:15 秒2. 配置环境变量变量名值WEBHOOK_HOSThook.example.comWEBHOOK_TOKEN配置清单中的值3. 创建 OSS 触发器「配置」→「触发器」→「创建触发器」:触发器类型:对象存储 OSSBucket:私有桶 example-photos-private文件前缀:photos/文件后缀:留空触发事件:oss:ObjectCreated:PutObjectoss:ObjectCreated:PostObjectoss:ObjectCreated:CompleteMultipartUploadoss:ObjectCreated:CopyObjectoss:ObjectRemoved:DeleteObject角色:使用默认角色 AliyunOSSEventNotificationRole,按提示授权Warning此时服务器还没部署好,如果现在往 photos/ 上传照片,转发函数会因为连不上 webhook 而报错重试,不影响后续使用。建议先不要上传,等第四章做完再说。3.9 核对 DNS 解析最后在 云解析 DNS 补上指向服务器的两条记录,并核对前面添加的记录,一共 5 条:TEXT复制 主机记录 │ 记录类型 │ 记录值 ──────────────┼──────────┼─────────────────────────────────────────── @ │ 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 和 OpenRestySSH 登录服务器,执行 1Panel 官方一键安装脚本(以 1Panel 官方文档 的最新命令为准):BASH复制bash -c "$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"安装结束后会显示面板地址、端口、用户名和密码。回到轻量服务器控制台的「防火墙」,放行这个面板端口浏览器登录 1Panel →「应用商店」→ 安装 OpenResty4.2 在 1Panel 中配置证书申请「网站」→「证书」→「Acme 账户」→ 创建一个账户(填邮箱,CA 选 Let's Encrypt)「网站」→「证书」→「DNS 账户」→ 创建,类型选「阿里云」,填入 afilmory-acme 子账号的 AccessKey「网站」→「证书」→「申请证书」,分别为 hook.example.com 和 comment.example.com 申请:验证方式选「DNS 账号」,勾选「自动续签」1Panel 会自动续期这两张证书。4.3 安装 Waline 评论「应用商店」→ 搜索 Waline → 安装,数据库使用默认的 SQLite,记下端口(默认 8360)「网站」→「创建网站」→「反向代理」:主域名:comment.example.com代理地址:http://127.0.0.1:8360进入该网站 →「HTTPS」→ 启用,选择 4.2 节申请的证书浏览器打开 https://comment.example.com/ui/register 注册账号,第一个注册的账号自动成为管理员配置环境变量在 1Panel「应用商店」→「已安装」→ Waline →「参数」(或编辑 compose 文件的 environment),按需添加以下变量,保存后重建应用:BASH复制# 只允许你的主站使用这个评论服务 SECURE_DOMAINS=example.com # 站点信息(通知邮件里会用到) SITE_NAME=我的相册 SITE_URL=https://example.com配置邮件通知(可选)阿里云服务器默认禁止 25 端口,需要使用 465 端口(SSL)的 SMTP 服务。以 QQ 邮箱为例:登录 QQ 邮箱网页版 →「设置」→「账号」→ 开启「POP3/SMTP 服务」,按提示获取授权码(不是 QQ 密码)为 Waline 添加环境变量:BASH复制SMTP_SERVICE=QQ SMTP_USER=你的QQ号@qq.com SMTP_PASS=授权码 SENDER_NAME=我的相册 AUTHOR_EMAIL=你的QQ号@qq.com网易 163 邮箱把 SMTP_SERVICE 换成 163,在 163 邮箱设置里开启 SMTP 并获取授权码即可。Gmail 等境外邮箱在国内服务器上通常连不上,不建议使用。如果不想用邮件,也可以设置 WEBHOOK=https://你的接收地址,Waline 会在收到新评论时向这个地址发送 POST 请求,可以对接自己的通知服务。4.4 创建 webhook 反向代理网站「网站」→「创建网站」→「反向代理」:主域名:hook.example.com代理地址:http://127.0.0.1:3002创建后进入该网站 →「HTTPS」→ 启用,选择 hook.example.com 的证书。webhook 服务在 4.8 节启动,现在访问会返回 502,属于正常现象。4.5 安装构建工具回到 SSH 终端。1. 基础软件与 swapBASH复制apt 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/fstab2. Node.js 22、pnpm、pm2BASH复制curl -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 -vpnpm 的版本由仓库的 package.json 锁定,进入项目目录后第一次执行 pnpm 时会自动下载对应版本。3. ossutil(发布网站用)BASH复制curl https://gosspublic.alicdn.com/ossutil/install.sh | bash ossutil configossutil config 会依次询问:配置文件路径(直接回车)、语言、Endpoint(填 https://oss-cn-chengdu.aliyuncs.com)、AccessKey ID 和 Secret(填 afilmory-publisher 的),STS Token 留空。BASH复制# 验证:能列出公开桶即为成功 ossutil ls oss://example-siteWarning构建脚本使用的是 ossutil 1.x 的命令格式(安装脚本装的就是 1.x)。如果你装的是 2.x,cp、set-meta 的参数格式不同,发布步骤会报错4. 阿里云 CLI(更新签名函数用)BASH复制/bin/bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)" aliyun configure --profile afilmory-fc --mode AK aliyun plugin install --names fcaliyun configure 时填入 afilmory-fc-deployer 的 AccessKey,Region 填 cn-chengdu。BASH复制# 验证:能返回函数信息即为成功 aliyun fc get-function --profile afilmory-fc --region cn-chengdu --function-name afilmory-photo-auth4.6 拉取代码并填写配置1. 拉取代码并安装依赖BASH复制mkdir -p /root/projects && cd /root/projects git clone https://github.com/<你的GitHub用户名>/afilmory.git cd afilmory git checkout private-oss pnpm installpnpm install 会自动创建 config.json 和 builder.config.ts(从模板复制),这两个文件和 .env 都不会被提交到 Git。2. 站点配置 config.jsonBASH复制nano config.json主要修改这些字段:name、title、description:站点名称和描述url:https://example.comauthor:你的名字、主页和头像地址comments.waline.serverURL:https://comment.example.combeian:把 icp.enabled 改为 true,填入你的备案号(公安备案同理)telemetry.vibeloft:保持 false3. 构建配置 builder.config.tsBASH复制nano builder.config.ts找到并修改下面几项,降低构建时的内存占用,并让照片 ID 直接使用文件名:TypeScript复制system: { processing: { // 照片 ID 直接用文件名,不加哈希后缀 digestSuffixLength: 0, }, observability: { performance: { worker: { // 并发数等于 CPU 核数 workerCount: os.cpus().length, useClusterMode: false, workerConcurrency: 1, }, }, }, },只改这几项,其他配置保持默认。4. 构建环境变量:项目根目录 .envBASH复制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 .envS3_REGION 填地域 ID(如 cn-chengdu),S3_ENDPOINT 填带 oss- 前缀的外网 Endpoint,两者写法不同,注意区分。PHOTO_PROXY_BASE_URL 是原图私有化的开关:设置后,照片清单里的原图地址会变成签名函数的地址,而不是 OSS 的公开地址。5. 发布与 webhook 配置:deploy/aliyun-static/.envBASH复制cp deploy/aliyun-static/.env.example deploy/aliyun-static/.env chmod 600 deploy/aliyun-static/.env nano deploy/aliyun-static/.env需要修改的项:BASH复制WEBHOOK_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-auth其余项保持默认。注意这个文件同时被 Node.js 和 bash 读取,值里不要有空格,也不要在行尾写注释。4.7 首次构建并发布建议在 tmux 里执行,SSH 断开也不影响:BASH复制apt install -y tmux tmux new -s build cd /root/projects/afilmory bash deploy/aliyun-static/scripts/build-and-publish.sh脚本会依次执行:处理照片 → 构建网站 → 打包签名函数 → 发布到公开桶 → 更新签名函数。最后一行显示「构建并发布完成」即为成功。某一步失败时,详细日志在 /tmp/afilmory-build-logs/ 目录下。检查照片清单里的原图地址是否已经是签名函数的地址:BASH复制node -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%95然后打开 https://example.com,应该能看到 3.3 节上传的测试照片,点开大图也能正常加载。4.8 启动 webhookBASH复制cd /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"}浏览器打开 https://hook.example.com,可以看到构建状态页:当前状态、最近构建记录,以及每 10 秒自动刷新的日志。状态页和日志是公开的,但日志只展示照片文件名和构建步骤,报错原文、路径、密钥等信息都会被隐藏;会触发构建的接口必须携带 WEBHOOK_TOKEN。4.9 证书自动续期(推荐)hook 和 comment 两张证书由 1Panel 自动续期。CDN 上的 example.com、img.example.com 使用的是 3 个月的免费证书,可以用 acme.sh 自动续期并部署到 CDN:BASH复制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_cdnacme.sh 安装时会自动添加定时任务,到期前自动续期并部署到 CDN。media-auth.example.com 是函数计算的自定义域名,不走 CDN,而且函数计算要求使用 RSA 证书。目前需要在证书到期前手动续期,或者自己写脚本调用阿里云 CLI 更新;我自己用的自动化脚本整理好后会放到仓库里。五、手机上传照片5.1 配置手机 AppiOS 上我用的是 App Store 里的 OSS Browser(第三方 App),Android 上也有类似的 OSS 客户端。以 OSS Browser 为例:添加账号,填入 afilmory-uploader 的 AccessKey ID 和 Secret地域选择与配置清单一致进入私有桶 example-photos-private → photos 目录afilmory-uploader 只能在私有桶的 photos/ 目录下读写,即使手机丢了或者 App 出了问题,也不会影响其他资源。5.2 导出照片的注意事项导出到「文件」,不要存到「相册」:iOS 存入相册时会重新编码,EXIF 信息可能丢失,网站上就看不到相机参数Lightroom、Photoshop 导出时,确认勾选了保留元数据(包括相机信息和拍摄参数),否则同样会丢失 EXIF文件名就是网站上的照片 ID,建议起一个有意义的名字,不要重名Live Photo 需要把照片和同名视频一起上传,我自己没有测试过,欢迎反馈5.3 上传并观察构建在 OSS Browser 里上传一张照片到 photos/打开 https://hook.example.com,大约几秒后日志里会出现「收到 OSS 事件通知」「1 个照片事件:xxx.jpg」等待 30 秒后开始构建,构建时间随照片数量增加,新增一张照片通常 1 分钟以内完成刷新 https://example.com,新照片就出现了六、验证清单部署完成后,建议逐项确认:https://example.com 能打开,刷新照片详情页不会 404打开浏览器开发者工具的 Network 面板,点开一张大图:先请求 media-auth.example.com/original/... 返回 302,再请求 img.example.com/photos/...?auth_key=... 返回 200直接访问 https://img.example.com/photos/某张照片.jpg(不带 auth_key)返回 403照片信息面板能看到 EXIF 信息,评论区能正常加载https://hook.example.com/health 返回 {"status":"ok"}手机上传一张照片,约 1 分钟后出现在网站上在手机上删除这张照片,网站上的照片也随之消失函数计算控制台里,oss-event-forwarder 的调用日志显示「转发成功」七、常见问题7.1 刷新子页面显示 404 或空白检查公开桶的「静态页面」配置(3.3 节),默认 404 页必须是 index.html。7.2 用 OSS 默认域名访问时浏览器直接下载 index.html这是 OSS 的安全限制,默认域名不能直接浏览网页,必须通过绑定的自定义域名(CDN)访问。7.3 原图打不开,返回 403CDN_AUTH_KEY 与 CDN URL 鉴权的主 KEY 不一致URL 鉴权类型不是 A 方式原图 CDN 没有开启「OSS 私有 Bucket 回源」或没有完成授权,CDN 无权读取私有桶7.4 原图打不开,签名函数返回 Photo Not Found签名函数里的照片清单没有更新。检查构建状态页里「自动部署函数 photo-auth」这一步是否成功;如果失败,在服务器上查看 /tmp/afilmory-build-logs/deploy-fc.log,通常是阿里云 CLI 的 profile 名称或 afilmory-fc-deployer 的权限不对。7.5 浏览器控制台报跨域错误原图 CDN 的 Access-Control-Allow-Origin 与主站地址不一致(注意 https:// 前缀,结尾不要带 /)签名函数的 ALLOW_ORIGIN 与主站地址不一致(没有设置时函数会直接报错,调用日志里能看到 ALLOW_ORIGIN are required)7.6 上传照片后没有自动构建按顺序排查:函数计算 → oss-event-forwarder →「调用日志」:没有调用记录,检查 OSS 触发器的前缀是否为 photos/、事件是否勾选日志显示 401:函数的 WEBHOOK_TOKEN 与服务器 deploy/aliyun-static/.env 中的不一致日志显示连接失败或 502:检查 webhook 是否在运行(pm2 list)、1Panel 反向代理和证书是否正常状态页显示「忽略非照片相关事件」:检查 WEBHOOK_ALLOWED_BUCKETS 和照片扩展名7.7 构建失败状态页会显示失败的步骤,详细原因在服务器上查看:BASH复制tail -50 /root/projects/afilmory/deploy/aliyun-static/webhook/webhook.log ls /tmp/afilmory-build-logs/内存不足时进程会被系统杀掉,表现为构建突然中断,加 swap 或降低 workerCount 即可。7.8 构建成功,但网站上看不到新照片index.html 在 CDN 上有 1 分钟缓存,网站还注册了 Service Worker,稍等一会儿再强制刷新即可。八、二次开发与同步更新8.1 在本机预览和修改BASH复制cd afilmory pnpm install # 从服务器复制 config.json、builder.config.ts、.env 到本机对应位置后: pnpm build:manifest # 生成照片清单和缩略图 pnpm --filter @afilmory/web dev # 启动本地预览本地预览时,原图域名的跨域限制只允许主站访问,大图可能加载不出来;需要调试原图时,可以临时把 CDN 的 Access-Control-Allow-Origin 改为 *,调试完记得改回去。改完代码后提交并推送到你的 fork:BASH复制git add -A && git commit -m "你的修改说明" && git push8.2 更新服务器上的代码BASH复制cd /root/projects/afilmory git restore apps/web/public # 每次构建都会重新生成 favicon,先还原 git pull pnpm install --frozen-lockfile pm2 restart afilmory-webhook # 只有 webhook 代码有变化时才需要下一次上传照片触发构建时,就会使用新代码;也可以手动触发一次构建:BASH复制T=$(grep ^WEBHOOK_TOKEN= deploy/aliyun-static/.env | cut -d= -f2) curl -X POST -H "X-Webhook-Token: $T" http://127.0.0.1:3002/build8.3 同步官方仓库的更新BASH复制git remote add upstream https://github.com/Afilmory/afilmory.git git fetch upstream git merge upstream/main官方仓库更新很频繁,合并时可能遇到冲突,建议在新分支上合并、测试通过后再合回 private-oss。写在最后整套流程第一次配置确实繁琐,但配好之后几乎不用维护:修图、导出、上传,剩下的全自动完成。最后感谢 Innei 和 Afilmory 团队开源了这么好的相册项目。如果这篇文章对你有帮助,欢迎给 我的仓库 和 官方仓库 点个 Star。部署中遇到问题,欢迎在评论区留言。
写在前面
这是一篇从零开始的完整部署教程。跟着做完,你会得到一个这样的摄影相册:
项目基于开源相册 Afilmory,我在它的基础上做了原图私有化、Waline 评论和自动构建,整理在我的 fork 仓库里:specialhua/afilmory(private-oss 分支)。我的相册:afilm.site。
步骤比较多,涉及本机、阿里云控制台和服务器三个地方。为了少来回切换,文章按「在哪里操作」来组织:先在本机准备好代码和密钥,再在阿里云控制台一次性把云资源配完,最后登录服务器完成部署。每一步用到的东西,都会在前面的步骤里准备好。
文中所有域名都用 example.com 示意,存储桶名、函数名也都是示例,请全部替换为你自己的。建议先完成第二章的「配置清单」,后面照着清单填,不容易出错。
一、方案概览
日常使用是什么样的
1.2 架构
整套方案由这几部分组成:
访问一张原图的完整过程:
发布一张新照片的完整过程:
1.3 和官方版本有什么不同
官方仓库目前的方向是 SaaS、iOS App 和一键部署。如果你不想自己运维,推荐直接用 官方 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. 配置环境变量
「配置」→「环境变量」,添加:
按上面的配置,签名地址每 10 分钟更换一次,每个地址从生成起至少可用约 35 分钟。同一个时间窗口内,同一张照片的地址不变,浏览器可以直接命中缓存。建议 CDN_AUTH_VALID_SECONDS 不超过 3.6 节设置的有效时长。
3. 创建 HTTP 触发器
「配置」→「触发器」→「创建触发器」:
变量名
值
说明
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
允许调用签名函数的主站地址,必填,漏配时函数会直接报错
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 条:
到这里,阿里云控制台的配置全部完成。
四、服务器:安装 1Panel、Waline 和构建环境
这一章通过 SSH 登录服务器操作,部分步骤在 1Panel 网页面板上完成。以下命令都以 root 用户执行。
4.1 安装 1Panel 和 OpenResty
4.2 在 1Panel 中配置证书申请
1Panel 会自动续期这两张证书。
4.3 安装 Waline 评论
配置环境变量
在 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 config 会依次询问:配置文件路径(直接回车)、语言、Endpoint(填 https://oss-cn-chengdu.aliyuncs.com)、AccessKey ID 和 Secret(填 afilmory-publisher 的),STS Token 留空。
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.log,通常是阿里云 CLI 的 profile 名称或 afilmory-fc-deployer 的权限不对。
7.5 浏览器控制台报跨域错误
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。
写在最后
整套流程第一次配置确实繁琐,但配好之后几乎不用维护:修图、导出、上传,剩下的全自动完成。
最后感谢 Innei 和 Afilmory 团队开源了这么好的相册项目。如果这篇文章对你有帮助,欢迎给 我的仓库 和 官方仓库 点个 Star。部署中遇到问题,欢迎在评论区留言。