thumbor
Overview
thumbor is an open-source, on-demand image processing service. Instead of pre-generating every thumbnail your application might need, you request a rendition in the URL and thumbor produces it at request time. It can resize, crop, smart-crop around the interesting part of the picture, run a chain of filters, and re-encode to another format.
The SecureAnyCloud image installs thumbor 7.8.0 into a dedicated Python virtualenv. It runs thumbor as a hardened, non-root thumbor systemd service bound to loopback and fronts it on port 80 with nginx. Each VM generates a unique URL signing key and a unique demo page password on first boot. Two sample images ship in the image, so every URL in this guide renders as soon as your VM boots.
What you get:
- thumbor 7.8.0 pinned in a dedicated Python 3.12 virtualenv at
/opt/thumbor/venv, run as the non-rootthumborsystemd service bound to loopback127.0.0.1:8888 - An nginx reverse proxy on
:80publishing renditions, the demo page and the health endpoints - A unique 64-character URL signing key (
SECURITY_KEY) generated per VM on first boot, never a baked-in constant - URL signing enforced: the
/unsafe/prefix is rejected, so nobody can request transformations without your key - The image loader restricted to local files under
/var/lib/thumbor/images, so the instance cannot be used as an SSRF pivot or an open image proxy - Smart cropping via OpenCV feature detection, the full thumbor filter set, and AVIF, HEIF, WebP and SVG support
- A per-VM demo page that renders live, signed examples of every headline transformation
- Two bundled sample images, so the guide works on first boot with nothing to supply
thumbor.serviceandnginx.serviceas systemd units, enabled and active- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 SecureAnyCloud support
thumbor is licensed under the MIT License (Copyright (c) 2014 Globo.com), a permissive license with no fee and no key to enter. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the thumbor project.
Security model, and why it matters here
A default thumbor install is not safe to put on a public address, for two reasons. First, the /unsafe/ URL prefix lets anyone request any transformation, which turns your VM into free compute for strangers. Second, thumbor's HTTP loader fetches whatever URL is embedded in the path. An exposed instance is therefore both an open image proxy and a way to reach hosts inside your network.
This image closes both gaps by default:
Setting Value in this image Effect
ALLOW_UNSAFE_URL False /unsafe/... is rejected. Every rendition URL must carry a valid HMAC signature.
SECURITY_KEY 64 random hex characters, generated on this VM's first boot The signature is unforgeable without your key, and no two SecureAnyCloud VMs share one.
LOADER thumbor.loaders.file_loader rooted at /var/lib/thumbor/images thumbor cannot fetch remote URLs at all, so it is neither an SSRF vector nor an open proxy.
ALLOWED_SOURCES [] Nothing is allowlisted for remote fetching, because remote fetching is off.
UPLOAD_ENABLED False No write endpoint is exposed.
Rendition URLs are deliberately not behind a password. Your users' browsers and a CDN fetch them directly, and the signature controls access. A separate per-VM HTTP Basic Auth credential gates the operator-facing demo page. First boot writes both the signing key and that password to a root-only file.
If you need to serve images from your own origin, the Maintenance section below shows how to opt in to the HTTP loader with a mandatory allowlist.
Prerequisites
- An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region.
- Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting size. thumbor is CPU-bound while rendering, so size up if you expect sustained concurrent traffic on large source images.
- NSG inbound rules: allow
22/tcpfrom your management network,80/tcpfor renditions and the demo page, and443/tcpif you add TLS.
thumbor serves plain HTTP on port 80. For production, terminate TLS in front of it with your own domain (see Maintenance).
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, select Create a resource, search the Marketplace for thumbor by SecureAnyCloud, and select Create.
On the Basics tab, choose your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).
Select Review + create, and then select Create.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:thumbor-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name thumbor \
--image sac1647283583153:thumbor-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name thumbor --port 80 --priority 1010Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>Step 4 - Confirm the services are running
systemctl is-active thumbor.service nginx.service /opt/thumbor/venv/bin/thumbor --version
systemctl is-active thumbor-firstboot.serviceBoth thumbor and nginx report active, and the version line confirms the pinned release. thumbor runs as the dedicated non-root thumbor user, bound to the loopback address 127.0.0.1:8888. nginx fronts it on port 80, so thumbor is never directly reachable from the network.
thumbor-firstboot.service also reports active. It is a one-shot unit that runs once, generates this VM's signing key and password, and then disables itself so it can never run again and overwrite them. For that reason, systemctl is-enabled thumbor-firstboot.service reports disabled on a VM that has already booted. The disabled state is the expected end state, not a fault.
Step 5 - Retrieve your per-VM key and password
First boot writes everything unique to your instance to a root-only file. Display the file and check its permissions:
sudo cat /root/thumbor-credentials.txt
sudo stat -c '%n %a %U:%G' /root/thumbor-credentials.txt /var/lib/thumbor/.htpasswdTHUMBOR_SECURITY_KEY is the 64-character key that signs rendition URLs. THUMBOR_USERNAME and THUMBOR_PASSWORD open the demo page in a browser at THUMBOR_URL.
The credentials file is 0600 root:root. The demo page password is stored on disk only as a bcrypt hash in /var/lib/thumbor/.htpasswd, so no plaintext password and no usable signing key ship inside the image itself.
Store both values somewhere safe, because anyone with the signing key can request renditions from your instance.
Step 6 - Confirm the health endpoints
Request both health endpoints.
curl -s http://localhost/healthz
curl -s http://localhost/healthcheck/healthz returns ok. nginx serves it statically, so it stays up even while thumbor restarts, which makes it the right target for an Azure Load Balancer health probe.
/healthcheck returns WORKING and is thumbor's own liveness endpoint. Neither endpoint requires authentication, and neither reveals anything about your configuration.
Step 7 - Confirm that URL signing is enforced
Run this check before you put the instance on a public address. The instance must refuse an unsigned /unsafe/ request and a forged signature, and the demo page must demand a password:
grep -E '^(ALLOW_UNSAFE_URL|LOADER|FILE_LOADER_ROOT_PATH|ALLOWED_SOURCES|UPLOAD_ENABLED) ' /etc/thumbor/thumbor.conf curl -s -o /dev/null -w 'unsigned /unsafe/ -> HTTP %{http_code}\n' http://127.0.0.1/unsafe/300x200/sample.jpg curl -s -o /dev/null -w 'forged signature -> HTTP %{http_code}\n' http://127.0.0.1/aaaaaaaaaaaaaaaaaaaaaaaaaaaa/300x200/sample.jpg curl -s -o /dev/null -w 'demo page, no password -> HTTP %{http_code}\n' http://127.0.0.1/The two image requests return 400, and the demo page returns 401. If /unsafe/ ever returns 200 on your instance, someone has set ALLOW_UNSAFE_URL = True, and anyone on the internet can drive your renderer.
Step 8 - Generate a signed URL and fetch a rendition
thumbor-url ships in the virtualenv and signs a URL with your key. Note the flags: -w is width, -e is height (-h is --help), and -s means smart crop. The tool prints a URL: header line first, so pipe the output through tail -1 to get just the path:
KEY=$(sudo grep '^THUMBOR_SECURITY_KEY=' /root/thumbor-credentials.txt | cut -d= -f2-)
SIGNED=$(/opt/thumbor/venv/bin/thumbor-url -k "$KEY" -w 400 -e 250 sample.jpg | tail -1)
echo "$SIGNED" curl -s -o /tmp/shot.jpg -w 'HTTP %{http_code} %{content_type} %{size_download} bytes\n' "http://127.0.0.1$SIGNED" /opt/thumbor/venv/bin/python -c 'from PIL import Image;i=Image.open("/tmp/shot.jpg");print("rendered", "%dx%d"%i.size, i.format)' rm -f /tmp/shot.jpgThe output is a path of the form /<signature>/400x250/sample.jpg, a 200 with content type image/jpeg, and a decoded image of exactly 400x250. Append that path to your VM's public address, and the same rendition is served to a browser.
In an application, generate these signatures in your own code rather than shelling out. thumbor publishes signing libraries for most languages, and the Python one (libthumbor) is already installed in the virtualenv.
Step 9 - Open the demo page in a browser
Browse to http://<vm-public-ip>/. When your browser prompts for a username and password, enter the THUMBOR_USERNAME and THUMBOR_PASSWORD from Step 5.
First boot generates the page. Every image on it is a live rendition, signed with your key and produced by your instance when you load the page. The banner across the top restates the security posture of the instance: signing enforced, unsafe URLs rejected, local file loader only.
Step 10 - Compare a smart crop with a plain crop
The first row of the grid takes the same 1600 x 1000 source and requests three different renditions:
- A plain
400x250resize keeps the whole scene. - A
400x400smart crop makes thumbor run feature detection first and center the square on whatever is visually interesting. Here that is the ringed disc and the cluster of buildings on the right. - The same
400x400square withoutsmartsimply crops the middle and slices the subject in half at the edge.
Each card shows the exact signed URL that produced it, so the difference is a single /smart/ segment.
Step 11 - Chain filters and convert formats
Filters compose left to right inside a single URL segment, so one request can grayscale an image, blur it, brighten it and round its corners. fit-in scales the whole image inside the target box instead of cropping it, and filters:fill(...) paints the letterbox. Transparency survives a resize.
filters:format(webp) re-encodes the image on the fly, so you can serve WebP to browsers that accept it without keeping a second copy.
Step 12 - Serve a rendition straight to a browser
Paste any signed path onto the end of your VM's address and open it on its own. The instance returns the rendition with no password prompt, because the signed URL is the credential. Your application references images the same way in an <img> tag or behind a CDN.
The example below is a 1000 x 560 smart crop with a brightness filter and rounded corners, produced on demand from the same source image.
Step 13 - Add your own images
Anything you place in /var/lib/thumbor/images is servable immediately, with no restart and no registration step. The path in the URL is relative to that directory, so subdirectories work too. Create a test image, sign a URL for it and fetch it:
sudo /opt/thumbor/venv/bin/python -c " from PIL import Image, ImageDraw im = Image.new('RGB', (1200, 800), (18, 42, 74)) d = ImageDraw.Draw(im) d.ellipse([760, 120, 1080, 440], fill=(255, 214, 102)) d.rectangle([80, 560, 1120, 620], fill=(249, 115, 22)) im.save('/var/lib/thumbor/images/my-photo.jpg', 'JPEG', quality=90) print('wrote /var/lib/thumbor/images/my-photo.jpg') "
KEY=$(sudo grep '^THUMBOR_SECURITY_KEY=' /root/thumbor-credentials.txt | cut -d= -f2-)
MINE=$(/opt/thumbor/venv/bin/thumbor-url -k "$KEY" -w 320 -e 320 -s my-photo.jpg | tail -1)
curl -s -o /dev/null -w 'my-photo.jpg -> HTTP %{http_code} %{content_type}\n' "http://127.0.0.1$MINE"The new file renders 200 on its first request. In production, sync your media library into that directory (or mount it there from Azure Files or a blob fuse mount) and let thumbor render every size your application requests.
thumbor caches rendered results under /var/lib/thumbor/result_storage for 24 hours, so a repeated request for the same rendition is served from disk instead of being re-rendered.
Maintenance
- Change the demo page password: run
sudo htpasswd -B /var/lib/thumbor/.htpasswd thumbor, and then update/root/thumbor-credentials.txtfor your records. No restart is needed. - Rotate the signing key: edit the
SECURITY_KEYline in/etc/thumbor/thumbor.confand runsudo systemctl restart thumbor. Every previously issued URL stops working immediately, which is what you want if a key leaks. Also clear the cached results withsudo rm -rf /var/lib/thumbor/result_storage/*. - Serve images from your own origin (opt in, never unrestricted): thumbor can fetch source images over HTTP instead of reading local files. Do this only with an explicit allowlist. Otherwise, the instance becomes an open image proxy and can be pointed at hosts inside your network. In
/etc/thumbor/thumbor.conf, set:
text LOADER = 'thumbor.loaders.http_loader' ALLOWED_SOURCES = ['media.example.com', '^https://cdn\.example\.com/']
Then run sudo systemctl restart thumbor. Leaving ALLOWED_SOURCES empty while the HTTP loader is active allows any host, so treat a non-empty allowlist as mandatory. You can also keep both loaders: thumbor accepts a list of loaders if you want local files and one trusted origin.
- Enable unsigned URLs (development only): setting ALLOW_UNSAFE_URL = True in /etc/thumbor/thumbor.conf re-enables the /unsafe/ prefix, which is convenient while you experiment locally. Never do this on an instance reachable from the internet, because it lets anybody run arbitrary transformations on your CPU.
- Add TLS for production: put your own domain in front of nginx and terminate TLS with a certificate from your CA or Let's Encrypt. Renditions are immutable for a given signed URL, so they work well behind Azure Front Door or any CDN.
- Tune limits: MAX_WIDTH and MAX_HEIGHT (4000 by default in this image) cap how large a rendition a caller may request, QUALITY sets the JPEG quality, and RESULT_STORAGE_EXPIRATION_SECONDS controls how long renditions are cached. All of these settings live in /etc/thumbor/thumbor.conf.
- Updates: this image pins thumbor to 7.8.0 for reproducibility. SecureAnyCloud publishes refreshed images. For security patches, apply OS updates with sudo apt update && sudo apt upgrade.
Support
SecureAnyCloud maintains this image and provides 24/7 support. For help deploying or operating thumbor on Azure, contact SecureAnyCloud support through the Azure Marketplace listing.
Deploy on Azure
Launch thumbor 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