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 detection

  • thumbor.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", "")