thumbor on Ubuntu 24.04 on Azure User Guide
Overview
thumbor is an open source, on demand image processing service. Instead of pre generating every thumbnail your application might one day need, you ask for a rendition in the URL and thumbor produces it at request time: resize it, crop it, smart crop it around the interesting part of the picture, run a chain of filters over it, and re encode it to another format. The cloudimg image installs thumbor 7.8.0 into a dedicated Python virtualenv, runs it as a hardened non root thumbor systemd service bound to loopback, and fronts it on port 80 with nginx. A unique URL signing key and a unique demo page password are generated on the first boot of every VM. Two sample images ship in the appliance, so every URL in this guide renders the moment your VM boots, with nothing for you to supply. Backed by 24/7 cloudimg support.
What is included:
- 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 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 supplied
thumbor.service+nginx.serviceas systemd units, enabled and active- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 cloudimg support
thumbor is licensed under the MIT License (Copyright (c) 2014 Globo.com), a permissive licence with no fee and no key to enter. cloudimg 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 will fetch whatever URL is embedded in the path, which makes an exposed instance both an open image proxy and a way to reach hosts inside your network.
This image closes both 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 cloudimg 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 themselves are deliberately not behind a password: they are meant to be fetched straight by your users' browsers and by a CDN, and the signature is what controls access. The operator facing demo page is separately gated by a per VM HTTP Basic Auth credential. Both the signing key and that password are written to a root only file on first boot. Maintenance below shows how to opt in to the HTTP loader with a mandatory allowlist if you need to serve images from your own origin.
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 point; thumbor is CPU bound while rendering, so size up if you expect sustained concurrent traffic on large source images. NSG inbound: allow 22/tcp from your management network, 80/tcp for renditions and the demo page, and 443/tcp if 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, choose Create a resource, search the Marketplace for thumbor by cloudimg, and select Create. On Basics pick 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). Then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name thumbor \
--image <marketplace-image-urn> \
--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 1010
Step 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.service
Both 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 connector 127.0.0.1:8888, and 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. That is why systemctl is-enabled thumbor-firstboot.service reports disabled on a VM that has already booted: disabled is the expected, correct end state, not a fault.

Step 5 - Retrieve your per VM key and password
Everything unique to your instance is written to a root only file on first boot:
sudo cat /root/thumbor-credentials.txt
sudo stat -c '%n %a %U:%G' /root/thumbor-credentials.txt /var/lib/thumbor/.htpasswd
THUMBOR_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, and 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 ever ship inside the image itself. Store both somewhere safe: anyone with the signing key can request renditions from your instance.

Step 6 - Confirm the health endpoints
curl -s http://localhost/healthz
curl -s http://localhost/healthcheck
/healthz returns ok and is served statically by nginx, 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 requires authentication and neither reveals anything about your configuration.
Step 7 - Confirm that URL signing is enforced
This is the check worth running before you put the instance on a public address. An unsigned /unsafe/ request and a forged signature must both be refused, 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 and -e is height (-h is --help), -s means smart crop, and the tool prints a URL: header line first, so pipe 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.jpg
You get 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 you would 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>/. Your browser prompts for a username and password: enter the THUMBOR_USERNAME and THUMBOR_PASSWORD from Step 5. The page is generated on your VM's first boot, and every image on it is a live rendition signed with your key and produced by your instance at the moment you loaded the page. The banner across the top restates the posture the instance is running: 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 asks for three different things. A plain 400x250 resize keeps the whole scene. A 400x400 smart crop asks thumbor to run feature detection first and centre the square on whatever is visually interesting, which here is the ringed disc and the cluster of buildings on the right. The same 400x400 square without smart simply 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, and filters:format(webp) re encodes on the fly, so you can serve WebP to browsers that accept it without keeping a second copy of anything.

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 rendition is returned with no password prompt at all, because a signed URL is the credential: this is exactly how your application will reference images 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 drop into /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:
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 you would 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 asks for.
Rendered results are cached under /var/lib/thumbor/result_storage for 24 hours, so a repeated request for the same rendition is served from disk rather than re rendered.
Maintenance
- Change the demo page password: run
sudo htpasswd -B /var/lib/thumbor/.htpasswd thumbor, 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 exactly what you want if a key leaks. Clear the cached results too withsudo rm -rf /var/lib/thumbor/result_storage/*. - Serve images from your own origin (opt in, and 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.confset:
text
LOADER = 'thumbor.loaders.http_loader'
ALLOWED_SOURCES = ['media.example.com', '^https://cdn\.example\.com/']
then 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: 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 are experimenting locally. Never do it on an instance reachable from the internet: 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 sit very comfortably behind Azure Front Door or any CDN.
- Tune limits: MAX_WIDTH and MAX_HEIGHT (4000 by default here) 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 live in /etc/thumbor/thumbor.conf.
- Updates: thumbor is pinned to 7.8.0 in this image for reproducibility. cloudimg publishes refreshed images; for security patches apply OS updates with sudo apt update && sudo apt upgrade.
Support
This image is maintained by cloudimg with 24/7 support. For help deploying or operating thumbor on Azure, contact cloudimg support through the Azure Marketplace listing.