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", "")