imgproxy 4.0.22 on AWS User Guide
Overview
This guide covers launching and using the imgproxy 4.0.22 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg media servers listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
imgproxy is a fast, standalone server that resizes, crops and converts images on the fly. Your website or app asks for an image by URL, for example "this photo, 300 pixels wide, as WebP", and imgproxy fetches the original from your storage, processes it with the libvips image library and returns the result. Put a CDN such as Amazon CloudFront in front of it and every size and format your pages need is generated once and cached, instead of being prepared ahead of time.
What cloudimg adds:
- URL signing always on. Every request path must carry an HMAC SHA256 signature made with a key and salt that are generated on your instance's first boot and written to
/root/imgproxy-credentials.txt. Unsigned or wrongly signed requests get HTTP 403, so nobody can use your instance as a free image proxy. No key ships in the image, and imgproxy refuses to start without a real one. - HTTPS from the first boot. nginx serves imgproxy on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS and answers
/healthzfor load balancer health checks. - Private and metadata addresses refused as sources. imgproxy will not fetch from loopback, link local (including the EC2 instance metadata service at
169.254.169.254) or private (RFC1918 and the IPv6 metadata address) addresses, even through a DNS name or a redirect. Upstream imgproxy allows private addresses by default; cloudimg ships stricter. - libvips 8.18.7 built from source on Ubuntu main. Ubuntu 24.04's own libvips is an older community maintained (universe) package. cloudimg compiles the current libvips release from its checksum verified source against Ubuntu main libraries only, so every library on the image receives Ubuntu's standard security updates.
- imgproxy on loopback. imgproxy listens only on
127.0.0.1:8080; nginx is the only service on the network. - Hardened defaults. imgproxy runs as a dynamic unprivileged user in a strict systemd sandbox, imgproxy and nginx are both blocked from the instance metadata service, only GET and HEAD requests reach imgproxy, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: imgproxy 4.0.22 in /opt/imgproxy/bin (compiled by cloudimg from the v4.0.22 release), libvips 8.18.7 in /opt/imgproxy/libvips (compiled by cloudimg, with its complete source in /usr/local/src/imgproxy-cloudimg), nginx, a 1200 x 800 sample image for the built in demo, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Image formats: JPEG, PNG, WebP, AVIF and TIFF in and out; SVG, GIF and HEIC (iPhone photos) in. GIF and HEIC output and JPEG XL are not included, because the libraries they need are not in Ubuntu main.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (imgproxy fetches your source images over the network).
- A security group that allows:
- TCP 22 (SSH) from your own networks only
- TCP 443 (imgproxy over HTTPS) from your CDN or your users, usually anywhere
- TCP 80 (redirect to HTTPS and
/healthz) from the same sources as 443
Image processing is CPU bound. An m5.large handles tens of typical web resizes per second; put a CDN in front so each size is processed only once, and choose a compute optimised instance (c5, c6i, c7i) for heavier traffic.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick imgproxy 4.0.22 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=imgproxy}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| imgproxy 4.0.22 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate, the signing key and the salt, then starts imgproxy and nginx:
systemctl is-active imgproxy nginx imgproxy-firstboot
sudo test -f /var/lib/cloudimg/imgproxy-firstboot.done && echo FIRSTBOOT_DONE
/opt/imgproxy/bin/imgproxy version
All three units are active, the first boot marker is present and imgproxy reports its version:
active
active
active
FIRSTBOOT_DONE
4.0.22
nginx is the only service on public addresses; imgproxy listens on loopback:
sudo ss -lntpH | awk '{split($6, a, "\""); print $4, a[2]}' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:8080 imgproxy
127.0.0.53%lo:53 systemd-resolve
127.0.0.54:53 systemd-resolve
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your URL, key and salt
sudo cat /root/imgproxy-credentials.txt
The file is readable by root only:
# imgproxy 4.0.22 - generated on first boot by imgproxy-firstboot.service
# The signing key and salt are unique to this instance; none was baked into the image. Keep this file secret.
# Every request path must carry a signature made with them: unsigned or wrongly signed requests get HTTP 403.
IMGPROXY_HOST=ec2-203-0-113-10.compute-1.amazonaws.com
IMGPROXY_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
IMGPROXY_KEY=<64 hex characters>
IMGPROXY_SALT=<64 hex characters>
IMGPROXY_SIGNATURE_SIZE=32
IMGPROXY_DEMO_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/<signature>/rs:fit:300:300/plain/local:///cloudimg-sample.png@png
...
IMGPROXY_URLis where imgproxy is served. It uses the instance's public DNS name from EC2 instance metadata; the certificate also names the public and private IP addresses. To use your own DNS name, see Step 9.IMGPROXY_KEYandIMGPROXY_SALTare what your application signs URLs with. They are also in/etc/imgproxy/imgproxy.env, which imgproxy reads.IMGPROXY_DEMO_URLis a ready signed URL for the bundled sample image, resized to fit 300 x 300.
When you need to share the file, for example in a support request, print it with the key and salt masked:
sudo sed -E 's/^(IMGPROXY_(KEY|SALT)=).*/\1********/' /root/imgproxy-credentials.txt | grep '^IMGPROXY_'
Step 5: Make your first requests
Open IMGPROXY_DEMO_URL in your browser (the certificate is self signed, so the browser asks you to confirm it the first time). From the instance, this fetches the demo URL and checks the result is a real 300 x 200 PNG, then shows that the same path without a valid signature is refused:
sudo bash -c '
F=/root/imgproxy-credentials.txt
H=$(sed -n "s/^IMGPROXY_HOST=//p" $F); D=$(sed -n "s/^IMGPROXY_DEMO_URL=//p" $F)
R="--resolve $H:443:127.0.0.1"
P=/rs:fit:300:300/plain/local:///cloudimg-sample.png@png
curl -sk $R -o /tmp/demo.png -w "signed demo URL -> HTTP %{http_code}, %{content_type}\n" "$D"
imgproxy-assert-png /tmp/demo.png 300 200
curl -sk $R -o /dev/null -w "no signature -> HTTP %{http_code}\n" "https://$H$P"
curl -sk $R -o /dev/null -w "/insecure (unsigned) -> HTTP %{http_code}\n" "https://$H/insecure$P"
rm -f /tmp/demo.png
'
signed demo URL -> HTTP 200, image/png
OK /tmp/demo.png: valid PNG 300x200, 924 bytes
no signature -> HTTP 403
/insecure (unsigned) -> HTTP 403
imgproxy-sign-url signs any path with this instance's key. This processes an image from the public internet (imgproxy's own test image) into a 100 x 50 PNG:
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt)
SRC=https://raw.githubusercontent.com/imgproxy/imgproxy/a08006d11003eef87218a7407b1f21a40f6da615/testdata/geometry.png
U=$(sudo imgproxy-sign-url --url "https://$H" "/rs:fit:100:100/plain/$SRC@png")
curl -sk --resolve "$H:443:127.0.0.1" -o /tmp/geometry.png -w 'HTTP %{http_code}\n' "$U"
imgproxy-assert-png /tmp/geometry.png 100 50
rm -f /tmp/geometry.png
HTTP 200
OK /tmp/geometry.png: valid PNG 100x50, 7051 bytes
Step 6: Request sizes and formats
An imgproxy URL is https://<host>/<signature>/<processing options>/plain/<source URL>@<format>. Common options are rs:<type>:<width>:<height> (resize: fit keeps the whole image, fill crops to the exact size), q:<quality> and g:<gravity> for the crop position. The full list is in the imgproxy documentation. This requests the sample image in several formats and sizes:
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt)
for o in rs:fill:400:400/q:85@webp rs:fit:640:0@avif rs:fill:160:90/g:ce@jpg rs:fit:1200:1200@tiff; do
U=$(sudo imgproxy-sign-url --url "https://$H" "/${o%@*}/plain/local:///cloudimg-sample.png@${o#*@}")
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "${o} -> %{http_code} %{content_type} %{size_download} bytes\n" "$U"
done
rs:fill:400:400/q:85@webp -> 200 image/webp 2416 bytes
rs:fit:640:0@avif -> 200 image/avif 1391 bytes
rs:fill:160:90/g:ce@jpg -> 200 image/jpeg 1196 bytes
rs:fit:1200:1200@tiff -> 200 image/tiff 2880278 bytes
To serve your own images, use their URL as the source, for example /rs:fit:800:0/plain/https://images.example.com/photos/cat.jpg@webp. If the source URL contains a query string or an @, percent encode it or use the base64 form /rs:fit:800:0/<base64url of the source URL>.webp.
Step 7: Sign URLs in your application
Your application builds the URL path, signs it with the key and salt from Step 4 and puts the signature in front:
signature = base64url( HMAC_SHA256( key = hex_decode(IMGPROXY_KEY), message = hex_decode(IMGPROXY_SALT) + path ) )
with the trailing "=" padding removed
URL = https://<host>/<signature><path>
A Python example (keep the key and salt in your secrets store, never in client side code):
import base64, hashlib, hmac
def imgproxy_url(host, key_hex, salt_hex, path):
digest = hmac.new(bytes.fromhex(key_hex), bytes.fromhex(salt_hex) + path.encode(), hashlib.sha256).digest()
signature = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
return f"https://{host}/{signature}{path}"
print(imgproxy_url("img.example.com", KEY, SALT, "/rs:fit:300:300/plain/https://images.example.com/cat.jpg@webp"))
This runs the same calculation on the instance and confirms it matches imgproxy-sign-url:
sudo python3 - <<'PY'
import base64, hashlib, hmac, subprocess
env = dict(l.split("=", 1) for l in open("/root/imgproxy-credentials.txt").read().splitlines() if l and not l.startswith("#"))
path = "/rs:fit:300:300/plain/local:///cloudimg-sample.png@png"
digest = hmac.new(bytes.fromhex(env["IMGPROXY_KEY"]), bytes.fromhex(env["IMGPROXY_SALT"]) + path.encode(), hashlib.sha256).digest()
mine = "/" + base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + path
helper = subprocess.run(["imgproxy-sign-url", path], capture_output=True, text=True).stdout.strip()
print("signatures match" if mine == helper else "MISMATCH")
PY
signatures match
Client libraries that implement this exist for most languages; see the imgproxy documentation on signing.
Step 8: Allow only your own image sources
By default a correctly signed URL may point at any public address. Only holders of your key can create one, but pinning the sources is good practice. Edit /etc/imgproxy/imgproxy.env and list your origins (comma separated, each a URL prefix), then restart imgproxy:
sudo nano /etc/imgproxy/imgproxy.env
# IMGPROXY_ALLOWED_SOURCES=https://images.example.com/,https://my-bucket.s3.amazonaws.com/,local://
sudo systemctl restart imgproxy
To serve images straight from a private S3 bucket, imgproxy also supports s3:// sources with IMGPROXY_USE_S3=true. It then needs AWS credentials; because imgproxy is blocked from the instance metadata service on this image, use an access key in /etc/imgproxy/imgproxy.env (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) for a dedicated IAM user that can only read that bucket, or make the bucket reachable through CloudFront and use https:// sources.
Step 9: Use your own DNS name and certificate
Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell the instance its name. This regenerates the self signed certificate for that name, updates the credentials file and reloads nginx:
sudo imgproxy-set-hostname img.your-domain.example
To use a certificate from a public CA instead, replace /etc/ssl/imgproxy/imgproxy.crt (full chain) and /etc/ssl/imgproxy/imgproxy.key (mode 0600) and run sudo systemctl reload nginx. Behind CloudFront or an Application Load Balancer, point the origin at port 443 and the health check at /healthz.
Step 10: Rotate the signing key
To replace the key and salt (for example after a staff change), run the rotation helper. It writes a new pair to /etc/imgproxy/imgproxy.env and the credentials file and restarts imgproxy; every URL signed with the old key stops working, so update your application's secret at the same time:
sudo imgproxy-rotate-keys
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt)
sleep 2
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w 'new demo URL -> HTTP %{http_code}\n' "$(sudo sed -n 's/^IMGPROXY_DEMO_URL=//p' /root/imgproxy-credentials.txt)"
imgproxy-rotate-keys: new signing key and salt in place; URLs signed with the previous key are now refused
new demo URL -> HTTP 200
Step 11: Security checks
A correctly signed request for the instance metadata service, a loopback address or a private address is refused, while a public source works (Step 5):
H=$(sudo sed -n 's/^IMGPROXY_HOST=//p' /root/imgproxy-credentials.txt)
T=$(date '+%Y-%m-%d %H:%M:%S')
for src in http://169.254.169.254/latest/meta-data/ http://127.0.0.1:8080/health http://10.0.0.1/photo.jpg; do
U=$(sudo imgproxy-sign-url --url "https://$H" "/rs:fit:50:50/plain/$src@png")
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "$src -> HTTP %{http_code}\n" "$U"
done
sudo journalctl -u imgproxy -o cat --since "$T" | grep -oE '(Link-local|Loopback|Private) source address is not allowed' | sort | uniq -c
http://169.254.169.254/latest/meta-data/ -> HTTP 404
http://127.0.0.1:8080/health -> HTTP 404
http://10.0.0.1/photo.jpg -> HTTP 404
1 Link-local source address is not allowed
1 Loopback source address is not allowed
1 Private source address is not allowed
imgproxy and nginx are also blocked from the instance metadata service at the network level, and imgproxy runs sandboxed as a dynamic user with a guard that refuses to start it without a real signing key:
systemctl show imgproxy nginx -p IPAddressDeny --value
systemctl show imgproxy -p DynamicUser -p ProtectSystem -p ExecStartPre --value | cut -c1-80
grep -E '^IMGPROXY_(BIND|ALLOW_[A-Z_]+)=' /etc/imgproxy/imgproxy.env.tmpl
fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
{ path=/usr/local/sbin/imgproxy-config-guard ; argv[]=/usr/local/sbin/imgproxy-c
yes
strict
IMGPROXY_BIND=127.0.0.1:8080
IMGPROXY_ALLOW_LINK_LOCAL_SOURCE_ADDRESSES=false
IMGPROXY_ALLOW_LOOPBACK_SOURCE_ADDRESSES=false
IMGPROXY_ALLOW_PRIVATE_SOURCE_ADDRESSES=false
systemd may list the three denied addresses in a different order; what matters is that all three appear for both units.
Maintenance
- Settings:
/etc/imgproxy/imgproxy.env(every option is anIMGPROXY_environment variable, see the configuration reference). Apply changes withsudo systemctl restart imgproxy. KeepIMGPROXY_BIND=127.0.0.1:8080: nginx publishes it. - Logs: imgproxy logs every request to the journal (
journalctl -u imgproxy) with the real client address; nginx logs are in/var/log/nginx/. - Health checks:
GET /healthzon port 80 or 443 returnsokwithout a signature. - Limits: source images are limited to 50 megapixels and 40 MB, downloads to 10 seconds and processing to 20 seconds. Raise
IMGPROXY_MAX_SRC_RESOLUTION,IMGPROXY_MAX_SRC_FILE_SIZEand the timeouts in the settings file if you need larger originals. - Security updates: unattended security upgrades are on and cover nginx and every library libvips uses, all from Ubuntu main. Reboot when a new kernel is installed. imgproxy and libvips themselves are updated by a new version of this image.
Licences and source code
imgproxy is open source software licensed under the Apache License 2.0; its licence and notice are in /usr/share/doc/imgproxy-cloudimg/, with the licences of every Go module compiled into it in /usr/share/doc/imgproxy-cloudimg/third-party/. libvips is licensed under the GNU Lesser General Public License version 2.1 or later. It is dynamically linked from /opt/imgproxy/libvips, and its complete source, the release archive this build was compiled from, is in /usr/local/src/imgproxy-cloudimg/, so you can rebuild and replace it. Build details are in /usr/share/doc/imgproxy-cloudimg/README.cloudimg.
Support
cloudimg provides 24/7/365 expert technical support for this image. Contact support@cloudimg.co.uk. This image is provided by cloudimg and is not affiliated with or endorsed by imgproxy or the libvips project.