Configure Thumbor for plone.pgthumbor¶
This guide covers every thumbor.conf setting relevant to the
plone.pgthumbor stack.
All settings are standard Thumbor configuration keys
that can also be set via environment variables.
Tip
When using the pre-built Docker image,
all settings below are configurable via environment variables.
No thumbor.conf editing is required.
Minimal configuration¶
LOADER = "zodb_pgjsonb_thumborblobloader.loader"
HANDLER_LISTS = [
"thumbor.handler_lists.healthcheck",
"zodb_pgjsonb_thumborblobloader.auth_handler",
]
SECURITY_KEY = "your-secret-key"
PGTHUMBOR_DSN = "dbname=zodb host=postgres port=5432 user=zodb password=zodb"
Loader and handlers¶
LOADER¶
The image loader module. Must be set to the zodb-pgjsonb blob loader:
LOADER = "zodb_pgjsonb_thumborblobloader.loader"
This loader reads image blobs directly from the blob_state table in
PostgreSQL using an async connection pool (psycopg 3).
HANDLER_LISTS¶
Custom Thumbor handler lists.
The auth_handler module adds access control
for non-public Plone content:
HANDLER_LISTS = [
"thumbor.handler_lists.healthcheck",
"zodb_pgjsonb_thumborblobloader.auth_handler",
]
The healthcheck handler must come first so /healthcheck is matched before
the image URL regex.
The auth_handler intercepts requests with 3-segment blob paths
(<blob_zoid>/<tid>/<content_zoid>) and verifies access with Plone via the
@thumbor-auth REST endpoint before delivering the image.
Two-segment paths
(<blob_zoid>/<tid>) are served without any access check.
Security¶
SECURITY_KEY¶
The shared HMAC-SHA1 key for signing Thumbor URLs. Plone uses this key to generate signed URLs; Thumbor uses it to verify them:
SECURITY_KEY = "your-secret-key"
Can also be set via the THUMBOR_SECURITY_KEY environment variable:
import os
SECURITY_KEY = os.environ.get("THUMBOR_SECURITY_KEY", "")
Warning
Use a strong, random key in production (at least 32 characters).
The key
must be identical in Thumbor’s SECURITY_KEY and Plone’s
PGTHUMBOR_SECURITY_KEY environment variable.
ALLOW_UNSAFE_URL¶
Allow unsigned /unsafe/ URLs. Must be False in production.
ALLOW_UNSAFE_URL = False
When True, Thumbor accepts URLs prefixed with /unsafe/ without HMAC
verification.
Useful for development only.
Automatic image format conversion¶
AUTO_WEBP¶
Automatically convert images to WebP when the browser’s Accept header
includes image/webp.
Default: True.
AUTO_WEBP = True
WebP typically reduces file size by 25-35% compared to JPEG at equivalent quality. All modern browsers support WebP.
AUTO_AVIF¶
Automatically convert images to AVIF when the browser’s Accept header
includes image/avif.
Default: False (opt-in).
AUTO_AVIF = False
AVIF offers better compression than WebP but requires significantly more CPU for encoding. Enable this if your Thumbor server has sufficient compute capacity.
Both settings can be configured via environment variables:
import os
AUTO_WEBP = os.environ.get("THUMBOR_AUTO_WEBP", "true").lower() in ("true", "1", "yes")
AUTO_AVIF = os.environ.get("THUMBOR_AUTO_AVIF", "false").lower() in ("true", "1", "yes")
Note
Format conversion is transparent – the same signed Thumbor URL serves different formats based on content negotiation. The result storage caches each format variant separately.
Smart cropping¶
DETECTORS¶
Thumbor’s detector modules for content-aware cropping via /smart/ URLs.
When configured, Thumbor runs OpenCV-based detection to identify focal
points (faces, corners, edges) and crops around them:
DETECTORS = [
"thumbor.detectors.face_detector",
"thumbor.detectors.feature_detector",
]
Face detection is tried first. If no faces are found, feature detection (corners/edges) is used as fallback. Detection runs in-process and results are cached by Thumbor’s result storage, so each unique URL triggers detection only once.
Available detectors:
thumbor.detectors.face_detector– frontal face detection (Haar cascade)thumbor.detectors.feature_detector– corner/edge detection (good-features-to-track)thumbor.detectors.profile_detector– side profile face detectionthumbor.detectors.glasses_detector– glasses detection (supplements face detector)
Can be configured via environment variable:
import os
_detectors = os.environ.get("THUMBOR_DETECTORS", "")
if _detectors:
DETECTORS = [d.strip() for d in _detectors.split(",") if d.strip()]
Note
The pre-built Docker image includes
opencv-python-headless. If running Thumbor without the Docker image,
install it manually: pip install opencv-python-headless.
On the Plone side, also enable smart_cropping in the registry
(@@thumbor-settings) so that Plone generates URLs with /smart/.
Result storage¶
RESULT_STORAGE¶
Thumbor’s built-in result cache. Stores already-transformed images so repeated requests skip re-processing:
RESULT_STORAGE = "thumbor.result_storages.file_storage"
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH = "/tmp/thumbor/result_storage"
The file storage is the simplest option.
For production deployments consider
thumbor.result_storages.no_storage if you rely solely on an upstream CDN
cache, or a Redis-based result storage for clustered Thumbor setups.
PostgreSQL connection¶
PGTHUMBOR_DSN¶
PostgreSQL connection string for the blob_state table.
Uses libpq
connection string format:
PGTHUMBOR_DSN = "dbname=zodb host=postgres port=5432 user=zodb password=zodb"
Can be set via environment variable:
import os
PGTHUMBOR_DSN = os.environ.get("PGTHUMBOR_DSN", "")
The loader verifies that the blob_state table exists on first connection.
PGTHUMBOR_POOL_MIN_SIZE¶
Minimum number of connections in the async connection pool.
Default: 1.
PGTHUMBOR_POOL_MIN_SIZE = 1
PGTHUMBOR_POOL_MAX_SIZE¶
Maximum number of connections in the async connection pool.
Default: 4.
PGTHUMBOR_POOL_MAX_SIZE = 4
Increase this if Thumbor handles many concurrent image requests. Each connection holds a PostgreSQL backend slot.
Plone access control¶
PGTHUMBOR_PLONE_AUTH_URL¶
Internal URL of the Plone site, used by the auth_handler to verify access
for non-public images.
This should be a direct URL to Plone, bypassing any
reverse proxy to avoid loops and reduce latency:
PGTHUMBOR_PLONE_AUTH_URL = "http://plone:8080/Plone"
Can be set via environment variable:
import os
PGTHUMBOR_PLONE_AUTH_URL = os.environ.get("PGTHUMBOR_PLONE_AUTH_URL", "")
The auth handler calls <url>/@thumbor-auth?zoid=<content_zoid_hex> with the
browser’s Cookie and Authorization headers forwarded.
Plone returns 200 if
the user may view the content, or 403/401 otherwise.
If this setting is empty and a 3-segment (authenticated) URL is requested, the handler denies the request.
PGTHUMBOR_AUTH_CACHE_TTL¶
How long (in seconds) to cache auth results.
Default: 60.
PGTHUMBOR_AUTH_CACHE_TTL = 60
Auth results are cached per (content_zoid, cookie_header) tuple.
A shorter
TTL means more frequent Plone round-trips but faster permission revocation.
Disk cache (loader-side)¶
The loader has its own disk cache, separate from Thumbor’s result storage. This caches raw blob bytes to avoid repeated PostgreSQL or S3 fetches.
PGTHUMBOR_CACHE_DIR¶
Directory for the local disk cache. Empty string (default) disables caching:
PGTHUMBOR_CACHE_DIR = "/tmp/thumbor/blob_cache"
PGTHUMBOR_CACHE_MAX_SIZE¶
Maximum cache size in bytes.
Default: 0 (disabled).
LRU eviction removes
the least-recently-accessed files when the cache exceeds this limit:
# 1 GB cache
PGTHUMBOR_CACHE_MAX_SIZE = 1073741824
The cache uses deterministic filenames ({zoid:016x}-{tid:016x}.blob).
Since blobs are addressed by immutable (zoid, tid) pairs, there is no
cache invalidation concern – only LRU eviction for space.
S3 fallback¶
For tiered blob storage where large blobs are offloaded to S3. See Enable S3 fallback for large blobs for a detailed setup guide.
PGTHUMBOR_S3_BUCKET¶
S3 bucket name. Empty string (default) disables S3 fallback:
PGTHUMBOR_S3_BUCKET = "my-blobs"
PGTHUMBOR_S3_REGION¶
AWS region.
Default: us-east-1:
PGTHUMBOR_S3_REGION = "eu-central-1"
PGTHUMBOR_S3_ENDPOINT¶
Custom S3 endpoint URL. Empty string (default) uses AWS. Set this for S3-compatible services like MinIO:
PGTHUMBOR_S3_ENDPOINT = "http://minio:9000"
Full example¶
import os
# Loader
LOADER = "zodb_pgjsonb_thumborblobloader.loader"
# Handlers
HANDLER_LISTS = [
"thumbor.handler_lists.healthcheck",
"zodb_pgjsonb_thumborblobloader.auth_handler",
]
# Security
SECURITY_KEY = os.environ.get("THUMBOR_SECURITY_KEY", "change-me")
ALLOW_UNSAFE_URL = False
# Auto-convert to modern formats when browser supports them
AUTO_WEBP = True
AUTO_AVIF = False
# Smart cropping (requires opencv-python-headless, included in Docker image)
_detectors = os.environ.get("THUMBOR_DETECTORS", "")
if _detectors:
DETECTORS = [d.strip() for d in _detectors.split(",") if d.strip()]
# Result storage
RESULT_STORAGE = "thumbor.result_storages.file_storage"
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH = "/tmp/thumbor/result_storage"
# PostgreSQL
PGTHUMBOR_DSN = os.environ.get("PGTHUMBOR_DSN", "")
PGTHUMBOR_POOL_MIN_SIZE = 2
PGTHUMBOR_POOL_MAX_SIZE = 8
# Plone access control
PGTHUMBOR_PLONE_AUTH_URL = os.environ.get("PGTHUMBOR_PLONE_AUTH_URL", "")
PGTHUMBOR_AUTH_CACHE_TTL = int(os.environ.get("PGTHUMBOR_AUTH_CACHE_TTL", "60"))
# Disk cache
PGTHUMBOR_CACHE_DIR = "/var/cache/thumbor/blobs"
PGTHUMBOR_CACHE_MAX_SIZE = 1073741824 # 1 GB
# S3 fallback (optional)
PGTHUMBOR_S3_BUCKET = os.environ.get("PGTHUMBOR_S3_BUCKET", "")
PGTHUMBOR_S3_REGION = os.environ.get("PGTHUMBOR_S3_REGION", "us-east-1")
PGTHUMBOR_S3_ENDPOINT = os.environ.get("PGTHUMBOR_S3_ENDPOINT", "")