Application Infrastructure Azure

thumbor on Ubuntu 24.04 on Azure User Guide

| Product: thumbor on Ubuntu 24.04 LTS on Azure

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 root thumbor systemd service bound to loopback 127.0.0.1:8888
  • An nginx reverse proxy on :80 publishing 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.service as systemd units, enabled and active
  • An unauthenticated /healthz endpoint 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.

The thumbor and nginx services reporting active, the pinned thumbor 7.8.0 version banner, the loopback thumbor listener on 127.0.0.1:8888 with nginx on port 80, and the one shot first boot unit reporting active

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.

The per VM credentials file listing the instance URL, demo page username, password and signing key with the secrets redacted, the 0600 root only permissions on the credentials file, the bcrypt only .htpasswd, and the two bundled sample images in the image library

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.

The hardened thumbor configuration showing the redacted per VM signing key, URL signing enforced, the local file loader and uploads disabled, followed by the unsigned request and the forged signature both rejected with HTTP 400 and the demo page returning HTTP 401 without a password

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.

Generating a signed 400 by 250 rendition URL with thumbor-url, fetching it over HTTP for a 200 image/jpeg response, decoding it to confirm the pixel dimensions are exactly 400 by 250, and the thumbor and nginx health endpoints both responding

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.

The thumbor demo page behind its password gate, showing the security posture badges for enforced URL signing, rejected unsafe URLs and the local only loader, above the bundled 1600 by 1000 source image and the start of the rendition grid

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.

Three renditions of the same source side by side: a 400 by 250 resize of the whole scene, a 400 by 400 smart crop that keeps the ringed disc and skyline fully in frame, and a plain 400 by 400 centre crop that cuts the disc in half at the right edge, each captioned with the signed URL that produced it

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.

Further renditions of the same source showing a grayscale conversion, a chained blur, brightness and rounded corner filter, a fit-in rendition with a filled background, and a transparent PNG banner resized and then re encoded to WebP, each with its signed URL

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.

A single 1000 by 560 smart cropped rendition with brightness and rounded corner filters applied, opened directly in a browser from its signed URL with no password prompt, showing the subject fully in frame

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.txt for your records. No restart is needed.
  • Rotate the signing key: edit the SECURITY_KEY line in /etc/thumbor/thumbor.conf and run sudo 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 with sudo 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.conf set:

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.