Published on

自建图床:零成本搭建 PicGo + SFTP + Nginx 图床方案

Authors
  • avatar
    作者
    子君
自建图床:零成本搭建 PicGo + SFTP + Nginx 图床方案

自建图床:零成本搭建 PicGo + SFTP + Nginx 图床方案

背景

我的博客图片管理经历了三个阶段:

  1. 阿里云 OSS + PicGo —— 对象存储付费,用着很爽,但长期不用就过期了
  2. Unsplash 自动封面 —— 构建时根据文章标签从 Unsplash 搜图,免费,但相关性时好时坏,有些标签根本搜不到
  3. 自建图床(本文方案)—— 服务器复用,零额外成本,图片完全受控

如果你有一台云服务器、一个博客、想摆脱第三方图床的束缚,本文方案只需要 10 分钟配置。

方案架构

┌──────────┐    SFTP (SSH)     ┌────────────────┐
│  PicGo   │ ────────────────> │  腾讯云服务器    │
│ (本机)    │   上传图片         │  139.155.183.138 │
└──────────┘                   │                │
                               │ /var/www/images/ │
                               │        │         │
                               │   Nginx          │
                               │        │         │
                               │ https://calvinhiram.top/images/
                               └────────────────┘
  • PicGo:本地图片上传客户端,支持拖拽/剪贴板上传
  • SFTP:走 SSH 协议直传服务器,无需额外开端口
  • Nginx:静态文件服务,加缓存头,支持 HTTPS

第一步:服务器端配置

1.1 创建图片存储目录

ssh root@你的服务器IP
mkdir -p /var/www/images
chown -R nginx:nginx /var/www/images

1.2 配置 Nginx

在现有的 HTTPS server 块中添加一个 location:

server {
    listen 443 ssl http2;
    server_name 你的域名;

    # ... 现有配置 ...

    # 图床 - 静态图片服务
    location /images/ {
        alias /var/www/images/;
        autoindex off;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

检查配置并重载:

nginx -t && systemctl reload nginx

1.3 验证

上传一张测试图:

echo '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
  <rect fill="#4CAF50" width="200" height="200"/>
  <text x="50" y="120" font-size="40" fill="white">OK</text>
</svg>' > /var/www/images/test.svg

访问 https://你的域名/images/test.svg,能正常显示就说明配置成功。

1.4 (可选)防盗链配置

如果担心图片被其他站点直接引用消耗带宽,可以加 valid_referers:

location /images/ {
    alias /var/www/images/;
    autoindex off;

    # 防盗链:只允许自己的域名,同时放行空 referer(RSS、直接打开)
    valid_referers none blocked 你的域名 www.你的域名;
    if ($invalid_referer) {
        return 403;
    }

    expires 30d;
    add_header Cache-Control "public, immutable";
}

说明:

  • none —— 无 Referer(浏览器直接打开、RSS 阅读器),放行
  • blocked —— 隐私策略隐藏了 Referer,放行
  • 如果你的博客是纯静态的、流量不大,个人觉得这一步非必需

第二步:PicGo 配置 SFTP 插件

2.1 安装插件

在 PicGo 中搜索安装 picgo-plugin-sftp-uploader(插件设置 → 搜索 sftp-uploader)。

版本兼容性提示:笔者实测 PicGo 2.4.x + 插件 v2.1.0 可正常工作。如果遇到 SyntaxError: Unexpected token . 说明 PicGo 版本太老,升级 PicGo 本身即可解决。

2.2 准备 SSH 免密登录

确保本机能免密 SSH 登录服务器(PicGo 通过 SFTP 插件走 SSH 协议)。如果还没有配置:

ssh-keygen -t rsa -b 4096
ssh-copy-id root@你的服务器IP

2.3 创建连接配置文件

在 C:\Users\你的用户名\.picgo\ 下创建 sftp-config.json:

{
  "myserver": {
    "host": "你的服务器IP",
    "port": 22,
    "username": "root",
    "privateKey": "C:/Users/你的用户名/.ssh/id_rsa",
    "path": "{fullName}",
    "uploadPath": "/var/www/images/{fullName}",
    "url": "https://你的域名/images/",
    "fileUser": "nginx"
  }
}

字段说明:

字段含义
host服务器 IP
usernameSSH 用户名
privateKey本机私钥路径
pathURL 路径模板,{fullName} 即原文件名
uploadPath服务器上的存储路径
url最终访问的域名前缀
fileUser上传后修改文件所有者(确保 Nginx 能读取)

2.4 配置 PicGo

打开 PicGo → 图床设置 → SFTP 上传,填入:

  • 网站标识:myserver(对应 sftp-config.json 中的 key)
  • 配置文件:C:/Users/你的用户名/.picgo/sftp-config.json

设为默认图床,完成。

2.5 (推荐)上传时自动转 WebP

安装 picgo-plugin-webp 插件,上传前自动将图片转为 WebP 格式(质量 80),体积比 PNG 小 30-50%,博客加载更快:

在 PicGo 插件设置中搜索 webp 安装即可,无需额外配置。插件会先转换格式再走 SFTP 上传,对使用流程完全透明。

2.4 (可选)Webp插件可能遇到Bug

笔者这个版本的PicGo(v2.4.0)与Webp插件有兼容性不一致bug,导致Webp转换时会有Bug

问题链:

  1. 开启了 picgo-plugin-webp 插件,它把 PNG 转成了 .webp
  2. 但转换后的 .webp 文件不存在或读取失败
  3. 导致 PicGo 的 output 数组为空
  4. SFTP 插件访问 output[0].buffer 时,output[0] 是 undefined,就报错了

打开C:\Users\CalvinHiram\AppData\Roaming\picgo\node_modules\picgo-plugin-webp\dist\index.js文件,找到beforeTransformolugins函数

可以发现WebP 插件只取首图且数组错当路径传参

  • const [tempPath] = ctx.input 解构只取了数组第一个元素,多图时其余全丢
  • cwebp(ctx.input, ...) 把整个数组当成文件路径字符串传给 cwebp,导致转换静默失败
const beforeTransformPlugins = {
      async handle(ctx) {
          const [tempPath] = ctx.input;
          if (path_1.default.extname(tempPath) === '.webp') {
              return ctx.input;
          }
          const input = changeExt(tempPath, '.webp');
          try {
              await webp_converter_1.default.cwebp(ctx.input, input, '-q 80', '-v');
              ctx.input = [input];
              return ctx;
          }
          catch (error) {
              ctx.log.error(error);
              ctx.emit('notification', {
                  title: '转webp错误',
                  body: error
              });
          }
      }
  };

修改这个函数

const beforeTransformPlugins = {
      async handle(ctx) {
          const inputs = [];
          for (const tempPath of ctx.input) {
              if (path_1.default.extname(tempPath) === '.webp') {
                  inputs.push(tempPath);
                  continue;
              }
              const input = changeExt(tempPath, '.webp');
              try {
                  await webp_converter_1.default.cwebp(tempPath, input, '-q 80', '-v');
                  inputs.push(input);
              }
              catch (error) {
                  ctx.log.error(error);
                  ctx.emit('notification', {
                      title: '转webp错误',
                      body: error
                  });
                  inputs.push(tempPath);
              }
          }
          ctx.input = inputs;
          return ctx;
      }
  };

然后重启PicGo,问题解决。

这是当时我的PicGo报错日志(日志文件位置:C:\Users\CalvinHiram\AppData\Roaming\picgo\picgo.log):

  ------Error Stack Begin------                                                                                           TypeError: Cannot read properties of undefined (reading 'buffer')                                                           at Object.n [as handle]
  (C:\Users\CalvinHiram\AppData\Roaming\picgo\node_modules\picgo-plugin-sftp-uploader\dist\index.cjs:1:8537)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async Qe.doUpload (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:20017)
      at async Qe.start (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:18788)
      at async vt.upload (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:75029)
      at async Object.upload (D:\PicGo\resources\app.asar\index.js:2:881642)
      at async gi (D:\PicGo\resources\app.asar\index.js:2:884397)
      at async D:\PicGo\resources\app.asar\index.js:2:885478
  -------Error Stack End-------
  2026-08-08 07:36:46 [PicGo ERROR]
  ------Error Stack Begin------
  TypeError: Cannot read properties of undefined (reading 'buffer')
      at Object.n [as handle]
  (C:\Users\CalvinHiram\AppData\Roaming\picgo\node_modules\picgo-plugin-sftp-uploader\dist\index.cjs:1:8537)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async Qe.doUpload (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:20017)
      at async Qe.start (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:18788)
      at async vt.upload (D:\PicGo\resources\app.asar\node_modules\picgo\dist\index.cjs.js:1:75029)
      at async Object.upload (D:\PicGo\resources\app.asar\index.js:2:881642)
      at async gi (D:\PicGo\resources\app.asar\index.js:2:884397)
      at async D:\PicGo\resources\app.asar\index.js:2:885478
  -------Error Stack End-------
  2026-08-08 07:36:46 [PicGo INFO] [PicGo Server] upload result
  2026-08-08 07:36:46 [PicGo WARN] [PicGo Server] upload failed, see picgo.log for more detail ↑

第三步:使用

在 PicGo 上传区拖入图片,上传成功后自动返回 Markdown / URL 格式链接。

博客文章引用封面图时,在 Frontmatter 中写:

images: [https://你的域名/images/my-cover.png]

每次想换封面图,打开 PicGo → 拖图 → 复制链接 → 粘贴到文章的 images 字段,搞定。

总结

维度OSSUnsplash自建图床
费用月费免费免费(复用服务器)
图片质量完全可控随机搜索结果完全可控
上手难度低低中等(需配服务器)
依赖云厂商Unsplash API自有服务器

自建图床最大的优势是零额外成本 + 完全可控。如果你已经有一台跑着博客的云服务器,这套方案几乎不需要额外维护。配合 PicGo 的拖拽上传,使用体验和商业图床几乎没有区别。