imgproxy on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of imgproxy on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. 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, and imgproxy fetches that image, processes it and streams the result straight back. All of the image work moves off your application servers, and you stop pre generating and storing every size you might one day need.
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 very first request. An image proxy that anyone can call is an abuse target: strangers can burn your bandwidth, and a careless one can be pointed at internal addresses. imgproxy solves this with URL signing, and this image generates a unique signing key and salt on the first boot of every VM, straight into /etc/imgproxy/imgproxy.env (owned by root, not world readable). The shipped image carries only placeholders, so no two VMs share a signing secret, and an unsigned request is refused. The service is additionally 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 is what protects the Azure Instance Metadata Service), loopback addresses, and private RFC1918 addresses are all refused as image sources, so the proxy cannot be turned into a way to read instance metadata or reach other hosts inside your virtual network. Upstream imgproxy allows private addresses by default; 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, so you can watch a real resize happen the moment the VM boots, without any internet access and without loosening any of the protections above.
What is included:
-
The latest stable imgproxy release, compiled from the Apache 2.0 open source sources and run under systemd as
imgproxy.service -
nginx owning the public port 80, reverse proxying to imgproxy on
127.0.0.1:8080, plus an unauthenticated/healthzload 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
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the imgproxy listing on Azure Marketplace
-
Somewhere 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 point. Image processing is CPU bound, so if you serve a lot of large images use Standard_D2s_v5 or larger. imgproxy is stateless and holds no database, so you scale it out simply by adding more instances behind a load balancer.
Step 1: Deploy from the Azure Portal
Search imgproxy in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 22 (admin) and TCP 80 (the image endpoint) from the networks that will request images, which in most production designs is your CDN or Application Gateway rather than the public internet.
Step 2: Deploy from the Azure CLI
RG="imgproxy-prod"; LOCATION="eastus"; VM_NAME="imgproxy-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/imgproxy/versions/<version>"
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
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--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
Step 3: Verify the imgproxy and nginx services
Connect over SSH, then confirm both services are active and see which ports are bound. Note that imgproxy itself listens only on 127.0.0.1:8080; it is never directly reachable from the network. nginx owns the public :80 and proxies to it:
/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

Step 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) and summarised, along with a ready made demo URL, in /root/imgproxy-credentials.txt:
sudo cat /root/imgproxy-credentials.txt
An unsigned request is refused, which is what 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:///cloudimg-sample.png"
sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///cloudimg-sample.png

Step 5: Process your first image
This is the whole product in one block. Sign a URL, fetch it, and confirm the bundled 1200x800 sample really did come back resized to 300x200:
SIGNED=$(sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///cloudimg-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, and then converted to WebP by adding an extension to the source:
SQUARE=$(sudo imgproxy-sign-url /rs:fill:150:150/plain/local:///cloudimg-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:///cloudimg-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 identically whichever hostname a client uses. You can confirm that against this VM's own network address rather than loopback:
VMIP=$(hostname -I | cut -d' ' -f1)
SIGNED=$(sudo imgproxy-sign-url /rs:fit:300:300/plain/local:///cloudimg-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 instead: http://<your-vm-address>/<signature>/rs:fit:300:300/plain/local:///cloudimg-sample.png.
Step 6: Serve your own images
Replace the local:///cloudimg-sample.png source with any HTTPS URL your images live at. The plain/ form takes the source URL literally:
SIGNED=$(sudo imgproxy-sign-url "/rs:fit:400:400/plain/https://www.cloudimg.co.uk/images/logo.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, all without anything being stored on the VM. Swap the URL for one of your own.
For Azure Blob Storage imgproxy has native support: set IMGPROXY_USE_ABS=true plus IMGPROXY_ABS_NAME and IMGPROXY_ABS_KEY in /etc/imgproxy/imgproxy.env, then address images as abs://container/path/to/image.jpg. That keeps the container private, because imgproxy authenticates to it rather than the image being publicly readable.
The processing options are 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, and @webp or @avif converts the output format. The full reference is at docs.imgproxy.net.
Step 7: Review the security posture
The shipped configuration is deliberately stricter than stock imgproxy. Review it, and confirm for yourself that the Azure metadata service really is unreachable through the proxy even with a perfectly 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 single most effective next step is to stop the proxy fetching from anywhere at all. 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 with your editor of choice so it lists only the origins you serve, keeping local:// if you still want the bundled sample to work:
IMGPROXY_ALLOWED_SOURCES=https://cdn.example.com/,local://
Then apply it with sudo systemctl restart imgproxy. Any source outside that list is refused from then on, 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 that is genuinely what you need, set IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES=true and pair it with IMGPROXY_ALLOWED_SOURCES so only that one origin is reachable. Leave the link local setting alone: it is what protects the instance metadata service.
Step 8: Generate signed URLs in your application
In production your application mints these URLs itself. The scheme is a HMAC SHA256 over the salt followed by the path, encoded with URL safe base64 and the padding removed, then 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.
Step 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 something that already 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 gives you certificates, caching of processed renditions, and a place to enforce rate limits, and it means imgproxy only ever processes an image once per rendition.
Restrict the NSG so port 80 is reachable only from your CDN or gateway, not from the public internet.
Step 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
Apply a configuration change with sudo systemctl restart imgproxy, and follow the logs live with sudo journalctl -u imgproxy -f. The configuration lives at /etc/imgproxy/imgproxy.env. After any change, restart imgproxy.
Step 11: Security recommendations
-
Keep URL signing on. It is the control that stops strangers using your server. Never blank
IMGPROXY_KEYorIMGPROXY_SALT: imgproxy treats an empty key as "signature checking disabled" and will serve anyone. -
Pin
IMGPROXY_ALLOWED_SOURCESto the origins you actually serve, so 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.envis 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.
Step 12: Support and Licensing
imgproxy is distributed under the Apache License 2.0. This cloudimg image bundles the unmodified official open source release, compiled from the published sources; cloudimg 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 is linked dynamically as the stock Ubuntu shared library.
Deploy on Azure
Find imgproxy on Ubuntu 24.04 on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.