Deployment guide

imgproxy

Overview

imgproxy is a fast, secure, standalone HTTP server that resizes, crops, rotates, watermarks and converts images on the fly. Your application builds a URL that names a source image and the processing you want. imgproxy fetches the image, processes it and streams the result straight back.

All image work moves off your application servers, and you stop pre-generating and storing every size you might one day need. This SecureAnyCloud image deploys imgproxy on Ubuntu 24.04 on Azure with signed URLs enforced from the first request.

The image installs the latest stable imgproxy release, resolved at build time from the official GitHub release and compiled from the Apache 2.0 open-source sources. The exact version, the Go toolchain used and the libvips runtime are all recorded in /opt/imgproxy/VERSION. imgproxy runs under systemd as imgproxy.service, bound to the loopback interface only, with nginx owning the public port 80 in front of it.

Signed URLs are switched on from the first request. An image proxy that anyone can call is an abuse target. Strangers can burn your bandwidth, and a careless setup can be pointed at internal addresses. imgproxy solves this with URL signing.

This image generates a unique signing key and salt on the first boot of every VM and writes them to /etc/imgproxy/imgproxy.env, which is owned by root and not world-readable. The shipped image contains no usable key, so no two VMs share a signing secret, and an unsigned request is refused. The service is also held back at boot until that key exists, so it can never come up as an open proxy.

Source loading is locked down beyond the upstream default. Link-local addresses (which protect the Azure Instance Metadata Service), loopback addresses and private RFC1918 addresses are all refused as image sources. The proxy therefore cannot be used to read instance metadata or to reach other hosts inside your virtual network. Upstream imgproxy allows private addresses by default, but this image does not. Limits on source resolution, file size, redirects and timeouts guard against decompression bombs.

A working demo ships in the image. A sample image is bundled and served through imgproxy's local file source. You can watch a real resize happen the moment the VM boots, without internet access and without loosening any of the protections above.

What you get:

  • The latest stable imgproxy release, compiled from the Apache 2.0 open-source sources and run under systemd as imgproxy.service, bound to the loopback interface only
  • nginx owning the public port 80 and reverse proxying to imgproxy on 127.0.0.1:8080, plus an unauthenticated /healthz load balancer probe
  • A unique URL signing key and salt generated on first boot, with unsigned requests refused
  • imgproxy-sign-url, a helper that mints a correctly signed URL from this VM's key in one command
  • Hardened source loading: link-local, loopback and private addresses refused, plus resolution, file size, redirect and timeout limits
  • A bundled sample image, so the demo works out of the box

Prerequisites

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the imgproxy listing on Azure Marketplace
  • A place where your source images live: an Azure Blob container, an S3 bucket, or any public HTTPS origin

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is a sensible starting size.

Image processing is CPU-bound, so use Standard_D2s_v5 or larger if you serve many large images. imgproxy is stateless and holds no database, so you scale it out by adding more instances behind a load balancer.

1

Deploy from the Azure Portal

Search the Marketplace for imgproxy, select the SecureAnyCloud publisher, and click Create.

Configure the network security group (NSG) with two inbound rules: TCP 22 for administration and TCP 80 for the image endpoint. Allow port 80 only from the networks that will request images. In most production designs, that means your CDN or Application Gateway rather than the public internet.

2

Deploy from the Azure CLI

RG="imgproxy-prod"; LOCATION="eastus"; VM_NAME="imgproxy-01" IMAGE_URN="sac1647283583153:imgproxy:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name imgproxy-vnet --address-prefix 10.100.0.0/16 --subnet-name imgproxy-subnet --subnet-prefix 10.100.1.0/24 az network nsg create -g "$RG" --name imgproxy-nsg az network nsg rule create -g "$RG" --nsg-name imgproxy-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp az network nsg rule create -g "$RG" --nsg-name imgproxy-nsg --name allow-http --priority 110 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:imgproxy:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name imgproxy-vnet --subnet imgproxy-subnet --nsg imgproxy-nsg --public-ip-sku Standard
3

Verify the imgproxy and nginx services

Connect over SSH. Confirm that both services are active and check which ports are bound.

imgproxy listens only on 127.0.0.1:8080, so it is never directly reachable from the network. nginx owns the public :80 and proxies to imgproxy:

/opt/imgproxy/bin/imgproxy version
sudo systemctl is-active imgproxy.service nginx.service
sudo ss -tlnp | grep -E '127.0.0.1:8080 |:80 ' curl -s -o /dev/null -w 'healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthz
4

Find this VM's signing key and mint a signed URL

Every VM generates its own signing key and salt on first boot. They are written to /etc/imgproxy/imgproxy.env (root only). A summary, together with a ready-made demo URL, is in /root/imgproxy-credentials.txt:

sudo cat /root/imgproxy-credentials.txt

An unsigned request is refused, which stops anyone who finds your server from using it. The bundled imgproxy-sign-url helper mints a correctly signed URL from this VM's key:

curl -s -o /dev/null -w 'unsigned -> HTTP %{http_code}\n' \
  "http://127.0.0.1/rs:fit:300:300/plain/local:///sac-sample.png"
sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///sac-sample.png
5

Process your first image

This one block shows the whole product. Sign a URL, fetch it, and confirm that the bundled 1200x800 sample comes back resized to 300x200:

SIGNED=$(sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///sac-sample.png)
curl -s -o /tmp/demo-fit.png -w 'HTTP %{http_code}\n' "http://127.0.0.1${SIGNED}" imgproxy-assert-png /tmp/demo-fit.png 300 200

Change the processing options and you get a different rendition of the same source, with no extra storage anywhere. Here the same image is cropped to fill an exact square. It is then converted to WebP by adding an extension to the source:

SQUARE=$(sudo imgproxy-sign-url /rs:fill:150:150/plain/local:///sac-sample.png)
curl -s -o /tmp/demo-square.png -w 'fill 150x150 -> HTTP %{http_code}\n' "http://127.0.0.1${SQUARE}" imgproxy-assert-png /tmp/demo-square.png 150 150
WEBP=$(sudo imgproxy-sign-url /rs:fit:600:600/plain/local:///sac-sample.png@webp)
curl -s -o /tmp/demo.webp -w 'webp -> HTTP %{http_code} %{content_type}\n' "http://127.0.0.1${WEBP}" rm -f /tmp/demo-fit.png /tmp/demo-square.png /tmp/demo.webp

The signature covers the request path only, so a signed URL works the same whichever hostname a client uses. Confirm this against the VM's own network address instead of loopback:

VMIP=$(hostname -I | cut -d' ' -f1)
SIGNED=$(sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///sac-sample.png)
curl -s -o /tmp/demo-net.png -w "via ${VMIP} -> HTTP %{http_code}\n" "http://${VMIP}${SIGNED}" imgproxy-assert-png /tmp/demo-net.png 300 200 rm -f /tmp/demo-net.png

From your own machine, use the VM's public address: http://<your-vm-address>/<signature>/rs:fit:300:300/plain/local:///sac-sample.png.

6

Serve your own images

Replace the local:///sac-sample.png source with any HTTPS URL where your images live. The plain/ form takes the source URL literally:

SIGNED=$(sudo imgproxy-sign-url "/rs:fit:400:400/plain/https://secureanycloud.com/assets/images/sac%20logo.png")
echo "signed path: ${SIGNED}" curl -s -o /tmp/remote.png -w 'remote HTTPS source -> HTTP %{http_code} %{content_type} %{size_download} bytes\n' "http://127.0.0.1${SIGNED}" rm -f /tmp/remote.png

That request reached out over HTTPS, fetched the image and returned a resized rendition. Nothing is stored on the VM. Swap the URL for one of your own.

imgproxy has native support for Azure Blob Storage. Set IMGPROXY_USE_ABS=true, IMGPROXY_ABS_NAME and IMGPROXY_ABS_KEY in /etc/imgproxy/imgproxy.env, then address images as abs://container/path/to/image.jpg. This keeps the container private, because imgproxy authenticates to it and the images do not need to be publicly readable.

The processing options form a compact URL grammar:

  • rs:fit:800:600 resizes
  • rs:fill:400:400 crops to fill
  • g:sm picks a smart crop centre
  • q:90 sets quality
  • bl:5 blurs
  • @webp or @avif converts the output format

The full reference is at docs.imgproxy.net.

7

Review the security posture

The shipped configuration is deliberately stricter than stock imgproxy. Review it, then confirm that the Azure metadata service is unreachable through the proxy, even with a valid signature:

grep -E '^IMGPROXY_(ALLOW_|ALLOWED_SOURCES|SIGNATURE_SIZE|MAX_|DOWNLOAD_TIMEOUT)' /etc/imgproxy/imgproxy.env
IMDS=$(sudo imgproxy-sign-url "/rs:fit:100:100/plain/http://169.254.169.254/metadata/instance")
curl -s -o /dev/null -w 'signed request for the metadata service -> HTTP %{http_code}\n' "http://127.0.0.1${IMDS}" stat -c '%n mode=%a owner=%U:%G' /etc/imgproxy/imgproxy.env

Pin the sources you actually serve. The most effective next step is to stop the proxy from fetching from just anywhere. The setting ships empty, which permits any public HTTPS origin:

grep -n '^IMGPROXY_ALLOWED_SOURCES=' /etc/imgproxy/imgproxy.env

Edit that line in /etc/imgproxy/imgproxy.env so it lists only the origins you serve. Keep local:// if you still want the bundled sample to work:

IMGPROXY_ALLOWED_SOURCES=https://cdn.example.com/,local://

Apply the change with sudo systemctl restart imgproxy. From then on, any source outside the list is refused, so even a leaked signing key cannot be used to proxy arbitrary third-party images.

If you need a private origin. Serving images from inside your own virtual network is refused by default. If you need that, set IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES=true and pair it with IMGPROXY_ALLOWED_SOURCES so that only that one origin is reachable. Leave the link-local setting alone, because it protects the instance metadata service.

8

Generate signed URLs in your application

In production, your application mints these URLs itself. The signature is an HMAC-SHA256 over the salt followed by the path. It is encoded with URL-safe base64, stripped of padding and placed in front of the path.

Read the key and salt from /etc/imgproxy/imgproxy.env and keep them wherever you keep your other secrets:

import base64, hashlib, hmac KEY = bytes.fromhex("<IMGPROXY_KEY>") SALT = bytes.fromhex("<IMGPROXY_SALT>") def imgproxy_url(base, path): digest = hmac.new(KEY, SALT + path.encode(), hashlib.sha256).digest() sig = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() return f"{base}/{sig}{path}" print(imgproxy_url("http://<IMGPROXY_HOST>", "/rs:fit:300:300/plain/https://example.com/photo.jpg"))

Official signing snippets for Ruby, PHP, Go, Node and others are in the imgproxy signing documentation.

9

Put it behind TLS and a CDN

This image serves plain HTTP on port 80 by design, because imgproxy is meant to sit behind a layer that terminates TLS and caches. In Azure, the natural choices are Azure Front Door or an Application Gateway in front of the VM, or any CDN you already run.

That layer gives you certificates, caching of processed renditions, and a place to enforce rate limits. It also means imgproxy processes each rendition only once.

Restrict the NSG so that port 80 is reachable only from your CDN or gateway, not from the public internet.

10

Managing the imgproxy service

sudo systemctl status imgproxy --no-pager | head -12 sudo systemctl is-active imgproxy nginx
sudo journalctl -u imgproxy --no-pager | tail -20

The configuration lives at /etc/imgproxy/imgproxy.env. After any change, apply it with sudo systemctl restart imgproxy. Follow the logs live with sudo journalctl -u imgproxy -f.

11

Security recommendations

Keep URL signing on. It stops strangers from using your server. Never blank IMGPROXY_KEY or IMGPROXY_SALT, because imgproxy treats an empty key as "signature checking disabled" and serves anyone.

Pin IMGPROXY_ALLOWED_SOURCES to the origins you actually serve, so that a leaked key cannot be used to proxy arbitrary third-party images.

Restrict the NSG. Allow port 80 only from your CDN or Application Gateway, and port 22 only from your management network.

Leave the source address restrictions alone. Link-local, loopback and private addresses are refused for good reason. Relaxing the private setting without an allow list opens a path into your virtual network.

Protect the key. /etc/imgproxy/imgproxy.env is mode 600 and owned by root. Keep it that way, and regenerate the key and salt if a VM is ever exposed.

Keep the OS patched. Unattended security upgrades remain enabled on the running VM.

12

Support and Licensing

imgproxy is distributed under the Apache License 2.0. This SecureAnyCloud image bundles the unmodified official open-source release, compiled from the published sources.

SecureAnyCloud provides the packaging, the hardened defaults, the per-instance signing key, the signed URL helper, the working demo and 24/7 support with a guaranteed 24-hour response SLA. This image ships the open-source edition only and contains no imgproxy Pro components.

imgproxy is an independent open-source project, and this image is not affiliated with or endorsed by the imgproxy project. imgproxy uses the libvips image processing library, which is licensed under the LGPL 2.1 and linked dynamically as the stock Ubuntu shared library.

Deploy on Azure

Find imgproxy on Ubuntu 24.04 on the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.

Need Help?

Email sales@secureanycloud.com for deployment help, configuration questions, or licensing enquiries.

Deploy on Azure

Launch imgproxy on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure