Production deployment checklist¶
This guide covers the key considerations for deploying plone.pgthumbor and Thumbor in a production environment.
Thumbor image¶
Use the pre-built OCI image from GHCR:
thumbor:
image: ghcr.io/bluedynamics/zodb-pgjsonb-thumborblobloader:latest
environment:
PGTHUMBOR_DSN: "dbname=zodb host=postgres user=zodb password=zodb"
THUMBOR_SECURITY_KEY: "${THUMBOR_SECURITY_KEY}"
PGTHUMBOR_PLONE_AUTH_URL: "http://plone:8080/Plone"
The image is available for linux/amd64 and linux/arm64.
It is configured entirely via environment variables – no thumbor.conf
editing is needed.
See Docker image for the full
variable reference.
The image is automatically rebuilt weekly when a new Thumbor version
appears on PyPI.
Pin a specific version tag (for example
thumbor-7.7.7_loader-0.3.0) for reproducible deployments.
HMAC key management¶
Generate a strong random key (at least 32 characters):
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
This key must be shared between Plone and Thumbor:
Service |
Setting |
|---|---|
Plone |
|
Thumbor |
|
Store the key in a secrets manager (Vault, AWS Secrets Manager, Docker secrets). Never commit it to version control.
Disable unsafe mode¶
In thumbor.conf:
ALLOW_UNSAFE_URL = False
On the Plone side, do not set PGTHUMBOR_UNSAFE=true.
Reverse proxy configuration¶
Thumbor should not be directly exposed to the internet. Place it behind a reverse proxy (nginx, Traefik, Caddy).
nginx example¶
server {
listen 443 ssl http2;
server_name example.com;
# Thumbor image serving
location /thumbor/ {
rewrite ^/thumbor/(.*)$ /$1 break;
proxy_pass http://thumbor:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache scaled images at the proxy level
proxy_cache_valid 200 30d;
proxy_cache_valid 404 1m;
}
# Plone backend with VirtualHostMonster
location / {
rewrite ^(.*)$ /VirtualHostBase/https/$host:443/Plone/VirtualHostRoot$1 break;
proxy_pass http://plone:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Key points:
The
/thumbor/prefix is stripped before forwarding to Thumbor.Set
PGTHUMBOR_SERVER_URLon the Plone side to the public-facing URL, for examplehttps://example.com/thumbor.Plone generates Thumbor URLs using this base URL, so it must be reachable from the end user’s browser.
Traefik example (Docker Labels)¶
thumbor:
labels:
- "traefik.enable=true"
- "traefik.http.routers.thumbor.rule=Host(`example.com`) && PathPrefix(`/thumbor/`)"
- "traefik.http.routers.thumbor.middlewares=thumbor-strip"
- "traefik.http.middlewares.thumbor-strip.stripprefix.prefixes=/thumbor"
- "traefik.http.services.thumbor.loadbalancer.server.port=8888"
Internal network for auth requests¶
The auth_handler in Thumbor calls Plone’s @thumbor-auth endpoint to verify
access for non-public images.
This must be an internal, direct URL that
bypasses the reverse proxy:
# thumbor.conf
PGTHUMBOR_PLONE_AUTH_URL = "http://plone:8080/Plone"
Reasons:
Avoids routing loops (nginx forwards
/thumbor/to Thumbor, which calls back to Plone through nginx).Reduces latency – no TLS termination or proxy overhead.
Allows network-level isolation (Thumbor and Plone on the same Docker network or internal subnet).
In Docker Compose, both services share a network by default.
In Kubernetes,
use the service DNS name (for example http://plone-service:8080/Plone).
HTTPS¶
All traffic between the browser and the reverse proxy must use HTTPS.
Configure TLS on the reverse proxy (Let’s Encrypt, cert-manager, or your own certificates).
Set
PGTHUMBOR_SERVER_URLtohttps://...so Plone generates HTTPS image URLs.Thumbor itself does not need TLS – it runs behind the reverse proxy on an internal network.
Thumbor result storage¶
Thumbor caches already-scaled images in its result storage. For production:
File storage (single-node deployments):
RESULT_STORAGE = "thumbor.result_storages.file_storage"
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH = "/var/cache/thumbor/results"
Mount this path as a persistent volume so the cache survives container restarts.
No storage (CDN-fronted deployments):
RESULT_STORAGE = "thumbor.result_storages.no_storage"
If a CDN (CloudFront, Fastly, Cloudflare) caches responses, Thumbor’s own result cache is unnecessary.
Blob disk cache sizing¶
The loader-side disk cache (PGTHUMBOR_CACHE_DIR / PGTHUMBOR_CACHE_MAX_SIZE)
caches raw blob bytes before Thumbor processes them.
This is especially
useful when the same source image is requested at multiple sizes.
Sizing guidelines:
Estimate the total size of your most frequently accessed images.
A 1–5 GB cache is a good starting point for most sites.
The cache uses LRU eviction based on access time. It evicts down to 90% of
PGTHUMBOR_CACHE_MAX_SIZEwhen the limit is hit.
PGTHUMBOR_CACHE_DIR = "/var/cache/thumbor/blobs"
PGTHUMBOR_CACHE_MAX_SIZE = 5368709120 # 5 GB
Connection pool sizing¶
The default pool settings (PGTHUMBOR_POOL_MIN_SIZE=1,
PGTHUMBOR_POOL_MAX_SIZE=4) work for low-traffic sites.
For higher
concurrency:
PGTHUMBOR_POOL_MIN_SIZE = 2
PGTHUMBOR_POOL_MAX_SIZE = 16
Each connection uses one PostgreSQL backend slot.
Make sure
max_connections in postgresql.conf has enough headroom for all services.
Auth cache TTL¶
The default PGTHUMBOR_AUTH_CACHE_TTL=60 means that permission changes take
up to 60 seconds to take effect for cached images.
Adjust based on your
security requirements:
Strict:
10– near-real-time permission enforcement, more Plone round-trips.Relaxed:
300– fewer round-trips, permissions lag up to 5 minutes.
Monitoring¶
Health check: Thumbor exposes
/healthcheckwhenthumbor.handler_lists.healthcheckis inHANDLER_LISTS. Use this for load balancer probes.Logs: Monitor Thumbor logs for
SchemaError(missingblob_statetable), pool connection failures, and S3 download errors.Metrics: Thumbor supports Statsd metrics out of the box. Configure
STATSD_HOSTandSTATSD_PORTinthumbor.conf.
Summary checklist¶
[ ] Strong random HMAC key, shared between Plone and Thumbor
[ ]
ALLOW_UNSAFE_URL = Falsein Thumbor[ ]
PGTHUMBOR_UNSAFEnot set (orfalse) in Plone[ ] Reverse proxy (nginx/Traefik) in front of both Plone and Thumbor
[ ]
PGTHUMBOR_SERVER_URLpoints to the public HTTPS URL[ ]
PGTHUMBOR_PLONE_AUTH_URLuses internal direct URL[ ] HTTPS on the reverse proxy
[ ] Persistent volume for Thumbor result storage
[ ] Blob disk cache sized appropriately
[ ] Connection pool sized for expected concurrency
[ ] Health check endpoint monitored