三股水
Afilmory 自建完整教程(静态版 + 多租户 SaaS 版)
本文从 Obsidian 撰写发布

Afilmory 自建完整教程(静态版 + 多租户 SaaS 版)

服务器:香港节点,Debian 12
域名:XXX.com
服务器 IP:XXX.XXX.XXX.XXX
SSH 端口:47520
七牛云存储桶:chronoframe

一、项目简介

Afilmory 是一个现代摄影画廊平台,支持从 S3/GitHub 自动同步照片、WebGL 渲染、完整 EXIF 展示、地图、Live Photo、HDR。

两种部署模式:

  • 静态版:纯静态文件,Nginx 托管,无需登录,适合个人作品展示、对外分享。
  • 多租户 SaaS 版:带后端 core 服务、数据库、OAuth,适合多用户管理、后台 CMS。

二、环境准备

服务器配置建议:内存最低 4G(推荐 8G),CPU 最低 2 核(推荐 8 核),系统盘最低 30G(推荐 50G+)。

踩坑:2G 内存构建时反复 OOM,最终升级到 8G 才成功。

安装 Node.js 22:

curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
node --version
npm --version

安装 pnpm:

npm install -g pnpm@10.19.0
pnpm --version

踩坑:如果报 syntax error near unexpected token,说明 pnpm 残留损坏,执行:

rm -rf /root/.local/share/pnpm
rm -f /usr/bin/pnpm
npm install -g pnpm@10.19.0

克隆项目:

cd /www/wwwroot
git clone --depth=1 https://github.com/Afilmory/Afilmory.git afilmory
cd afilmory

安装依赖:

pnpm config set registry https://registry.npmmirror.com
pnpm config set fetch-timeout 600000
sed -i '/"packageManager"/d' package.json
pnpm install

踩坑:package.json 里的 "packageManager": "pnpm@12.3.2" 会导致 pnpm 自动切换到一个损坏的版本,必须删除。

三、静态版部署

创建 .env 文件:

cat > /www/wwwroot/afilmory/.env << 'EOF'
S3_REGION=cn-south-1
S3_ACCESS_KEY_ID=你的七牛云AccessKey
S3_SECRET_ACCESS_KEY=你的七牛云SecretKey
S3_ENDPOINT=https://s3.cn-south-1.qiniucs.com
S3_BUCKET_NAME=chronoframe
S3_PREFIX=duomi/
S3_CUSTOM_DOMAIN=
S3_EXCLUDE_REGEX=
PG_CONNECTION_STRING=postgresql://afilmory:afilmory@127.0.0.1:5432/afilmory
GIT_TOKEN=
EOF

创建 config.json 文件:

cat > /www/wwwroot/afilmory/config.json << 'EOF'
{
"name": "你的网站名称",
"title": "你的网站标题",
"description": "网站描述",
"url": "https://XXX.com",
"accentColor": "#007bff",
"author": {

"name": "你的名字",
"url": "https://XXX.com/",
"avatar": "https://XXX.com/logo.png"

},
"social": { "github": "", "twitter": "" },
"map": ["maplibre"],
"mapStyle": "builtin",
"mapProjection": "mercator"
}
EOF

生成照片清单:

cd /www/wwwroot/afilmory
pnpm run build:manifest

踩坑:如果报大量 下载失败:duomi/xxx.jpg (60秒超时),需要修改 builder.config.default.ts:

sed -i 's/downloadConcurrency: 16/downloadConcurrency: 4/' builder.config.default.ts
sed -i 's/totalTimeoutMs: 60_000/totalTimeoutMs: 600_000/' builder.config.default.ts

构建静态版:

cd /www/wwwroot/afilmory
pnpm --filter @afilmory/web build

踩坑:必须用 build 而不是 build:serve。build 会注入 MANIFEST 全局变量,适合纯静态托管;build:serve 会请求 /api/manifest 接口,适合有后端服务。

修复 base 路径:

sed -i "s|base: BUILD_FOR_SERVER_SERVE ? '/static/web/' : '/'|base: '/'|" apps/web/vite.config.ts
pnpm --filter @afilmory/web build

用 Nginx 托管:

docker run -d \
--name afilmory-web \
-p 8086:80 \
-v /www/wwwroot/afilmory/apps/web/dist:/usr/share/nginx/html:ro \
nginx:alpine

访问:http://XXX.XXX.XXX.XXX:8086

踩坑:如果 403,重建容器即可:

docker rm -f afilmory-web
docker run -d --name afilmory-web -p 8086:80 \
-v /www/wwwroot/afilmory/apps/web/dist:/usr/share/nginx/html:ro \
nginx:alpine

更新照片流程:

cd /www/wwwroot/afilmory
pnpm run build:manifest
pnpm --filter @afilmory/web build
docker restart afilmory-web

四、多租户 SaaS 版部署

启动数据库和 Redis:

cd /www/wwwroot/afilmory
docker-compose up -d db redis

执行数据库迁移:

for f in $(ls be/packages/db/migrations/*.sql | sort); do
echo "执行: $f"
docker exec -i afilmory_db psql -U afilmory -d afilmory < "$f"
done
docker exec -it afilmory_db psql -U afilmory -d afilmory -c "\dt"

配置 systemd 服务:

cat > /etc/systemd/system/afilmory-core.service << 'EOF'
[Unit]
Description=Afilmory Core Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/www/wwwroot/afilmory
Environment=PORT=3001
Environment=NODE_ENV=production
Environment=BETTER_AUTH_SECRET=你的随机密钥
Environment=BETTER_AUTH_URL=https://root.XXX.com
ExecStart=/usr/bin/node be/apps/core/dist/main.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable afilmory-core
systemctl start afilmory-core

踩坑:BETTER_AUTH_SECRET 必须设置,否则报错 BetterAuthError: You are using the default secret。用 openssl rand -hex 32 生成。

构建并复制前端静态文件:

cd /www/wwwroot/afilmory
pnpm --filter @afilmory/dashboard build:prod:saas
sed -i "s|base: isSaas ? 'https://static.afilmory.art/platform/' : '/platform/',|base: '/platform/',|" be/apps/dashboard/vite.config.ts
pnpm --filter @afilmory/dashboard build:prod:saas
mkdir -p be/apps/core/dist/static/web
mkdir -p be/apps/core/dist/static/dashboard
cp -r apps/web/dist/* be/apps/core/dist/static/web/
cp -r be/apps/dashboard/dist/* be/apps/core/dist/static/dashboard/

设置 baseDomain:

docker exec -it afilmory_db psql -U afilmory -d afilmory

INSERT INTO system_setting (id, key, value, is_sensitive, created_at, updated_at)
VALUES (gen_random_uuid(), 'system.domain.base', '"XXX.com"'::jsonb, false, NOW(), NOW())
ON CONFLICT (key) DO UPDATE SET value = '"XXX.com"'::jsonb;
\q

踩坑:value 字段是 JSON 类型,字符串必须写成 '"XXX.com"'::jsonb,注意单引号嵌套。

重置 root 账号(如果密码丢失):

docker exec -it afilmory_db psql -U afilmory -d afilmory

DELETE FROM system_setting WHERE key = 'system.app.initialized';
DELETE FROM auth_user WHERE email = 'root@local.host';
\q

systemctl restart afilmory-core
sleep 10
journalctl -u afilmory-core -n 100 --no-pager | grep -A 5 "Root dashboard"

日志会打印新的 root 密码,只显示一次,务必保存。

启动 core 服务:

systemctl restart afilmory-core
systemctl status afilmory-core
ss -tlnp | grep 3001

五、域名与反向代理配置

DNS 解析需要添加:

记录类型主机记录记录值说明
A@服务器 IP主域名
Aroot服务器 IP后台管理
Astatic服务器 IP静态资源
CNAME*服务器 IP泛解析(租户子域名)

root.XXX.com 站点配置:

server {

listen 80;
server_name root.XXX.com;
location / {
    proxy_pass http://127.0.0.1:3001;
    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;
}

}

static.XXX.com 站点配置(关键):

server {

listen 443 ssl;
server_name static.XXX.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /platform/ {
    alias /www/wwwroot/afilmory/be/apps/core/dist/static/dashboard/;
    try_files $uri $uri/ /platform/index.html;
}
location / {
    proxy_pass http://127.0.0.1:3001;
    proxy_set_header Host $host;
}

}

踩坑:SaaS 模式会把前端资源指向 static.XXX.com,必须给这个子域名配 SSL 证书和静态文件映射,否则页面白屏。

申请通配符证书:宝塔 → 网站 → SSL → Let's Encrypt → DNS 验证 → 域名填 *.XXX.com。

六、踩坑记录汇总

问题原因解决方案
Docker 构建超时服务器访问 npm 源慢用淘宝镜像 + 增加超时时间
OOM 内存不足2G 内存不够升级到 8G
pnpm 报 12.3.2 语法错误packageManager 字段强制切换版本删除该字段
MANIFEST is not defined用了 build:serve 而不是 build静态版用 build
页面白屏,JS 404base 路径带 /static/web/改成 /
Workspace unavailable租户解析失败设置 baseDomain
Failed to provision rootBETTER_AUTH_SECRET 未设置systemd 里加环境变量
dashboard 资源指向 static.afilmory.art官方 CDN 地址改 vite.config.ts 的 base
图片下载超时并发太高 + 超时太短降并发 + 增超时
Nginx 403dist 被构建清空重建容器或重启 Nginx
静态资源证书错误static.XXX.com 没证书申请通配符证书
系统盘爆满Docker 构建缓存 + node_modulesdocker system prune -fa

七、日常维护命令

静态版更新照片:

cd /www/wwwroot/afilmory
pnpm run build:manifest
pnpm --filter @afilmory/web build
docker restart afilmory-web

多租户版管理:

systemctl status afilmory-core
systemctl restart afilmory-core
journalctl -u afilmory-core -f
ss -tlnp | grep 3001

数据库管理:

docker exec -it afilmory_db psql -U afilmory -d afilmory
SELECT id, slug, name FROM tenant;
SELECT id, email FROM auth_user;
SELECT key, value FROM system_setting;
\q

清理磁盘:

docker system prune -fa
docker builder prune -af
df -h /
du -sh /www/wwwroot/afilmory/*

八、七牛云 S3 配置参考

配置项值
Providers3
Bucketchronoframe
Regioncn-south-1
Endpointhttps://s3.cn-south-1.qiniucs.com
Access Key ID你的AccessKey
Secret Access Key你的SecretKey
Prefixduomi/ 或 afilmory/
Custom Domain留空(私有空间)

七牛云私有空间 + 不绑 CDN 也可以用,core 服务会代理图片访问。

九、访问地址汇总

服务地址
静态版前台http://XXX.XXX.XXX.XXX:8086
多租户后台登录https://root.XXX.com/platform/root-login
多租户前台https://.XXX.com
静态资源 CDNhttps://static.XXX.com/platform/

文档结束

😊
提交