摘要

写博客最烦的是配图:贴第三方图床怕跑路、怕外链被盗刷;放本地又怕备份麻烦。这篇记录一次完整的自建图床落地:CT702 上 Docker 部署 EasyImage 2.0-F 2.9.1,经 nps 隧道(VPS 28504 → 192.168.0.127:8090)公网化,域名 img.028000.xyz 挂上 Let's Encrypt 证书。重点写了三个真实踩坑:web 安装向导走不通(POST 请求被回环)、容器内 nginx 生成 http 跳转、改配置后必须重启容器清 OPcache。另外 API 上传的字段细节(字段名 image、token 放 POST body 而非 query)也一并记下,省得下次再翻源码。

目录

一、为什么自建图床

博客配图无非三条路:第三方免费图床、对象存储(OSS/S3)、自建。第三方图床的问题是"命不由己"——外链政策说变就变,图片说没就没,拿来做博客配图等于把命脉交出去;对象存储要花钱,还要处理防盗链和 CDN 配置。自建图床的代价是一次部署,换来的是完全自主:域名是自己的、数据在自己 NAS 备份体系里、上传策略自己定。对"折腾 · 记录 · 分享"这个博客定位来说,自建是唯一顺手的选项。

选型上定了 EasyImage 2.0-F(2.9.1 版),PHP 写成的轻量图床,Docker 一把梭,功能覆盖上传、管理、API、外链全套,够用不臃肿。

二、EasyImage 是什么:目录结构与工作原理

动手前先了解它是个什么东西,免得后面改配置像摸黑。EasyImage 本质是一个 PHP 应用,跑在"容器内自带 nginx + PHP-FPM"的组合里,对外只暴露一个 80 端口。核心目录:

路径(容器内视角) 作用
/app/config/config.php 主配置文件:站点名、域名、上传策略、登录开关,全部集中在这里
/app/config/api_key.php API token 存储,格式是 $tokenList=Array('TOKEN'=>Array(...)) 的变量赋值
/app/i/ 上传的图片实际存放目录,数据卷要持久化它
/app/install/ web 安装向导,初始化完成后必须删除

请求流程很直白:浏览器 → 容器内 nginx → PHP-FPM → 读 config.php 按规则处理上传 → 文件落盘到 /app/i/ → 返回图片 URL。理解这个链条,后面"改配置不生效"(OPcache 缓存了 config.php)、"http 跳转"(容器内 nginx 生成的)两个坑就都好理解了——它们都发生在链条的固定环节上。

三、部署:CT702 上的 Docker Compose

EasyImage 部署在 CT702(docker-lab 容器)上,端口 8090,数据卷落在宿主 /opt/easyimage 下,分 config 和 i 两个目录,分别对应配置和图片存储。

# /opt/easyimage/docker-compose.yml(结构示意)
services:
  easyimage:
    image: easyimage:2.9.1
    ports:
      - "8090:80"
    volumes:
      - /opt/easyimage/config:/app/config
      - /opt/easyimage/i:/app/i

数据卷拆成两个是有讲究的:config 存配置,i 存图片。以后升级版本,只换镜像,两个卷原样挂回去,配置和图片都不会丢。图片目录 /opt/easyimage/i 要纳入备份视野(和 NAS 备份体系衔接,见后文)。

docker compose up -d 之后,先在内网验证:curl http://192.168.0.127:8090 能返回首页,说明容器本身是健康的,再往下走公网化。

四、初始化:web 向导走不通,那就直写配置

EasyImage 官方流程是打开 /install 走 web 安装向导,填管理员账号、站点信息,点几下完成初始化。但在这套环境里,向导的 POST 请求存在回环问题,提交走不通,页面转圈无响应。

排查思路:向导本质上就是往 config.php 里写几行配置。既然 HTTP 这条路不通,那就绕过它——直接编辑配置文件,效果完全等价。具体写了这些:

配置项 值 说明
站点品牌 028000 · 图床 前台标题
domain / imgurl https://img.028000.xyz 对外域名,生成图片 URL 时用
mustLogin 1 游客禁止上传,必须登录
apiStatus 1 开启 API 上传
API token 重新生成的新 token 旧 token 作废

另外还做了几件事:定制了一套樱粉 CSS(和博客主站风格统一,图床也是"门面"的一部分)、开启上传日志、删除 /install 目录(向导留着就是攻击面)。

关键动作:改完配置必须 docker restart easyimage。 PHP 的 OPcache 会把 config.php 缓存在内存里,不重启容器,改了配置也看不到效果。这个坑极其隐蔽——文件明明改对了,页面就是不变,第一次遇到很容易怀疑人生。记住:EasyImage 改配置 = 改文件 + 重启容器,两步缺一不可。

五、公网化:nps 隧道 + nginx + HTTPS

图床是博客配图用的,Markdown 里贴的是公网 URL,所以它必须公网可达。链路和博客主站同构:

公网用户 → https://img.028000.xyz
  → VPS 443(nginx,LE 证书)
  → 127.0.0.1:28504(nps 隧道)
  → 192.168.0.127:8090(CT702 容器)

nps 侧新增一条隧道任务:VPS 28504 → 192.168.0.127:8090。VPS 的 nginx 新建 img.028000.xyz 站点:

# /etc/nginx/sites-available/img.028000.xyz(关键片段)
server {
    listen 443 ssl http2;
    server_name img.028000.xyz;

    # TLS 仅 1.2/1.3,HSTS
    ssl_protocols TLSv1.2 TLSv1.3;

    # 限流:登录接口与 API 接口分开限
    # /admin 5 次/分钟,/api/ 60 次/分钟

    location / {
        proxy_pass http://127.0.0.1:28504;
        # 修复容器内 nginx 生成的 http:// 跳转
        proxy_redirect http:// https://;
    }
}

证书用 certbot 签发 Let's Encrypt(有效期至 2026-12-30),自动续期。续期后有个衔接动作:deploy-hook 把新证书拷贝到宝塔面板的 cert 目录并 reload nginx,保证面板侧和 nginx 侧的证书一致。

proxy_redirect http:// https://; 这一行值得单独解释:EasyImage 容器内的 nginx 在处理某些路径(比如 /admin 补斜杠成 /admin/)时会生成 http:// 开头的跳转。公网用户走的是 HTTPS,收到 http 跳转会被浏览器拦截或降级。最省事的修法是在 VPS 的 nginx 这一层统一把 http:// 重写成 https://,不用进容器改配置。PHP 应用在反代后端生成 http 跳转是常见病,这个 server 级 proxy_redirect 是通用解法。

安全策略上,图床和博客主站看齐:/admin 登录接口 5 次/分钟限流,/api/ 接口 60 次/分钟限流,TLS 只开 1.2/1.3,HSTS 开启。图床的上传接口是公开暴露的,限流是必须的。

六、验收清单:从 301 到 API 上传

公网全链路验收,逐项过:

测试项 预期 实测
HTTP 访问 301 跳转到 HTTPS 通过
首页 品牌「028000 · 图床」+ 樱粉美化正常显示 通过
/admin 登录页 正常打开 通过
无 token 调 API 被拒绝 通过
错误 token 调 API 被拒绝 通过
正确 token 上传 200,返回 https 开头的图片地址 通过
返回的图片地址 公网可直接加载 通过
测试图片 上传后删除,不留垃圾 已删

特别要提的是"带 token 上传返回 200 且图片地址公网可加载"这一项:它一次性验证了 nps 隧道、nginx 反代、容器内 PHP、图片落盘、URL 生成五个环节,任何一个环节出问题,这个断言都过不了。验收图床,测这一项就够了,其他都是顺带的。

七、API 上传的字段细节

博客写文章时配图走 API 上传,字段细节不记下来,下次还得翻源码。EasyImage 的 API 有两个容易错的地方:

  1. 文件字段名是 image,不是 file,不是 upload。写错了服务端直接当没收到文件。
  2. token 放 POST body 里,不是 query 参数。?token=xxx 这种写法不认。
# 正确的 API 上传姿势
curl -X POST https://img.028000.xyz/api/index.php \
  -F "token=<你的API_TOKEN>" \
  -F "image=@/path/to/cover.png"
# 成功返回 JSON,里面带图片的 https 地址

另外注意 config 目录下 api_key.php 的格式:$tokenList=Array('TOKEN'=>Array(...)),是变量赋值不是 return,所以 PHP 里 require 它只会返回 1,要用 $tokenList 变量再 array_keys 取键。这个细节在写自动化脚本取 token 时会用到。

八、踩坑记录

坑 1:web 安装向导 POST 回环,走不通

/install 向导的 POST 请求存在回环问题,提交无响应。解法:绕过向导,直接写 config.php,效果等价。教训:安装向导只是"写配置文件的图形壳",理解它在写什么,就能在向导失效时手动完成。

坑 2:改配置不生效——OPcache

PHP OPcache 把 config.php 缓存在内存里,改完文件页面不变。解法:docker restart easyimage 清掉 OPcache。规律:EasyImage 的任何配置修改,结尾都要跟一次容器重启,把这个当成固定流程。

坑 3:容器内 nginx 生成 http:// 跳转

访问 /admin 会被 301 到 http://img.028000.xyz/admin/,浏览器直接拦截。解法:VPS nginx 的 server 级加 proxy_redirect http:// https://;,统一修正。这是 PHP 应用在反代后端的通病,记住这个解法,以后遇到同类应用直接套用。

坑 4:登录页密码是前端 SHA256

EasyImage 的登录是前端把密码做 SHA256(无盐)再提交。所以重置 admin 密码时,不是明文写进 config.php,而是先算 sha256(新密码),把哈希写进去,再重启容器清 OPcache,最后用真实登录验证"管理员登录成功"。无盐 SHA256 的强度一般,但这是应用本身的设计,配合"密码只存 NAS 私密配置、mustLogin 开启、登录接口限流"这几层,整体可接受。

九、凭据管理

图床涉及两组凭据:admin 登录密码、API token。按这套家庭数据中心的规矩:

  • 凭据明文只出现在两个地方:EasyImage 自身的 config.php(运行时必需)、NAS「私密配置」共享下的图床 README.md(归档备份);
  • 文章、日志、聊天记录里一律不出现明文;
  • API token 已经换过一次新的,旧 token 作废;
  • admin 密码也做过一次重置:config.php 里换 SHA256 新哈希 → docker restart 清 OPcache → 真实登录验出"管理员登录成功" → 新密码同步写入 NAS 归档,旧密码行替换掉;
  • NAS 归档后从 NAS 回读做 MD5 校验,确保归档件和源文件一致。

另外,图床的上传图片目录 /opt/easyimage/i 要进备份视野。图片是博客文章的命脉,丢了就是大面积 broken image。目前备份策略还在和 NAS 的整体备份衔接,至少要做到:容器重建不丢图(数据卷独立),灾难时能从备份恢复。

和 Halo 博客的联动:写文章配图工作流

图床搭起来不是为了"有个图床",是为了博客写作顺滑。这套工作流现在是这样的:

  1. 写文章时需要配图,截图/作图存到本地;
  2. 调 EasyImage 的 API 上传(字段名 image,token 放 POST body,见第七节);
  3. API 返回 https 开头的图片地址,直接贴进 Markdown;
  4. 发布后检查:图片公网可加载、无盗链问题。

可以写个小脚本把"上传→返回 Markdown 图片标签"自动化:

#!/bin/bash
# upload.sh:传图并输出 Markdown 标签
RESP=$(curl -s -X POST https://img.028000.xyz/api/index.php \
  -F "token=<你的API_TOKEN>" \
  -F "image=@$1")
URL=$(echo "$RESP" | grep -o 'https://[^"]*')
echo "![配图]($URL)"

这 20 篇技术文章的封面图,批量上传时就是这么干的。图床 + 博客 + 脚本,三件套,写作配图的摩擦降到零。以后写游记、晒装备,配图也是同一条流水线。

图床的长期维护:清理与配额

图床跑起来后,维护是细水长流。两个关键词:清理,配额。

清理:上传日志开着,定期看。测试图、传错的图、文章删掉后 orphan 的图,定期清。图床的 /i 目录只增不减,一年下来,垃圾能占三分之一。清理前先备份(删错了能找回),清理后看一眼磁盘。博客文章删了,配图还在图床上躺着,这种 orphan 图是清理的重点——可以写个脚本,对比文章里的图片 URL 和 /i 目录,找出没被引用的。

配额:mustLogin=1 挡住了游客,但挡不住"自己人"的手滑。传个 50MB 的原图当封面,图床不拦,博客加载哭。配额分两层:应用层(EasyImage 的上传大小限制,按需设,比如 10MB),习惯层(传图前先压缩,封面转 WebP)。习惯比配置管用,配置是底线,习惯是上限。

图床的终极形态:你写文章时,根本想不起来它的存在。截图、上传、贴链接,一气呵成。维护做到"无感",图床才算真正"建成"了。

十、总结

自建图床这件事,技术动作本身不复杂:Docker 跑起来、隧道打通、证书配好。但真正决定它"能不能长期用"的,是三个周边:初始化走不通时有手动改配置的能力(理解向导在干什么)、OPcache 这类隐蔽缓存有固定应对流程(改配置必重启)、凭据有归档不散落(NAS 私密配置)。EasyImage 现在稳定 serving 博客的全部配图,API 上传字段细节(image 字段、token 放 POST)记下来,下次写批量上传脚本直接抄。

相关阅读:《Halo 博客从零搭建:2.26.1 + Hao 主题全记录》、《PVE 里跑 Docker:LXC 容器 CT702 实战》。