独立博客或前端网站在加载大图片时,如果直接把几兆的原图扔给浏览器,不仅页面加载极慢,还会白白消耗服务器带宽与用户的手机流量。
如果每张图片都手动在本地 PS 裁剪导出 WebP,维护成本又太高。最优雅的架构是引入动态图片代理中间件:原始大图安稳存在对象存储(如 Cloudflare R2 / S3)中,前端页面根据不同终端的显示容器尺寸,通过 URL 参数请求经过实时裁剪、缩放、压缩并转换为 WebP 的轻量图片。
Imgproxy 是一款基于 libvips 的极速图片处理服务,内存开销极小且速度远超 ImageMagick。它通过 HMAC-SHA256 签名机制杜绝恶意攻击者伪造参数爆刷服务器算力。
这篇笔记记录用 Docker Compose 部署 Imgproxy、配置 Nginx 边缘缓存分担 CPU 压力、URL 签名脚本编写与接入验证。
为什么网站需要动态图片压缩代理
完成本教程后,你将获得以下生产级优化:
- 体积暴减 80%~90%:高分辨率 4K/2K 原始大图,在保证肉眼无损画质的前提下,自动转码为 WebP/AVIF,体积从数兆压缩至几十或上百 KB;
- URL 即参数的灵活性:无需在服务器存储多套缩略图,通过改变 URL 路径参数即可实时完成智能裁切(Crop)、限宽缩放(Resize)、模糊(Blur)、水印(Watermark);
- 极速与低内存占用:底层由 C 语言
libvips驱动,处理单图仅需数毫秒,内存占用远低于传统 ImageMagick; - 防御级安全机制:配置 HMAC-SHA256 签名与域名源站白名单,彻底杜绝服务器被外部恶意扫描盗刷为公共图片代理。
准备工作
在开始部署前,先集中定义涉及的核心变量。请根据你的生产拓扑替换对应值:
# ================= 全局参数定义 =================
DOMAIN="imgproxy.0000996.xyz" # Imgproxy 对外访问域名 (已解析至本服务器)
ALLOWED_SOURCE="https://img.0000996.xyz/" # 允许拉取原图的图床或存储桶前缀
DATA_DIR="/opt/imgproxy" # 配置文件与 Nginx 缓存存放根目录
PORT="8080" # 容器内部监听端口 (本地回环暴露)
# ================================================
生成 HMAC-SHA256 签名密钥防盗刷
Imgproxy 如果在公网裸奔(未开启签名),任何攻击者都可以把你的域名当作免费图片中转与算力肉鸡,构造恶意超大畸形图片直接耗尽服务器 CPU。
Imgproxy 采用 Key + Salt 双重十六进制签名。请在部署前使用 OpenSSL 生成两个高强度的随机 Hex 字符串:
服务器端:
# 1. 创建规范持久化目录
sudo mkdir -p /opt/imgproxy/cache
cd /opt/imgproxy
# 2. 生成 64 位 HEX Key 与 Salt 并妥善记录
HEX_KEY=$(openssl rand -hex 32)
HEX_SALT=$(openssl rand -hex 32)
echo "KEY : $HEX_KEY"
echo "SALT: $HEX_SALT"
(请将屏幕输出的 Key 与 Salt 妥善保存到密码管理器,稍后写入环境变量)
Docker Compose 部署与内存资源限制
在生产配置中,除了签名密钥外,必须做好以下关键调优:
– IMGPROXY_ALLOWED_SOURCES:白名单限制。只允许 Imgproxy 处理来自指定图床(如你的 Cloudflare R2、OSS 或本地站点)的图片,拒绝对外网任意 URL 处理;
– IMGPROXY_AUTO_WEBP / IMGPROXY_AUTO_AVIF:智能协商。当访问客户端的浏览器支持 WebP/AVIF 时,服务端自动在响应头协商输出最佳格式,URL 无需显式修改后缀;
– IMGPROXY_CONCURRENCY:限制并发线程数,防止多图并发突发跑满多核 CPU。
服务器端:
sudo tee /opt/imgproxy/docker-compose.yml << EOF
version: '3.8'
services:
imgproxy:
image: darthsim/imgproxy:latest
container_name: imgproxy
restart: always
ports:
- "127.0.0.1:8080:8080" # 仅绑定本地回环,交由 Nginx 缓存层代理
environment:
# --- 安全签名防护 (填入刚才生成的 Key 与 Salt) ---
- IMGPROXY_KEY=${HEX_KEY}
- IMGPROXY_SALT=${HEX_SALT}
# --- 上游源站域名白名单 (防止被当作全网公共代理) ---
- IMGPROXY_ALLOWED_SOURCES=https://img.0000996.xyz/,https://blog.0000996.xyz/
# --- 格式与画质智能协商 ---
- IMGPROXY_QUALITY=85 # 视觉无损推荐画质 (75-85 最佳平衡)
- IMGPROXY_AUTO_WEBP=true # 客户端支持时自动输出 WebP
- IMGPROXY_AUTO_AVIF=true # 客户端支持时自动输出更高压缩比的 AVIF
- IMGPROXY_ENFORCE_WEBP=false
# --- 算力与内存保护基线 ---
- IMGPROXY_CONCURRENCY=4 # 最大并发处理线程
- IMGPROXY_MAX_SRC_RESOLUTION=30 # 最大支持 3000 万像素源图,防御超大图攻击
- IMGPROXY_DOWNLOAD_TIMEOUT=10 # 下载原图超时 10 秒
- IMGPROXY_TTL=2592000 # 客户端缓存 30 天 (Cache-Control)
resources:
limits:
memory: 1024M # 限制单容器内存不超过 1GB
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "3"
EOF
启动容器并核验日志
服务器端:
sudo docker compose up -d
sudo docker compose logs -n 10 imgproxy
核验标准:日志显示 Server started on :8080,无任何签名缺失报警。
配置 Nginx 本地缓存层分担 CPU 压力
Imgproxy 属于计算密集型服务。如果每次用户刷新网页,服务器都要用 CPU 重新解码、缩放、编码一次图片,遭遇并发爬虫时 CPU 会瞬间飙到 100%。
解决方案:在宿主机 Nginx / OpenResty 前置配置 proxy_cache。图片首次请求由 Imgproxy 计算生成,随后结果直接缓存在磁盘上,后续请求纯走静态 I/O,毫秒响应!
1. 配置 Nginx 站点与缓存空间
在宿主机 /etc/nginx/conf.d/imgproxy.conf 写入反代与缓存配置:
服务器端:
# 1. 声明 1GB 的图片静态缓存池 (30 天不访问自动清理)
proxy_cache_path /opt/imgproxy/cache levels=1:2 keys_zone=IMG_CACHE:50m max_size=5g inactive=30d use_temp_path=off;
server {
listen 80;
server_name imgproxy.0000996.xyz;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name imgproxy.0000996.xyz;
# SSL 证书配置
ssl_certificate /etc/nginx/ssl/imgproxy.0000996.xyz.crt;
ssl_certificate_key /etc/nginx/ssl/imgproxy.0000996.xyz.key;
ssl_protocols TLSv1.2 TLSv1.3;
# 客户端上传与接收大图缓冲配置
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:8080;
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;
# 核心:开启 Nginx 磁盘级长效缓存
proxy_cache IMG_CACHE;
# 必须带上 Accept 标头参与缓存哈希,确保 WebP 与原图客户端各自命中正确版本
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept";
proxy_cache_valid 200 302 30d;
proxy_cache_valid 404 1m;
# 在响应头中回显缓存命中状态 (HIT / MISS / EXPIRED)
add_header X-Cache-Status $upstream_cache_status;
add_header Cache-Control "public, max-age=2592000, immutable";
}
}
2. 测试配置并重载
服务器端:
sudo nginx -t && sudo systemctl reload nginx
URL 签名生成算法与 Python 脚本
Imgproxy 的标准安全访问 URL 遵循以下规范:
https://<你的域名>/<signature>/<processing_options>/<source_url>
<signature>:使用前面配置的KEY和SALT,对/<processing_options>/<source_url>计算的 HMAC-SHA256 签名(截取 Base64 前 32 位或完整值);<processing_options>:处理指令,如rs:fill:800:600(缩放填充为 800×600)、q:80(画质 80);<source_url>:原图公网地址,建议采用 URL-Safe Base64 编码。
🐍 附送:Python 签名一键生成器
我们在宿主机创建实用签名脚本 /usr/local/bin/gen-imgproxy-url,无论是日常写博客还是配合自动化流水线,一行命令秒出链接:
服务器端:
sudo tee /usr/local/bin/gen-imgproxy-url << 'EOF'
#!/usr/bin/env python3
import hmac
import hashlib
import base64
import sys
# 填入第一步生成的配置
KEY = "REPLACE_WITH_YOUR_KEY"
SALT = "REPLACE_WITH_YOUR_SALT"
DOMAIN = "https://imgproxy.0000996.xyz"
def sign_url(image_url, options="rs:fill:1200:0:0/q:82"):
key_bytes = bytes.fromhex(KEY)
salt_bytes = bytes.fromhex(SALT)
# 1. 对目标图片 URL 进行 urlsafe base64 编码
encoded_url = base64.urlsafe_b64encode(image_url.encode()).decode().rstrip("=")
# 2. 组装待签名路径
path = f"/{options}/{encoded_url}".encode()
# 3. 计算 HMAC-SHA256 签名
h = hmac.new(key_bytes, salt_bytes + path, hashlib.sha256)
signature = base64.urlsafe_b64encode(h.digest()).decode().rstrip("=")
return f"{DOMAIN}/{signature}{path.decode()}"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: gen-imgproxy-url <原图公网URL> [处理参数, 默认: rs:fill:1200:0:0/q:82]")
sys.exit(1)
url = sys.argv[1]
opts = sys.argv[2] if len(sys.argv) > 2 else "rs:fill:1200:0:0/q:82"
print(sign_url(url, opts))
EOF
sudo chmod +x /usr/local/bin/gen-imgproxy-url
动态压缩效果与缓存命中率验证
1. 基础处理与体积比对测试
在本地电脑终端发起测试请求,对比原始图与处理后的图片体积:
本地电脑:
# 1. 原始图片大小探测 (假设是一张 3.5MB 的高清风景原图)
curl -sI "https://img.0000996.xyz/2026/08/fj/01.jpg" | grep -i "content-length"
# 2. 经过 Imgproxy 限宽 1200px、质量 82 处理的链接
curl -sI -H "Accept: image/webp" "<生成的签名URL>"
验证输出判定:
HTTP/2 200
content-type: image/webp
content-length: 142850 <--- 仅约 140KB,相比原图骤降 95%!
x-cache-status: MISS <--- 首次请求 MISS
2. Nginx 磁盘缓存命中判定
再次发起相同的查询:
本地电脑:
curl -sI -H "Accept: image/webp" "<生成的签名URL>" | grep -i "x-cache-status"
验证输出:
X-Cache-Status: HIT
当看到 HIT 时,说明图片已经由宿主机 Nginx 缓存接管,CPU 消耗为零,响应延迟压至 5~15ms!
常见问题与排障
Q1:访问图片返回 403 Forbidden: Signature mismatch?
- 核心排查:
1. 签名脚本里的KEY和SALT与 compose 环境变量中的值是否 100% 一致(注意十六进制不能有多余空格);
2. URL 编码是否使用了标准urlsafe_b64encode,末尾的等号=必须剥离(rstrip("="))。
Q2:访问图片返回 404 Not Found: Source is not allowed?
- 根因:原图域名没有在
IMGPROXY_ALLOWED_SOURCES白名单中。 - 排错方案:检查 compose 中的环境变量,确保包含了你的原图域名前缀(例如
https://img.0000996.xyz/,末尾务必带/)。
Q3:为什么部分用户的 Safari 浏览器看不到 WebP?
- 自愈机制:老版本 Safari 仅支持 JPEG/PNG,而现代浏览器支持 AVIF/WebP。
- 关键点:由于我们配置了 Nginx
proxy_cache_key携带$http_accept,Nginx 会自动为不同浏览器客户端缓存对应版本的图片,绝不发生错乱。
还没有人在这篇手记留下纸条,来做第一个亮灯的人吧。