01项目概览
ImageHub 是一个单实例、自托管的图床与画廊应用:把图片与视频放在你自己的服务器上, 通过浏览器管理,不依赖任何第三方云服务。全站需要登录,文件二进制只经过站内受保护路由输出, 公开链、私有下载、缩略图缓存都有独立策略。
它面向的是这样一群人:有一台能跑 Docker 的机器(家用小主机、NAS、云服务器都行), 想要一个「数据在自己手里、不用付费订阅、不必担心服务关停」的相册与图床, 同时又不愿意牺牲现代 Web 应用的使用体验。
设计取舍:不做社交、不做多租户 SaaS,只把「隐私、可控、运维体验」三件事做扎实。因此没有花哨的推荐流,也没有把数据托管到别人服务器上的可能。
02核心特性
私有优先
全站登录才可见;公开、友链、私有三级可见性,随时可切换,私有图片始终走后端鉴权。
多桶存储
本地磁盘 + 任意数量 S3 兼容桶(R2 / OSS / COS / MinIO),每张图记录真实位置,可任意互迁。
大文件上传
超过 16MB 自动分片,断点续传只补缺片;sha256 秒传去重,内存占用恒为单片大小。
画廊与相册
瀑布流画廊、灯箱原图、相册封面;缩略图三级缓存,大列表滚动不卡顿。
筛选与整理
按存储位置、画廊状态、相册归属组合筛选;筛选同样作用于全选,批量操作不越界。
灯箱旋转
自己的图片可在灯箱内左右旋转 90°;服务端原位重写并刷新缓存,方向立刻更正。
图文动态
朋友圈式发布,配九宫格图片;照片自动汇总进「日常记录」,带上传者与时间标记。
两级后台
超级管理员管用户、设置、迁移与备份;管理员只做内容审核,看不到他人私密内容。
存储对账
双向核对数据库与桶内对象:缺文件的记录可清理,桶内残留可收录或删除,附图片预览。
备份还原
后台一键整包备份(数据库 + 存储),管理脚本可装每日 04:00 自动备份。
03技术架构
整套系统由两个容器组成:一个 Web 容器负责页面、接口与缩略图,一个 PostgreSQL 容器负责元数据。文件默认落在 Docker 卷里,也可以外接对象存储桶。
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 前端 | Next.js 15 App Router + React 19 + Tailwind CSS 4 | 服务端渲染页面与客户端交互组件混合 |
| 鉴权 | Auth.js v5(NextAuth) | JWT 会话 + tokenVersion 版本号,可主动吊销 |
| 数据层 | Prisma 6 + PostgreSQL 16 | 生产用 PostgreSQL,本地开发用 SQLite,模型一致 |
| 存储层 | 本地磁盘 / AWS SDK S3 客户端 | 统一驱动接口,本地与远端桶可混用 |
| 图像处理 | sharp | 四档缩略图(400/800/1600/2560)+ AVIF/WebP 自适应,可选自动转 WebP |
| 限流 | ioredis(可选)/ 进程内存 | 配置 Redis 时计数落 Redis,否则单实例内存降级 |
| 邮件 | nodemailer | 后台配置 SMTP,带连接状态检测;未配置时打印到控制台 |
一次图片请求的路径
- 浏览器请求站内图片地址(如
/api/images/:id/file)。 - 中间件校验会话与路由可见性,未登录直接跳转登录页。
- 服务层查库确认图片存在、归属与可见性,再做权限判断。
- 按记录里的存储位置(本地卷或某个桶)读取二进制,流式写回响应。
- 缩略图按显示宽度取档(400 / 800 / 1600 / 2560):现代浏览器优先返回 AVIF,其余回退 WebP,并带 ETag 支持 304;命中缓存直接返回,未命中时生成后落盘复用,同时生成的张数有限流,避免把小机器的 CPU 打满。
图片二进制不直接暴露为静态目录。Nginx 里不要给 /storage 做 alias,否则私有图片的鉴权会被绕过。
04系统要求
| 项目 | 最低 | 建议 |
|---|---|---|
| 操作系统 | Linux(x86_64 / arm64)、macOS | Debian / Ubuntu LTS |
| 内存 | 1 GB | 2 GB 以上(大图批量上传更稳) |
| 磁盘 | 按图库规模自定 | 预留 2 倍于预期图片体积 |
| Docker | 20.10+ | 最新稳定版 + Compose v2 |
| 数据库 | PostgreSQL 16 | 由 compose 自带,无需单独安装 |
| Node.js | 20+(仅本地开发需要) | 22 LTS |
默认只监听 127.0.0.1,也就是只有本机能打开。这既是安全默认,也意味着你需要一台能通过 SSH 或内网访问的机器。
05安装部署
镜像由 GitHub Actions 构建并发布到 GHCR,服务器上不需要本地构建,也不需要装 Node.js。
方式一:一键安装(推荐)
curl -fsSL https://raw.githubusercontent.com/YsSZ-INK/ImageHub/main/install.sh | bash
脚本会检查并安装 Docker、拉取镜像、生成 .env(自动生成 AUTH_SECRET)、启动服务,最后引导你注册站长账号。交互问答约 6 项,全程可以回车走默认值。
方式二:Docker Compose
git clone https://github.com/YsSZ-INK/ImageHub.git cd ImageHub cp .env.example .env # 修改 SITE_URL 与 AUTH_SECRET docker compose pull web docker compose up -d curl http://localhost:3000/api/health # {"ok":true,"db":"up"}
AUTH_SECRET 必须自己生成:openssl rand -base64 32。缺失、为默认值或长度不足时应用会直接启动失败 —— 这是刻意设计,避免人人可伪造会话。
方式三:本地开发
npm install npm run db:push # SQLite 建表(prisma/dev.db) npm run dev # http://localhost:3000
next dev 按需编译,首次打开某个页面会慢几秒,属于正常现象;要评估真实性能请用 npm run build && npm start。
数据卷
| 卷名 | 内容 | 丢失后果 |
|---|---|---|
imagehub_postgres_data | 全部元数据:用户、图片记录、相册、动态、设置 | 记录全丢,图片变成孤儿文件 |
imagehub_storage_data | 原图、视频、缩略图缓存 | 记录还在但文件打不开 |
06首次使用
-
打开站点并注册
访问
http://localhost:3000,点击注册。空库状态下第一个注册的账号自动成为超级管理员(站长),后续可以在用户管理里给其他人分配角色。 -
处理首注册门控(公网部署必看)
如果这台机器已经能被外网访问,强烈建议提前设置
SETUP_TOKEN。设置后,首次注册必须在请求里带上这个口令,否则任何人都可能抢先成为站长。# .env SETUP_TOKEN="自己定的一串口令"
-
进入站点设置
超级管理员可在「站点设置」里配置:站点名称与备案信息、上传大小上限与配额、是否自动转 WebP、存储桶、邮件 SMTP、Redis 缓存、备份与还原。
-
上传第一张图片
进入「上传」或直接在画廊拖拽文件,也可以粘贴剪贴板图片、填写图片 URL 抓取。上传完成后可在「我的图片」中调整可见性与所属相册。
07环境变量
完整清单见仓库根目录的 .env.example,每一项都有中文注释。关键项如下:
| 变量 | 必填 | 说明 |
|---|---|---|
SITE_URL | 是 | 站点对外地址,用于生成站内链接,如 https://img.example.com |
AUTH_SECRET | 是 | 会话加密密钥,openssl rand -base64 32 生成 |
POSTGRES_PASSWORD | 是 | 数据库密码,compose 用它拼接连接串 |
DATABASE_URL | 自动 | 本地开发为 file:./dev.db;Docker 由 compose 注入 |
SETUP_TOKEN | 否 | 首注册门控:空库首次注册必须携带,防公网抢注超管 |
BIND_IP | 否 | 监听地址,默认 127.0.0.1;外网直连改 0.0.0.0 |
PORT | 否 | 宿主机端口,默认 3000 |
MAX_UPLOAD_MB | 否 | 图片单文件上限天花板(最大 512),实际生效取与界面设置的较小值;视频不设上限 |
CONVERT_TO_WEBP | 否 | 是否把 jpg/png 自动转 WebP 存储,默认 false |
LOCAL_STORAGE_DIR | 否 | 本地存储目录,Docker 内固定为 /app/storage |
REDIS_URL | 否 | 限流计数落 Redis;后台开关打开时优先用设置里的地址 |
TRUST_PROXY | 否 | 部署在反向代理后设为 1,按真实客户端 IP 限流 |
改完环境变量后需要重启容器:docker compose up -d。后台里能改的设置项(邮件、Redis 地址、存储桶等)则是即时生效的。
08存储与多桶
ImageHub 的存储模型是「位置即事实」:每张图片记录里都存着它真正所在的存储位置,读取时按记录去取,而不是假定所有文件在同一个地方。
本地磁盘
默认方案。文件落在 LOCAL_STORAGE_DIR(Docker 中是卷 imagehub_storage_data),备份、迁移、迁移都不依赖网络,速度最快。
添加 S3 兼容桶
在「站点设置 → 存储」里新增存储桶,需要填写:
| 字段 | 说明 |
|---|---|
| 名称 | 自定义标识,仅用于界面区分 |
| Endpoint | 服务地址,如 R2 的 https://<account>.r2.cloudflarestorage.com |
| Bucket | 桶名 |
| Region | 区域,部分服务商填 auto 即可 |
| Access Key / Secret Key | 具备该桶读写权限的密钥 |
| 公开访问域名 | 可选,配置后公开图片可直接走 CDN 域名 |
可以添加多个桶,并指定其中一个作为新上传的默认目标。已存在的图片不会因为改默认桶而移动,它们仍按各自记录的位置读取。
在存储之间迁移
「站点设置 → 存储迁移」支持在任意两个位置之间批量搬图,流程是:读源 → 写目标 → 更新记录 → 删源。任务具备幂等性,中断后重跑不会重复处理已完成的条目。
迁移前请确认目标桶的密钥有写入权限,并保证磁盘剩余空间足够。迁移过程中不要同时执行对账删除。
09存储对账
长期运行后,数据库记录与真实文件难免出现偏差:有人手工删过桶里的文件,或者旧版本删除时漏了清理。对账面板就是用来把两边对齐的。
双向核对
| 方向 | 含义 | 可执行动作 |
|---|---|---|
| 站有桶无 | 数据库里有记录,但对应文件已不存在 | 清理记录;或把这些记录对应的旧文件从别处「移入桶内」 |
| 桶有站无 | 桶里存在文件,但没有任何记录引用它(孤儿) | 收录进站点成为正式图片;或从桶中删除 |
核对结果以左右两栏展示,点哪一边就显示哪一边的图片缩略图,支持翻页与勾选批量处理。所有删除动作都会在服务端复核引用:正被图片记录、相册封面或动态引用的对象不会被误删。
为什么有时看不到孤儿?后台有一个以「桶为事实源」的同步循环(约 30 秒一次),会把无人认领的文件自动收录成管理员图片。刚上传或刚删除的文件也会在 2 分钟内跳过判定,避免误伤正在进行的写入。
10邮件与通知
验证邮箱、重置密码、进画廊审核提醒、存储降级告警都靠邮件发出。未配置 SMTP 时,服务端会把邮件内容打印到容器日志,开发环境可直接从日志里取验证链接。
配置 SMTP
超级管理员在「站点设置 → 邮件配置」中填写,保存即生效,无需重启:
| 字段 | 说明 |
|---|---|
| SMTP 服务器 | 发信服务器地址,如 smtp.example.com;留空表示未配置 |
| 端口 | 常用 465(SSL)或 587(STARTTLS) |
| SSL / TLS | 与端口配套:465 通常开启,587 通常关闭(连接后升级加密) |
| 账号 / 密码 | 多数邮箱服务商要求填授权码,而不是登录密码 |
| 发件人 | 如 ImageHub <no-reply@example.com>,部分服务商要求与账号同域 |
连接状态
邮件配置页顶部有一枚状态徽标。打开页面、保存配置、点击「检测连接」都会重新探测一次:
- 已连接:真实完成 TCP 连接与认证握手,当前配置可用。
- 连接失败:旁边会附上服务商返回的具体错误,如授权码失效、账号被限制、端口不通。
- 未配置:SMTP 服务器为空,邮件只会打印到日志。
SMTP 是「用完即断」的协议,没有常驻会话可以长期保持,所以这里的「已连接」是即时探测结果,而不是一次性的成功记录。这样一来,第一次配好、之后因授权码被改、服务商风控或网络策略变化而失效的情况,也能一眼看出来。检测过程只做连接与认证,不会发信。
发送测试邮件
「发送测试邮件」会按当前已保存的配置真实投递一封到全部超级管理员邮箱。收到即说明整条链路(连通、认证、发件人)都是通的;失败时页面会直接显示 SMTP 的原始报错。
常见失败原因:端口与 SSL 开关不匹配(465 未开 SSL、587 硬开 SSL)、填了登录密码而非授权码、发件人域名与账号不一致被拒收、云厂商默认封禁 25 端口。
11权限模型
三个角色,职责边界清晰。第一个注册的账号是超级管理员,其余角色由站长在用户管理里分配。
| 能力 | 超级管理员 | 管理员 | 普通用户 |
|---|---|---|---|
| 用户管理 · 站点设置 · 存储迁移与备份 | 可 | — | — |
| 图片审核 · 删除违规动态与评论 | 可 | 可 | — |
| 查看「日常记录」(动态照片汇总) | 可 | 可 | — |
| 管理自己的图片、相册与动态 | 可 | 可 | 可 |
| 浏览他人私有图片 | 可 | — | 需授权 |
角色变更后,对方重新登录才会生效 —— 会话里带的是签发时的角色。
普通用户能否看到你的私有图,取决于你把图片的可见性设为「公开」「友链」还是「私有」;「私有」只有你本人和超级管理员能看。
12上传机制
三种入口
- 拖拽 / 选择文件:支持多选,并发上传并显示每个文件的进度。
- 粘贴:截图后直接
Ctrl+V即可上传。 - URL 抓取:填远程图片地址,服务端拉取后存为站内图片(带 SSRF 防护)。
分片与秒传
超过 16MB 的文件自动切分为分片上传,中断后重试只补缺失的分片。上传前先算 sha256:若库里已有同一哈希的文件,直接建立新记录并复用已有二进制,也就是「秒传」。
分片上传的整个过程服务端内存占用恒为单个分片的大小,与文件总大小无关,因此低内存机器也能传大视频。
校验与处理
- 魔数校验:按文件头判断真实类型,改扩展名伪造的文件会被拒绝。
- EXIF 剥离:默认不保留拍摄地点等隐私信息。
- 可选 WebP 转换:
CONVERT_TO_WEBP=true时把 jpg/png 转成 WebP 存储,节省空间。 - 缩略图预热:上传完成即生成 400 档的 AVIF 与 WebP,首次浏览不必等现场转码。
- 配额与上限:单文件上限取
MAX_UPLOAD_MB与界面设置的较小值;账号配额满后会拒绝上传。 - 视频:支持 mp4 / webm / mov / mkv,播放与拖动走流式 Range,不把整个文件读进内存。
13画廊与相册
画廊
瀑布流展示站点内你可见的图片,点开进入灯箱可看原图、切换上一张下一张。缩略图有三级缓存(内存 → 磁盘 → 现场生成),浏览器还会按屏幕宽度自动选最小够用的一档(手机 400、大屏 800,灯箱才用 1600/2560),所以大列表滚动时不会反复解码原图。
相册
相册是图片的自定义分组,支持设置封面。删除相册默认不会删除里面的图片,只是解除归属。相册封面在图片被删除时会自动回退到下一张。
我的图片:筛选
「我的图片」顶部有三组可组合的筛选条件,切换任意一组都会自动回到第一页:
- 存储位置:本地磁盘,或某个具体的存储桶。
- 画廊状态:已入画廊(公开且已过审)/未入画廊(含审核中、私有、友链)。
- 相册归属:按相册筛选,或只看尚未归类的图片。
筛选条件同样作用于「全选」,批量改可见性、移动相册、删除都只影响当前筛选范围内的图片。超级管理员在选择相册时还能看到括号里的相册主人,用来区分不同用户同名的「我的相册」。
灯箱与旋转
灯箱内可查看原图、翻页切换(← →)、按 Esc 关闭。自己的图片可以在灯箱顶部直接逆时针或顺时针旋转 90°:服务端按原格式重写原图并更新宽高,同时失效缩略图与原图缓存,返回列表即可看到新方向。
受重编码代价限制,旋转支持 jpg / png / webp;GIF 动图与 AVIF 不支持,视频同理。
回收站
删除图片先进回收站,此阶段文件仍保留,可以还原。回收站提供「全选所有页」的切换按钮,便于批量还原或彻底删除。彻底删除才会同时清理数据库记录与存储中的文件。
彻底删除不可撤销。若图片被动态或相册封面引用,服务端会先解除或拒绝删除,避免页面上出现破图。
14动态与日常记录
「动态」是朋友圈式的发布形式:一段文字 + 最多九张图,按时间倒序展示在 feed 里。
照片的归属规则
- 发布动态时附带的照片,只出现在「动态」与「日常记录」两处。
- 它们不会出现在「我的图片」、画廊与普通相册里,避免同一张照片在多个入口重复刷屏。
- 「日常记录」是动态照片的汇总视图,带上传者与拍摄/上传时间,仅管理员与超级管理员可见。
日常记录页也支持直接上传照片:上传后自动生成一条动态,照片随即进入汇总,适合当作团队或家庭的流水记录使用。
15备份与还原
后台整包备份
超级管理员进入「站点设置 → 备份与还原」,可一键下载整包:包含数据库导出与全部存储文件(原图、视频、缩略图)。生成的包可以直接上传还原到另一台实例。
服务器侧备份
# 在安装目录执行 bash manage.sh backup # 立即备份:db-时间戳.sql.gz + storage-时间戳.tar.gz bash manage.sh cron # 安装每日 04:00 自动备份 bash manage.sh restore # 从备份列表中选择还原
备份文件默认落在安装目录的 backups/。务必定期把它们同步到另一台机器或异地存储 —— 和站点同盘备份无法应对磁盘损坏。
16运维命令
安装完成后,安装目录里会有 manage.sh,执行 bash manage.sh 进入交互菜单,也支持子命令直接调用。
| # | 功能 | 说明 |
|---|---|---|
| 1 | 更新并清理旧镜像 | 拉取最新镜像并重启,随后清理旧层 |
| 2 | 卸载 | 删除容器与全部数据卷(谨慎) |
| 3 | 更改端口 | 修改宿主机监听端口 |
| 4 | 修改站点地址 | 同步更新 SITE_URL |
| 5 | 重置用户密码 | 按邮箱重置为 Admin123456 |
| 6 | 角色管理 | 设为超管/管理员/普通用户,保证至少保留一个超管 |
| 7 | 用户列表 | 查看邮箱、角色、状态、图片数、注册日期 |
| 8 | 服务控制 | 启动 / 停止 / 重启 |
| 9 | 实时日志 | 跟随 web 容器最后 100 行输出 |
| 10 | 备份数据 | 数据库 + 存储文件 |
| 11 | 还原数据 | 从备份列表中选择 |
| 12 | 每日自动备份 | 安装 / 移除 04:00 定时任务 |
| 13 | 对外开放切换 | 在 127.0.0.1 与 0.0.0.0 之间切换 |
| 14 | 清理旧镜像 | 查看磁盘占用并清理本项目旧镜像 |
| 15 | 站点信息 | 地址、端口、版本、数据卷位置 |
| 16 | Redis 开关 | 启用 / 停用 Redis 限流计数 |
| 17 | 数据库控制台 | 进入 psql 交互 |
| 18 | 拉取新镜像 | 只检查是否有更新,不重启 |
| 19 | 强制重建容器 | 配置文件改动后彻底重建 |
健康检查
curl http://localhost:3000/api/health # {"ok":true,"db":"up"}
这个接口同时被 compose 的 healthcheck 使用。返回非 200 说明容器活着但数据库连不上,先看 docker compose logs postgres。
17反向代理与 HTTPS
官方默认把端口绑在本机回环,这是最省心的安全姿态。要让外网访问,推荐在宿主机前面放一层 Nginx 或 Caddy,由它负责 TLS。
Nginx 示例
server { listen 443 ssl http2; server_name img.example.com; ssl_certificate /etc/letsencrypt/live/img.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/img.example.com/privkey.pem; # 视频与大文件上传,避免默认 1M 限制 client_max_body_size 512m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; 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_buffering off; proxy_request_buffering off; } }
同时把 .env 里的 TRUST_PROXY 设为 1,并把 SITE_URL 改成 https://img.example.com,然后重启容器。这样限流会按真实客户端 IP 计数,站内生成的链接也不会退回 http。
不要把 /storage 目录用 Nginx alias 直接暴露出去。那会绕过站点的鉴权,私有图片等于公开。
18安全说明
- 全站登录:页面与接口都经过中间件与会话校验,未登录无法访问任何图片。
- 会话吊销:用户表带
tokenVersion,改密码或封禁后旧会话立即失效。 - 登录限流:按 IP 与账号双维度计数,可落 Redis;反代部署请开
TRUST_PROXY以免被伪造头绕过。 - 首注册门控:
SETUP_TOKEN防止公网部署后超管被陌生人抢注。 - SSRF 防护:URL 抓取只允许 http/https,拒绝内网地址与非常规端口。
- 路径穿越防护:所有存储键做规范化校验,拒绝
..与绝对路径。 - 类型校验:上传按魔数判断真实类型,不信任扩展名与 Content-Type。
- 预览接口限制:只有超级管理员可用,且限定
images/与videos/前缀。
发现安全问题请通过 GitHub Issues 或仓库主页的联系方式反馈,不要直接公开细节。
19常见问题
必须有自己的服务器吗?
需要一台能跑 Docker 的机器,家用小主机、NAS、云服务器都可以。默认只监听 127.0.0.1,本机或反代访问最安全。
图片存在哪里?会占用多少空间?
默认存本机磁盘(Docker 卷),也可以在站点设置里加对象存储桶,新上传进桶、旧文件仍按原位置读取。图片有配额上限,超出会被拒绝。空间占用约为原图总和 + 缩略图缓存。
我的图片会被公开吗?
默认私有。只有你显式选择公开、或把图片发到图文动态时才对外可见;公开直链可作外链,私有与下载接口始终需要登录。
忘记密码怎么办?
有邮件配置时走「忘记密码」;没有时用 bash manage.sh password 按邮箱重置为 Admin123456,登录后请立即修改。
删除图片后桶里的文件还在?
回收站阶段的删除只改状态,文件仍然保留以便还原。彻底删除才会清理文件。如果确认桶里有残留,用「存储对账」筛出「桶有站无」并批量删除即可。
能从本地磁盘迁移到对象存储吗?
可以。先在设置里添加并验证桶,再用「存储迁移」把既有图片批量搬到桶里,任务幂等,中断可重跑。
升级会不会丢数据?
数据在 Docker 卷里,升级只换镜像不动卷。仍建议升级前先 bash manage.sh backup 打一份备份。
支持视频吗?
支持 mp4 / webm / mov / mkv,播放与拖动走流式 Range,不把整个文件读进内存。视频不设单文件大小上限。
20目录结构
imagehub/ ├── prisma/ # PostgreSQL / SQLite 两套 schema 与迁移 ├── src/ │ ├── app/ # 页面与 API 路由(App Router) │ ├── components/ # UI 组件 │ ├── server/ # 业务逻辑(图片 / 动态 / 迁移 / 对账 / 分片) │ ├── lib/ # 会话、校验、存储驱动、缓存、限流 │ └── middleware.ts # 访问控制 ├── install.sh # 一键安装脚本 ├── manage.sh # 站点管理脚本(19 项菜单) ├── Dockerfile ├── docker-compose.yml └── .env.example
二次开发请从 src/server 入手看业务逻辑,从 src/app/api 看接口边界,从 prisma/schema.prisma 看数据模型。
本文档对应仓库 main 分支。若界面与描述有出入,以实际版本与 .env.example 注释为准,也欢迎在 GitHub 提 issue 指出。