[{"data":1,"prerenderedAt":1179},["ShallowReactive",2],{"content:\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy":3,"surroundings:\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy":1170},{"id":4,"title":5,"body":6,"description":1150,"extension":1151,"meta":1152,"navigation":1164,"path":1165,"seo":1166,"stem":1168,"__hash__":1169},"content\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy\u002Findex.md","Self-Hosting an Image Proxy with imgproxy",{"type":7,"value":8,"toc":1132},"minimark",[9,14,34,41,207,212,241,245,251,257,263,269,273,278,469,473,691,695,713,717,724,858,862,872,876,879,883,886,949,953,960,964,990,994,1000,1006,1012,1018,1022,1034,1043,1055,1064,1081,1090,1094,1117,1122,1125,1128],[10,11,13],"h1",{"id":12},"how-to-self-host-an-image-proxy-with-imgproxy","How to Self-Host an Image Proxy with imgproxy",[15,16,17,18,23,24,28,29,33],"p",{},"This guide extends ",[19,20,22],"a",{"href":21},"\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002F","Image CDNs and fetchpriority",", part of ",[19,25,27],{"href":26},"\u002Fimage-media-optimization\u002F","Image & Media Optimization",". Managed image CDNs resize, convert and cache images from a URL — convenient, but priced per transformation or per request, which becomes expensive for large catalogues or user-generated content. imgproxy is an open-source, libvips-based server that does the same job: you request ",[30,31,32],"code",{},"\u002F\u003Csignature>\u002Frs:fit:800:0\u002Ff:avif\u002Fplain\u002Fs3:\u002F\u002Fbucket\u002Fphoto.jpg"," and it fetches the source, transforms it and returns the result. Put a CDN in front of it and each variant is computed once, then served from the edge.",[15,35,36,37,40],{},"Self-hosting shifts the cost from per-request fees to compute you run yourself, and gives full control over formats, quality and source locations. The responsibilities move too: you must sign URLs to prevent abuse, cap resource usage, cache correctly with ",[30,38,39],{},"Vary: Accept",", and monitor the service like any other piece of production infrastructure.",[15,42,43],{},[44,45,51,52,51,59,51,63,51,66,51,84,51,92,51,98,51,105,51,112,51,115,51,119,51,122,51,126,51,129,51,133,51,142,51,145,51,148,51,151,51,157,51,163,51,167,51,172,51,175,51,180,51,185,51,189,51,194,51,198,51,203,51],"svg",{"viewBox":46,"width":47,"role":48,"ariaLabel":49,"style":50},"0 0 760 370","100%","img","Sequence of a browser image request through a CDN to imgproxy and the source bucket on a cache miss.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[53,54],"rect",{"className":55,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],"svg-canvas","0","#ffffff",[60,61,62],"title",{},"Request path through a CDN-fronted imgproxy",[64,65,49],"desc",{},[67,68,69],"defs",{},[70,71,78],"marker",{"id":72,"viewBox":73,"refX":74,"refY":75,"markerWidth":76,"markerHeight":76,"orient":77},"fa9478035b","0 0 10 10","9","5","7","auto-start-reverse",[79,80],"path",{"d":81,"fill":82,"style":83},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[53,85],{"x":86,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":82,"style":91},"1","758","368","10","none","stroke-opacity:0.18",[93,94,62],"text",{"x":95,"y":96,"fill":82,"style":97},"28.0","34.0","font-size:16px;font-weight:700",[53,99],{"x":100,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"41.0","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[93,106,111],{"x":107,"y":108,"fill":82,"style":109,"textAnchor":110},"116.0","78.0","font-size:13px;font-weight:700","middle","Browser",[53,113],{"x":114,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"217.0",[93,116,118],{"x":117,"y":108,"fill":82,"style":109,"textAnchor":110},"292.0","CDN",[53,120],{"x":121,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"393.0",[93,123,125],{"x":124,"y":108,"fill":82,"style":109,"textAnchor":110},"468.0","imgproxy",[53,127],{"x":128,"y":101,"width":102,"height":96,"rx":103,"fill":82,"stroke":82,"style":104},"569.0",[93,130,132],{"x":131,"y":108,"fill":82,"style":109,"textAnchor":110},"644.0","Bucket",[134,135],"line",{"x1":107,"y1":136,"x2":107,"y2":137,"stroke":82,"strokeWidth":138,"strokeDashArray":139,"style":141},"90.0","348.0","1.5",[140,140],"4","stroke-opacity:0.35",[134,143],{"x1":117,"y1":136,"x2":117,"y2":137,"stroke":82,"strokeWidth":138,"strokeDashArray":144,"style":141},[140,140],[134,146],{"x1":124,"y1":136,"x2":124,"y2":137,"stroke":82,"strokeWidth":138,"strokeDashArray":147,"style":141},[140,140],[134,149],{"x1":131,"y1":136,"x2":131,"y2":137,"stroke":82,"strokeWidth":138,"strokeDashArray":150,"style":141},[140,140],[134,152],{"x1":107,"y1":153,"x2":154,"y2":153,"stroke":82,"strokeWidth":138,"style":155,"markerEnd":156},"124.0","290.0","stroke-opacity:0.6","url(#fa9478035b)",[93,158,162],{"x":159,"y":160,"fill":82,"style":161,"textAnchor":110},"204.0","117.0","font-size:12px","GET signed URL + Accept",[134,164],{"x1":117,"y1":165,"x2":166,"y2":165,"stroke":82,"strokeWidth":138,"style":155,"markerEnd":156},"164.0","466.0",[93,168,171],{"x":169,"y":170,"fill":82,"style":161,"textAnchor":110},"380.0","157.0","cache miss → forward",[134,173],{"x1":124,"y1":159,"x2":174,"y2":159,"stroke":82,"strokeWidth":138,"style":155,"markerEnd":156},"642.0",[93,176,179],{"x":177,"y":178,"fill":82,"style":161,"textAnchor":110},"556.0","197.0","fetch original",[134,181],{"x1":131,"y1":182,"x2":183,"y2":182,"stroke":82,"strokeWidth":138,"strokeDashArray":184,"style":155,"markerEnd":156},"244.0","470.0",[75,140],[93,186,188],{"x":177,"y":187,"fill":82,"style":161,"textAnchor":110},"237.0","original bytes",[134,190],{"x1":124,"y1":191,"x2":192,"y2":191,"stroke":82,"strokeWidth":138,"strokeDashArray":193,"style":155,"markerEnd":156},"284.0","294.0",[75,140],[93,195,197],{"x":169,"y":196,"fill":82,"style":161,"textAnchor":110},"277.0","AVIF 800w + Vary",[134,199],{"x1":117,"y1":200,"x2":201,"y2":200,"stroke":82,"strokeWidth":138,"strokeDashArray":202,"style":155,"markerEnd":156},"324.0","118.0",[75,140],[93,204,206],{"x":159,"y":205,"fill":82,"style":161,"textAnchor":110},"317.0","cached response",[208,209,211],"h2",{"id":210},"rapid-diagnosis","Rapid Diagnosis",[213,214,215,223,229,235],"ul",{},[216,217,218,222],"li",{},[219,220,221],"strong",{},"Estimate volume."," Count unique variants (sources × widths × formats) and monthly image requests; managed pricing scales with these.",[216,224,225,228],{},[219,226,227],{},"Check source locations."," imgproxy can read from S3, GCS, Azure, local files or HTTP origins.",[216,230,231,234],{},[219,232,233],{},"Check existing URL patterns."," A migration is easiest if templates already generate image URLs from one helper.",[216,236,237,240],{},[219,238,239],{},"Check CDN capabilities."," You need cache keys that include the format decision, or URLs that encode the format explicitly.",[208,242,244],{"id":243},"root-cause-analysis-why-teams-self-host","Root Cause Analysis: Why Teams Self-Host",[15,246,247,250],{},[219,248,249],{},"1. Cost at scale."," Per-transformation pricing dominates for catalogues with millions of images and many variants.",[15,252,253,256],{},[219,254,255],{},"2. Control."," Custom quality per content type, specific encoders, and data residency requirements.",[15,258,259,262],{},[219,260,261],{},"3. User-generated content."," Batch conversion is impractical for continuous uploads; on-the-fly transformation handles it.",[15,264,265,268],{},[219,266,267],{},"4. Vendor flexibility."," Keeping transformation separate from the CDN makes it easier to change CDNs.",[208,270,272],{"id":271},"step-by-step-resolution","Step-by-Step Resolution",[274,275,277],"h3",{"id":276},"_1-run-imgproxy-with-signing-and-limits","1. Run imgproxy with signing and limits",[279,280,285],"pre",{"className":281,"code":282,"language":283,"meta":284,"style":284},"language-yaml shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# docker-compose.yml\nservices:\n  imgproxy:\n    image: darthsim\u002Fimgproxy:latest\n    environment:\n      IMGPROXY_KEY: ${IMGPROXY_KEY}            # hex-encoded signing key\n      IMGPROXY_SALT: ${IMGPROXY_SALT}\n      IMGPROXY_USE_S3: \"true\"\n      IMGPROXY_ENABLE_AVIF_DETECTION: \"true\"   # choose AVIF from Accept\n      IMGPROXY_ENABLE_WEBP_DETECTION: \"true\"\n      IMGPROXY_MAX_SRC_RESOLUTION: \"50\"         # megapixels\n      IMGPROXY_ALLOWED_SOURCES: \"s3:\u002F\u002Fproduct-images\u002F\"\n      IMGPROXY_AVIF_SPEED: \"7\"\n      IMGPROXY_FORMAT_QUALITY: \"avif=55,webp=78,jpeg=80\"\n    ports: [\"8080:8080\"]\n# trade-off: AVIF_SPEED 7 encodes faster but produces larger files than slower\n# speeds; with a CDN in front, misses are rare, so slower speeds may be affordable.\n","yaml","",[30,286,287,295,306,314,327,335,349,360,371,385,395,409,420,431,442,457,463],{"__ignoreMap":284},[288,289,291],"span",{"class":134,"line":290},1,[288,292,294],{"class":293},"sjfSM","# docker-compose.yml\n",[288,296,298,302],{"class":134,"line":297},2,[288,299,301],{"class":300},"sZBmE","services",[288,303,305],{"class":304},"saISM",":\n",[288,307,309,312],{"class":134,"line":308},3,[288,310,311],{"class":300},"  imgproxy",[288,313,305],{"class":304},[288,315,317,320,323],{"class":134,"line":316},4,[288,318,319],{"class":300},"    image",[288,321,322],{"class":304},": ",[288,324,326],{"class":325},"sZ8jY","darthsim\u002Fimgproxy:latest\n",[288,328,330,333],{"class":134,"line":329},5,[288,331,332],{"class":300},"    environment",[288,334,305],{"class":304},[288,336,338,341,343,346],{"class":134,"line":337},6,[288,339,340],{"class":300},"      IMGPROXY_KEY",[288,342,322],{"class":304},[288,344,345],{"class":325},"${IMGPROXY_KEY}",[288,347,348],{"class":293},"            # hex-encoded signing key\n",[288,350,352,355,357],{"class":134,"line":351},7,[288,353,354],{"class":300},"      IMGPROXY_SALT",[288,356,322],{"class":304},[288,358,359],{"class":325},"${IMGPROXY_SALT}\n",[288,361,363,366,368],{"class":134,"line":362},8,[288,364,365],{"class":300},"      IMGPROXY_USE_S3",[288,367,322],{"class":304},[288,369,370],{"class":325},"\"true\"\n",[288,372,374,377,379,382],{"class":134,"line":373},9,[288,375,376],{"class":300},"      IMGPROXY_ENABLE_AVIF_DETECTION",[288,378,322],{"class":304},[288,380,381],{"class":325},"\"true\"",[288,383,384],{"class":293},"   # choose AVIF from Accept\n",[288,386,388,391,393],{"class":134,"line":387},10,[288,389,390],{"class":300},"      IMGPROXY_ENABLE_WEBP_DETECTION",[288,392,322],{"class":304},[288,394,370],{"class":325},[288,396,398,401,403,406],{"class":134,"line":397},11,[288,399,400],{"class":300},"      IMGPROXY_MAX_SRC_RESOLUTION",[288,402,322],{"class":304},[288,404,405],{"class":325},"\"50\"",[288,407,408],{"class":293},"         # megapixels\n",[288,410,412,415,417],{"class":134,"line":411},12,[288,413,414],{"class":300},"      IMGPROXY_ALLOWED_SOURCES",[288,416,322],{"class":304},[288,418,419],{"class":325},"\"s3:\u002F\u002Fproduct-images\u002F\"\n",[288,421,423,426,428],{"class":134,"line":422},13,[288,424,425],{"class":300},"      IMGPROXY_AVIF_SPEED",[288,427,322],{"class":304},[288,429,430],{"class":325},"\"7\"\n",[288,432,434,437,439],{"class":134,"line":433},14,[288,435,436],{"class":300},"      IMGPROXY_FORMAT_QUALITY",[288,438,322],{"class":304},[288,440,441],{"class":325},"\"avif=55,webp=78,jpeg=80\"\n",[288,443,445,448,451,454],{"class":134,"line":444},15,[288,446,447],{"class":300},"    ports",[288,449,450],{"class":304},": [",[288,452,453],{"class":325},"\"8080:8080\"",[288,455,456],{"class":304},"]\n",[288,458,460],{"class":134,"line":459},16,[288,461,462],{"class":293},"# trade-off: AVIF_SPEED 7 encodes faster but produces larger files than slower\n",[288,464,466],{"class":134,"line":465},17,[288,467,468],{"class":293},"# speeds; with a CDN in front, misses are rare, so slower speeds may be affordable.\n",[274,470,472],{"id":471},"_2-generate-signed-urls-in-templates","2. Generate signed URLs in templates",[279,474,478],{"className":475,"code":476,"language":477,"meta":284,"style":284},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","import { createHmac } from 'node:crypto';\nconst KEY = Buffer.from(process.env.IMGPROXY_KEY, 'hex');\nconst SALT = Buffer.from(process.env.IMGPROXY_SALT, 'hex');\nexport function imgUrl(src, width) {\n  const path = `\u002Frs:fit:${width}:0\u002Fplain\u002F${src}`;\n  const sig = createHmac('sha256', KEY).update(SALT).update(path).digest('base64url');\n  return `https:\u002F\u002Fimg.example.com\u002F${sig}${path}`;\n}\n\u002F\u002F trade-off: signing prevents arbitrary resize requests (a cheap DoS vector), but\n\u002F\u002F every URL must be generated server-side; clients cannot build new sizes.\n","javascript",[30,479,480,498,531,555,581,606,656,676,681,686],{"__ignoreMap":284},[288,481,482,486,489,492,495],{"class":134,"line":290},[288,483,485],{"class":484},"sPARh","import",[288,487,488],{"class":304}," { createHmac } ",[288,490,491],{"class":484},"from",[288,493,494],{"class":325}," 'node:crypto'",[288,496,497],{"class":304},";\n",[288,499,500,503,507,510,513,516,519,522,525,528],{"class":134,"line":297},[288,501,502],{"class":484},"const",[288,504,506],{"class":505},"sPXB4"," KEY",[288,508,509],{"class":484}," =",[288,511,512],{"class":304}," Buffer.",[288,514,491],{"class":515},"smZ65",[288,517,518],{"class":304},"(process.env.",[288,520,521],{"class":505},"IMGPROXY_KEY",[288,523,524],{"class":304},", ",[288,526,527],{"class":325},"'hex'",[288,529,530],{"class":304},");\n",[288,532,533,535,538,540,542,544,546,549,551,553],{"class":134,"line":308},[288,534,502],{"class":484},[288,536,537],{"class":505}," SALT",[288,539,509],{"class":484},[288,541,512],{"class":304},[288,543,491],{"class":515},[288,545,518],{"class":304},[288,547,548],{"class":505},"IMGPROXY_SALT",[288,550,524],{"class":304},[288,552,527],{"class":325},[288,554,530],{"class":304},[288,556,557,560,563,566,569,573,575,578],{"class":134,"line":316},[288,558,559],{"class":484},"export",[288,561,562],{"class":484}," function",[288,564,565],{"class":515}," imgUrl",[288,567,568],{"class":304},"(",[288,570,572],{"class":571},"sQw3B","src",[288,574,524],{"class":304},[288,576,577],{"class":571},"width",[288,579,580],{"class":304},") {\n",[288,582,583,586,589,591,594,596,599,601,604],{"class":134,"line":329},[288,584,585],{"class":484},"  const",[288,587,588],{"class":505}," path",[288,590,509],{"class":484},[288,592,593],{"class":325}," `\u002Frs:fit:${",[288,595,577],{"class":304},[288,597,598],{"class":325},"}:0\u002Fplain\u002F${",[288,600,572],{"class":304},[288,602,603],{"class":325},"}`",[288,605,497],{"class":304},[288,607,608,610,613,615,618,620,623,625,628,631,634,636,639,641,643,646,649,651,654],{"class":134,"line":337},[288,609,585],{"class":484},[288,611,612],{"class":505}," sig",[288,614,509],{"class":484},[288,616,617],{"class":515}," createHmac",[288,619,568],{"class":304},[288,621,622],{"class":325},"'sha256'",[288,624,524],{"class":304},[288,626,627],{"class":505},"KEY",[288,629,630],{"class":304},").",[288,632,633],{"class":515},"update",[288,635,568],{"class":304},[288,637,638],{"class":505},"SALT",[288,640,630],{"class":304},[288,642,633],{"class":515},[288,644,645],{"class":304},"(path).",[288,647,648],{"class":515},"digest",[288,650,568],{"class":304},[288,652,653],{"class":325},"'base64url'",[288,655,530],{"class":304},[288,657,658,661,664,667,670,672,674],{"class":134,"line":351},[288,659,660],{"class":484},"  return",[288,662,663],{"class":325}," `https:\u002F\u002Fimg.example.com\u002F${",[288,665,666],{"class":304},"sig",[288,668,669],{"class":325},"}${",[288,671,79],{"class":304},[288,673,603],{"class":325},[288,675,497],{"class":304},[288,677,678],{"class":134,"line":362},[288,679,680],{"class":304},"}\n",[288,682,683],{"class":134,"line":373},[288,684,685],{"class":293},"\u002F\u002F trade-off: signing prevents arbitrary resize requests (a cheap DoS vector), but\n",[288,687,688],{"class":134,"line":387},[288,689,690],{"class":293},"\u002F\u002F every URL must be generated server-side; clients cannot build new sizes.\n",[274,692,694],{"id":693},"_3-cache-at-the-cdn-with-format-awareness","3. Cache at the CDN with format awareness",[15,696,697,698,700,701,704,705,708,709,712],{},"With Accept-based detection, imgproxy returns ",[30,699,39],{},". Configure the CDN to normalise ",[30,702,703],{},"Accept"," into a small key (avif, webp, other), or encode the format in the URL instead (",[30,706,707],{},"f:avif",") and choose it in ",[30,710,711],{},"\u003Cpicture>"," markup.",[274,714,716],{"id":715},"_4-monitor-and-scale","4. Monitor and scale",[15,718,719,720,723],{},"Track request rate, latency, memory and error rate. Scale horizontally behind a load balancer; set concurrency limits (",[30,721,722],{},"IMGPROXY_WORKERS",") close to the number of cores, since image processing is CPU-bound.",[15,725,726],{},[44,727,51,730,51,733,51,736,51,738,51,741,51,743,51,747,51,754,51,758,51,762,51,765,51,769,51,772,51,776,51,778,51,781,51,785,51,788,51,790,51,794,51,796,51,799,51,801,51,804,51,807,51,811,51,813,51,816,51,821,51,824,51,827,51,831,51,833,51,836,51,838,51,841,51,844,51,848,51,850,51,853,51,855,51],{"viewBox":728,"width":47,"role":48,"ariaLabel":729,"style":50},"0 0 760 258","Comparison of a managed image CDN with a self-hosted imgproxy deployment across cost, effort and control.",[53,731],{"className":732,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,734,735],{},"Managed image CDN vs self-hosted imgproxy",[64,737,729],{},[53,739],{"x":86,"y":86,"width":87,"height":740,"rx":89,"fill":90,"stroke":82,"style":91},"256",[93,742,735],{"x":95,"y":96,"fill":82,"style":97},[53,744],{"x":95,"y":101,"width":745,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},"147.5","30.0",[93,748,753],{"x":749,"y":750,"fill":82,"style":751,"textAnchor":752},"38.0","75.5","font-size:12.5px;font-weight:700","start","Aspect",[53,755],{"x":756,"y":101,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},"175.5","278.3",[93,759,761],{"x":760,"y":750,"fill":82,"style":751,"textAnchor":110},"314.6","Managed image CDN",[53,763],{"x":764,"y":101,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},"453.7",[93,766,768],{"x":767,"y":750,"fill":82,"style":751,"textAnchor":110},"592.9","imgproxy + CDN",[53,770],{"x":95,"y":771,"width":745,"height":746,"rx":57,"fill":90,"stroke":82,"style":141},"86.0",[93,773,775],{"x":749,"y":774,"fill":82,"style":751,"textAnchor":752},"105.5","Pricing model",[53,777],{"x":756,"y":771,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},[93,779,780],{"x":760,"y":774,"fill":82,"style":161,"textAnchor":110},"per request or transform",[53,782],{"x":764,"y":771,"width":757,"height":746,"rx":57,"fill":783,"stroke":783,"style":784},"#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[93,786,787],{"x":767,"y":774,"fill":82,"style":161,"textAnchor":110},"compute + CDN egress",[53,789],{"x":95,"y":107,"width":745,"height":746,"rx":57,"fill":90,"stroke":82,"style":141},[93,791,793],{"x":749,"y":792,"fill":82,"style":751,"textAnchor":752},"135.5","Setup effort",[53,795],{"x":756,"y":107,"width":757,"height":746,"rx":57,"fill":783,"stroke":783,"style":784},[93,797,798],{"x":760,"y":792,"fill":82,"style":161,"textAnchor":110},"minutes",[53,800],{"x":764,"y":107,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},[93,802,803],{"x":767,"y":792,"fill":82,"style":161,"textAnchor":110},"days",[53,805],{"x":95,"y":806,"width":745,"height":746,"rx":57,"fill":90,"stroke":82,"style":141},"146.0",[93,808,810],{"x":749,"y":809,"fill":82,"style":751,"textAnchor":752},"165.5","Operations",[53,812],{"x":756,"y":806,"width":757,"height":746,"rx":57,"fill":783,"stroke":783,"style":784},[93,814,815],{"x":760,"y":809,"fill":82,"style":161,"textAnchor":110},"vendor-managed",[53,817],{"x":764,"y":806,"width":757,"height":746,"rx":57,"fill":818,"stroke":819,"style":820},"#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[93,822,823],{"x":767,"y":809,"fill":82,"style":161,"textAnchor":110},"your on-call",[53,825],{"x":95,"y":826,"width":745,"height":746,"rx":57,"fill":90,"stroke":82,"style":141},"176.0",[93,828,830],{"x":749,"y":829,"fill":82,"style":751,"textAnchor":752},"195.5","Control over encoders",[53,832],{"x":756,"y":826,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},[93,834,835],{"x":760,"y":829,"fill":82,"style":161,"textAnchor":110},"presets",[53,837],{"x":764,"y":826,"width":757,"height":746,"rx":57,"fill":783,"stroke":783,"style":784},[93,839,840],{"x":767,"y":829,"fill":82,"style":161,"textAnchor":110},"full",[53,842],{"x":95,"y":843,"width":745,"height":746,"rx":57,"fill":90,"stroke":82,"style":141},"206.0",[93,845,847],{"x":749,"y":846,"fill":82,"style":751,"textAnchor":752},"225.5","Data residency",[53,849],{"x":756,"y":843,"width":757,"height":746,"rx":57,"fill":82,"stroke":82,"style":104},[93,851,852],{"x":760,"y":846,"fill":82,"style":161,"textAnchor":110},"vendor regions",[53,854],{"x":764,"y":843,"width":757,"height":746,"rx":57,"fill":783,"stroke":783,"style":784},[93,856,857],{"x":767,"y":846,"fill":82,"style":161,"textAnchor":110},"your choice",[208,859,861],{"id":860},"verification","Verification",[15,863,864,865,867,868,871],{},"Request the same signed URL with different ",[30,866,703],{}," headers and confirm the format changes (or that explicit ",[30,869,870],{},"f:"," URLs return the stated format). Check that unsigned or tampered URLs return 403. Confirm CDN hit ratio for image paths climbs above 95% after warm-up, and that origin requests to imgproxy are mostly first-time variants. Load-test misses to find how many concurrent transformations each instance sustains.",[208,873,875],{"id":874},"worked-example-a-classifieds-platform","Worked Example: A Classifieds Platform",[15,877,878],{},"A classifieds platform received 400,000 photo uploads per day and served about 900 million image requests per month through a managed image CDN. Transformation charges had become the second-largest infrastructure cost. The team deployed imgproxy on four 8-core instances behind their existing CDN, with signed URLs, AVIF detection and an Accept-normalised cache key. CDN hit ratio settled at 97%, so imgproxy handled about 27 million transformations per month — roughly 10 per second on average, with peaks around 60. Monthly image costs fell by about 70%, p75 image response time from the edge was unchanged, and time to first image for newly uploaded listings improved slightly because transformation no longer queued at the vendor.",[208,880,882],{"id":881},"capacity-planning-for-cache-misses","Capacity Planning for Cache Misses",[15,884,885],{},"The CDN absorbs nearly all traffic once warm, so capacity planning is about misses: new uploads, new sizes after a design change, and cache purges. Estimate peak misses per second, then measure transformation time for typical sources on your hardware (AVIF encoding dominates; a 2000px photo might take 200–600ms of CPU). A rough rule: instances × cores ÷ average seconds per transform gives sustainable misses per second. Keep headroom for purge storms — avoid purging all image paths at once, and pre-warm the most popular variants after a deploy that changes sizes. Origin shield or tiered caching in the CDN reduces duplicate misses from different edge locations to a single request.",[15,887,888],{},[44,889,51,892,51,895,51,898,51,900,51,903,51,905,51,912,51,919,51,924,51,928,51,933,51,937,51,941,51,945,51],{"viewBox":890,"width":47,"role":48,"ariaLabel":891,"style":50},"0 0 760 163","Bar chart comparing monthly image delivery cost components for a managed image CDN and self-hosted imgproxy.",[53,893],{"className":894,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,896,897],{},"Monthly image cost before and after self-hosting",[64,899,891],{},[53,901],{"x":86,"y":86,"width":87,"height":902,"rx":89,"fill":90,"stroke":82,"style":91},"161",[93,904,897],{"x":95,"y":96,"fill":82,"style":97},[93,906,911],{"x":907,"y":908,"fill":82,"style":909,"textAnchor":910},"207.0","70.0","font-size:13px","end","Managed transforms + delivery",[53,913],{"x":914,"y":101,"width":915,"height":916,"rx":917,"fill":90,"stroke":82,"style":918},"219.0","449.0","19","3","fill-opacity:0.7;stroke-opacity:0.35",[93,920,923],{"x":921,"y":908,"fill":82,"style":922},"674.0","font-size:12px;font-weight:600","41k USD",[93,925,927],{"x":907,"y":926,"fill":82,"style":909,"textAnchor":910},"101.0","imgproxy compute",[53,929],{"x":914,"y":930,"width":931,"height":916,"rx":917,"fill":783,"stroke":783,"style":932},"87.0","35.0","fill-opacity:0.55;stroke-opacity:0.9",[93,934,936],{"x":935,"y":926,"fill":82,"style":922},"260.0","3.2k USD",[93,938,940],{"x":907,"y":939,"fill":82,"style":909,"textAnchor":910},"132.0","CDN egress (both setups)",[53,942],{"x":914,"y":201,"width":943,"height":916,"rx":917,"fill":82,"stroke":82,"style":944},"99.7","fill-opacity:0.7;stroke-opacity:0.4",[93,946,948],{"x":947,"y":939,"fill":82,"style":922},"324.6","9.1k USD",[208,950,952],{"id":951},"security-hardening-checklist","Security Hardening Checklist",[15,954,955,956,959],{},"An image proxy fetches and decodes untrusted input, so treat it as an exposed service. Run it as a non-root container with a read-only filesystem and memory limits enforced by the orchestrator, not just by imgproxy's own settings. Restrict outbound network access to the source buckets, so a crafted URL cannot reach internal services. Keep the signing key and salt in a secret store and rotate them on a schedule, accepting both old and new keys during the overlap. Set ",[30,957,958],{},"IMGPROXY_MAX_SRC_FILE_SIZE"," and animation frame limits, and keep libvips current through regular image updates, since decoder vulnerabilities are the most common class of issue. Finally, alert on 4xx spikes for invalid signatures — they often indicate probing.",[208,961,963],{"id":962},"common-mistakes","Common Mistakes",[213,965,966,972,978,984],{},[216,967,968,971],{},[219,969,970],{},"Unsigned URLs."," Anyone can request expensive transformations of any size.",[216,973,974,977],{},[219,975,976],{},"No source allow-list."," imgproxy becomes an open proxy for fetching arbitrary URLs.",[216,979,980,983],{},[219,981,982],{},"Caching without considering Accept."," Browsers without AVIF receive AVIF.",[216,985,986,989],{},[219,987,988],{},"No resolution limit."," A malicious or huge upload can exhaust memory.",[208,991,993],{"id":992},"edge-cases","Edge Cases",[15,995,996,999],{},[219,997,998],{},"Animated images."," imgproxy can process animated GIF and WebP, but CPU cost multiplies by frame count; set frame limits.",[15,1001,1002,1005],{},[219,1003,1004],{},"SVG sources."," Pass SVGs through unchanged rather than rasterising, unless you need a raster thumbnail.",[15,1007,1008,1011],{},[219,1009,1010],{},"Private images."," Combine signed URLs with expiring CDN tokens for access-controlled content.",[15,1013,1014,1017],{},[219,1015,1016],{},"Smart cropping."," Content-aware gravity options help thumbnails but cost more CPU; test on representative images.",[208,1019,1021],{"id":1020},"faq","FAQ",[1023,1024,1027,1031],"details",{"className":1025},[1026],"faq-item",[1028,1029,1030],"summary",{},"Is imgproxy free?",[15,1032,1033],{},"The open-source version is free. A paid Pro version adds features such as advanced format options and some smart processing capabilities.",[1023,1035,1037,1040],{"className":1036},[1026],[1028,1038,1039],{},"Do I need a CDN in front of imgproxy?",[15,1041,1042],{},"Yes, for production traffic. imgproxy does not cache results itself; the CDN makes each variant a one-time cost.",[1023,1044,1046,1049],{"className":1045},[1026],[1028,1047,1048],{},"How do I choose between Accept detection and explicit formats?",[15,1050,1051,1052,1054],{},"Accept detection keeps markup simple but requires format-aware CDN caching. Explicit formats in URLs with ",[30,1053,711],{}," are cache-friendly everywhere at the cost of more markup.",[1023,1056,1058,1061],{"className":1057},[1026],[1028,1059,1060],{},"What about thumbor or other proxies?",[15,1062,1063],{},"Thumbor and similar tools fill the same role. imgproxy is known for speed and low memory use thanks to libvips; evaluate on your images and operations needs.",[1023,1065,1067,1070],{"className":1066},[1026],[1028,1068,1069],{},"Can imgproxy add client hint support?",[15,1071,1072,1073,1076,1077,1080],{},"Yes, it can use ",[30,1074,1075],{},"Width"," and ",[30,1078,1079],{},"DPR"," hints when enabled, though signing such dynamic sizes needs care. See the client hints guide.",[1023,1082,1084,1087],{"className":1083},[1026],[1028,1085,1086],{},"How do I handle a deploy that changes image sizes?",[15,1088,1089],{},"New sizes produce new URLs and cache misses. Roll the change gradually or pre-warm popular variants to avoid a spike in transformations.",[208,1091,1093],{"id":1092},"related","Related",[213,1095,1096,1103,1110],{},[216,1097,1098,1102],{},[19,1099,1101],{"href":1100},"\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Ffixing-a-slow-lcp-image-behind-an-image-cdn\u002F","Fixing a slow LCP image behind an image CDN"," — latency problems on the miss path.",[216,1104,1105,1109],{},[19,1106,1108],{"href":1107},"\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fcaching-transformed-images-at-the-edge\u002F","Caching transformed images at the edge"," — cache keys and hit rates.",[216,1111,1112,1116],{},[19,1113,1115],{"href":1114},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Ftiered-caching-and-origin-shield\u002F","Tiered caching and origin shield"," — reducing duplicate misses.",[1118,1119,1121],"script",{"type":1120},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Self-Host an Image Proxy with imgproxy\",\n  \"description\": \"How to deploy imgproxy as a self-hosted image transformation service behind a CDN, with signed URLs, format negotiation and safe resource limits.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Run imgproxy with signing and limits\",\n      \"text\": \"Run imgproxy with signing and limits\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Generate signed URLs in templates\",\n      \"text\": \"Generate signed URLs in templates\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Cache at the CDN with format awareness\",\n      \"text\": \"With Accept-based detection, imgproxy returns Vary: Accept.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Monitor and scale\",\n      \"text\": \"Track request rate, latency, memory and error rate.\"\n    }\n  ]\n}\n",[1118,1123,1124],{"type":1120},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Self-Host an Image Proxy with imgproxy\",\n  \"description\": \"How to deploy imgproxy as a self-hosted image transformation service behind a CDN, with signed URLs, format negotiation and safe resource limits.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy\u002F\"\n  }\n}\n",[1118,1126,1127],{"type":1120},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Image & Media Optimization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Image CDNs and fetchpriority\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Self-Hosting an Image Proxy with imgproxy\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy\u002F\"\n    }\n  ]\n}\n",[1129,1130,1131],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":284,"searchDepth":297,"depth":297,"links":1133},[1134,1135,1136,1142,1143,1144,1145,1146,1147,1148,1149],{"id":210,"depth":297,"text":211},{"id":243,"depth":297,"text":244},{"id":271,"depth":297,"text":272,"children":1137},[1138,1139,1140,1141],{"id":276,"depth":308,"text":277},{"id":471,"depth":308,"text":472},{"id":693,"depth":308,"text":694},{"id":715,"depth":308,"text":716},{"id":860,"depth":297,"text":861},{"id":874,"depth":297,"text":875},{"id":881,"depth":297,"text":882},{"id":951,"depth":297,"text":952},{"id":962,"depth":297,"text":963},{"id":992,"depth":297,"text":993},{"id":1020,"depth":297,"text":1021},{"id":1092,"depth":297,"text":1093},"How to deploy imgproxy as a self-hosted image transformation service behind a CDN, with signed URLs, format negotiation and safe resource limits.","md",{"slug":1153,"type":1154,"breadcrumb":1155,"datePublished":1163,"dateModified":1163},"self-hosting-an-image-proxy-with-imgproxy","article",[1156,1159,1160,1161],{"name":1157,"url":1158},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1162},"\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy\u002F","2026-10-06",true,"\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy",{"title":5,"description":1167},"Run imgproxy behind a CDN to resize and convert images on the fly: signed URLs, AVIF\u002FWebP negotiation, caching, limits and cost compared with managed image CDNs.","image-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fself-hosting-an-image-proxy-with-imgproxy\u002Findex","T_zUkp12WJr5E8_ZOwusVr1AAO2up8a6UsczHzZeqrU",[1171,1175],{"title":1172,"path":1173,"stem":1174},"Fixing a Slow LCP Image Behind an Image CDN","\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Ffixing-a-slow-lcp-image-behind-an-image-cdn","image-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Ffixing-a-slow-lcp-image-behind-an-image-cdn\u002Findex",{"title":1176,"path":1177,"stem":1178},"Using Client Hints for Image Width and DPR","\u002Fimage-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fusing-client-hints-for-image-width-and-dpr","image-media-optimization\u002Fimage-cdns-and-fetchpriority\u002Fusing-client-hints-for-image-width-and-dpr\u002Findex",1791308079657]