Docker image for WordPress and ClassicPress hosting with NGINX and PHP 8.4.
- ⚡ Based on Debian + NGINX 1.26 + PHP 8.4 + s6-overlay v3
- 🌱 Everything is done via environment variables; PHP configurations, NGINX, and even WordPress constants are handled by environment variables. No need to edit wp-config.php.
- 🧠 Real caching support, works well with WP Super Cache, W3 Total Cache, WP Fastest Cache, and also with NGINX FastCGI Cache (via NGINX Helper).
- 🌐 CDN-ready without plugins!
- 🗄️ Optional local Redis/Valkey server
- 🔐 Docker secrets (
*_FILE) supported everywhere, including PHP-FPM requests. - 📦 Separate code, uploads, cache, and logs!
- 🔧 Interactive installer, start the container, run
docker exec -it presshost ./presshostand perform the guided installation.
PressHost is already trusted by high-traffic websites, including:
- 🎫 Catraca Livre - cultural events and entertainment platform
- 📖 Manual do Usuario - technology and tutorials website
Together, these sites handle more than 20 million pageviews per month, proving PressHost's reliability and performance at scale.
wget https://raw.githubusercontent.com/butialabs/presshost/main/compose.yml
nano compose.yml
docker compose up -d| Variable | Description | Example |
|---|---|---|
DB_NAME |
Database name | presshost |
DB_USER |
Database user | presshost |
DB_PASSWORD |
Database password | p@ssw0rd |
DB_HOST |
Database host | db |
SITEURL |
Site URL, used for server_name and as default for WP_SITEURL/WP_HOME |
https://your-domain.xyz |
WP_SITEURLandWP_HOMEfall back toSITEURLwhen not set.server_nameis also derived from the host portion ofSITEURL.
| Path | Description |
|---|---|
/site/press |
Press files |
/site/uploads |
Media files (wp-content/uploads) |
/site/cache |
Cache files (wp-content/cache) and NGINX |
/site/logs |
Log files |
If it's a migration, you can skip the installation and just copy the files to the correct volumes.
Upon startup, an index.php file would be displayed automatically if none already exists.
If you are upgrading from a previous version, review these changes:
| Change | Impact |
|---|---|
PHP_APC_* renamed to PHP_APCU_* |
Update your env vars (PHP_APC_ENABLED -> PHP_APCU_ENABLED, etc.) |
HSTS no longer sends preload by default |
Set NGINX_HSTS_PRELOAD=true to restore the old header |
| Spam-keyword blocking is now opt-in | Set NGINX_BLOCK_SPAM=true to restore the old behavior |
| Mobile-split cache removed | NGINX_CACHE_SPLIT_MOBILE no longer exists; cache key is unified |
| Timeout chain aligned | PHP_FPM_REQUEST_TERMINATE_TIMEOUT 60->130, NGINX_FASTCGI_READ_TIMEOUT 300s->180s |
disable_functions no longer applies to PHP-CLI |
Use PHP_CLI_DISABLE_FUNCTIONS if you want CLI restrictions |
open_basedir no longer includes /proc/ |
PHP code can no longer read /proc (security hardening) |
| Variable | Default | Description |
|---|---|---|
DB_CHARSET |
utf8mb4 |
Database character set |
DB_COLLATE |
utf8mb4_unicode_ci |
Database collation |
| Variable | Default | Description |
|---|---|---|
WP_ENVIRONMENT_TYPE |
production |
Environment type (production, staging, development) |
WP_DEBUG |
false |
Enable debug mode |
WP_DEBUG_LOG |
false |
Enable debug logging |
WP_DEBUG_DISPLAY |
false |
Display debug messages |
SAVEQUERIES |
false |
Save database queries for debugging |
AUTOMATIC_UPDATER_DISABLED |
false |
Disable automatic updates |
DISALLOW_FILE_EDIT |
false |
Disable file editing in admin |
DISALLOW_FILE_MODS |
false |
Disable file modifications in admin |
WPLANG |
en_US |
Language setting |
FS_METHOD |
direct |
Filesystem method |
FORCE_SSL_ADMIN |
true |
Force SSL for admin |
FORCE_SSL_LOGIN |
true |
Force SSL for login |
AUTOSAVE_INTERVAL |
120 |
Autosave interval (seconds) |
WP_POST_REVISIONS |
30 |
Post revisions limit (-1 for unlimited) |
WP_MEMORY_LIMIT |
256M |
Memory limit |
WP_MAX_MEMORY_LIMIT |
512M |
Maximum memory limit on Admin |
WP_CACHE |
false |
Enable caching |
WP_CACHE_KEY_SALT |
`` | Cache key salt |
WP_TABLE_PREFIX |
wp_ |
Database table prefix, change before first install |
MEDIA_TRASH |
true |
Enable media trash functionality |
DISABLE_NAG_NOTICES |
true |
Disable admin nag notices |
DISABLE_WP_CRON |
true |
Disable WP-Cron (handled by supercronic instead) |
Salts are generated automatically at startup if they are not defined.
| Variable | Default | Description |
|---|---|---|
AUTH_KEY |
`` | Authentication key |
SECURE_AUTH_KEY |
`` | Secure authentication key |
LOGGED_IN_KEY |
`` | Logged-in key |
NONCE_KEY |
`` | Nonce key |
AUTH_SALT |
`` | Authentication salt |
SECURE_AUTH_SALT |
`` | Secure authentication salt |
LOGGED_IN_SALT |
`` | Logged-in salt |
NONCE_SALT |
`` | Nonce salt |
| Variable | Default | Description |
|---|---|---|
SMTP_USER |
`` | SMTP username |
SMTP_PASS |
`` | SMTP password |
SMTP_HOST |
`` | SMTP host |
SMTP_FROM |
`` | SMTP from email |
SMTP_NAME |
`` | SMTP from name |
SMTP_PORT |
`` | SMTP port |
SMTP_SECURE |
`` | SMTP security type (ssl/tls) |
SMTP_AUTH |
true |
Enable SMTP authentication |
SMTP_DEBUG |
false |
Enable SMTP debugging |
| Variable | Default | Description |
|---|---|---|
APP_PATH |
/site/press |
Application path |
UPLOADS_PATH |
/site/uploads |
Uploads directory path |
CACHE_PATH |
/site/cache |
Cache directory path |
LOGS_PATH |
/site/logs |
Logs directory path |
APP_USER |
www-data |
User that owns /site/press and runs the php-fpm/nginx workers |
APP_GROUP |
www-data |
Group counterpart of APP_USER |
NGINX_HTTP_PORT |
80 |
HTTP port the container listens on |
NGINX_HTTPS_PORT |
443 |
HTTPS port the container listens on |
NGINX_HTTP_REDIRECT |
true |
Redirect HTTP > HTTPS when SITEURL is https://. Set false behind a TLS-terminating proxy to serve the site on NGINX_HTTP_PORT |
NGINX_PATH_PREFIX |
derived from WP_HOME |
Subpath the site is published under (e.g. /blog for SITEURL=https://example.com/blog). Requests under it are rewritten to the WordPress files at APP_PATH root |
TZ |
UTC |
Timezone |
FIX_OWNERSHIP |
true |
At startup, chown -R app dirs to APP_USER when their owner is wrong, in the background |
FIX_UPLOADS_OWNERSHIP |
false |
Also include UPLOADS_PATH in the startup ownership fix (skipped by default, it's usually the largest tree) |
WP/CP cron events are driven by supercronic (DISABLE_WP_CRON defaults to true), running /usr/local/bin/press-cron.sh every minute
By default the events run through WP-CLI, on the PHP CLI binary. That is a different SAPI from the PHP-FPM workers serving the site: separate opcache, separate disable_functions, and a separate object-cache connection. When an object-cache.php drop-in is installed on ClassicPress, cache-flushing events executed on the CLI side never reach the cache the FPM workers are using, so the flush silently does nothing.
To avoid that, press-cron.sh detects a ClassicPress install and dispatches the run with curl to wp-cron.php over the loopback interface instead.
| Variable | Default | Description |
|---|---|---|
PRESS_CRON_MODE |
auto |
auto = HTTP only when an object-cache.php drop-in is present on ClassicPress; http = always dispatch over HTTP; cli = always use WP-CLI |
PRESS_CRON_HTTP_TIMEOUT |
120 |
curl --max-time for the wp-cron.php request (seconds); keep it below PHP_FPM_REQUEST_TERMINATE_TIMEOUT |
PRESS_CRON_HTTP_CONNECT_TIMEOUT |
5 |
curl --connect-timeout for the same request (seconds) |
When baking WordPress/ClassicPress files into your own image, set the owner at build time for fastest startup:
FROM ghcr.io/butialabs/presshost:latest
COPY --chown=www-data:www-data ./wordpress/ /site/press/If you forget --chown, the init fixes ownership automatically at container start (see FIX_OWNERSHIP).
| Variable | Default | Description |
|---|---|---|
PHP_MEMORY_LIMIT |
512M |
Memory limit |
PHP_MAX_EXECUTION_TIME |
120 |
Max execution time (seconds) |
PHP_MAX_INPUT_TIME |
120 |
Max input time (seconds) |
PHP_MAX_INPUT_VARS |
3000 |
Max input variables |
PHP_POST_MAX_SIZE |
64M |
Max POST size |
PHP_UPLOAD_MAX_FILESIZE |
64M |
Max upload size |
PHP_DEFAULT_SOCKET_TIMEOUT |
60 |
Default socket timeout (seconds) |
PHP_OUTPUT_BUFFERING |
4096 |
Output buffering size |
PHP_PM |
auto |
Process manager type (auto, static, dynamic, ondemand). auto picks static for ≤2GB containers, dynamic otherwise. |
PHP_PM_MAX_CHILDREN |
auto |
Max children processes (auto sizes from container memory; or set a fixed integer like 50) |
PHP_PM_WORKER_MB |
128 |
Assumed average worker size (MB) used by the auto calculation |
PHP_PM_START_SERVERS |
10 |
Start servers (preforked workers) |
PHP_PM_MIN_SPARE_SERVERS |
10 |
Min spare servers |
PHP_PM_MAX_SPARE_SERVERS |
35 |
Max spare servers |
PHP_PM_MAX_REQUESTS |
500 |
Max requests per child (prevents memory leaks) |
PHP_PM_PROCESS_IDLE_TIMEOUT |
10s |
Idle timeout for ondemand PM |
PHP_FPM_REQUEST_TERMINATE_TIMEOUT |
130 |
Kill stuck workers after N seconds (aligned with PHP_MAX_EXECUTION_TIME=120 + grace) |
PHP_FPM_LISTEN_BACKLOG |
65535 |
Listen queue backlog size |
PHP_FPM_RLIMIT_FILES |
65535 |
Max open files limit |
PHP_OPCACHE_ENABLE |
1 |
Enable OPcache |
PHP_OPCACHE_MEMORY |
256 |
OPcache memory (MB) |
PHP_OPCACHE_INTERNED_STRINGS |
16 |
Interned strings buffer (MB) |
PHP_OPCACHE_MAX_FILES |
20000 |
Max cached files |
PHP_OPCACHE_REVALIDATE_FREQ |
2 |
Revalidate frequency (seconds) |
PHP_OPCACHE_VALIDATE_TIMESTAMPS |
0 |
Validate timestamps (0 = production; redeploy/restart container to pick up code changes) |
PHP_OPCACHE_JIT |
off |
JIT mode (tracing, function, off). Disabled by default. |
PHP_OPCACHE_JIT_BUFFER_SIZE |
128M |
JIT buffer size (only used when JIT is enabled) |
PHP_OPCACHE_ENABLE_CLI |
0 |
Enable OPcache for PHP-CLI |
PHP_DISABLE_FUNCTIONS |
*1 | Comma-separated list of disabled PHP functions for FPM (set to empty string to allow all) |
PHP_CLI_DISABLE_FUNCTIONS |
`` | Comma-separated list for CLI (empty by default so WP-CLI works fully) |
PHP_SESSION_COOKIE_HTTPONLY |
1 |
Session cookie httponly |
PHP_SESSION_COOKIE_SECURE |
1 |
Session cookie secure |
PHP_SESSION_COOKIE_SAMESITE |
Strict |
Session cookie SameSite (Strict, Lax, None) |
PHP_SESSION_USE_STRICT_MODE |
1 |
Session use strict mode |
PHP_APCU_ENABLED |
1 |
Enable APCu |
PHP_APCU_SHM_SIZE |
64M |
APCu shared memory size |
PHP_APCU_TTL |
7200 |
APCu TTL (seconds) |
PHP_APCU_ENABLE_CLI |
0 |
Enable APCu for CLI |
PHP_REALPATH_CACHE_SIZE |
4096K |
Realpath cache size |
PHP_REALPATH_CACHE_TTL |
600 |
Realpath cache TTL (seconds) |
PHP_ERROR_REPORTING |
E_ALL & ~E_DEPRECATED & ~E_STRICT |
PHP error reporting level |
*1 exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source,pcntl_exec
| Variable | Default | Description |
|---|---|---|
NGINX_CLIENT_MAX_BODY_SIZE |
64m |
Client max body size |
NGINX_CLIENT_BODY_BUFFER_SIZE |
128k |
Client body buffer size |
NGINX_CLIENT_HEADER_BUFFER_SIZE |
1k |
Client header buffer size |
NGINX_LARGE_CLIENT_HEADER_BUFFERS |
4 16k |
Large client header buffers |
NGINX_OUTPUT_BUFFERS |
1 32k |
Output buffers |
NGINX_FASTCGI_BUFFER_SIZE |
32k |
FastCGI buffer size |
NGINX_FASTCGI_BUFFERS |
16 16k |
FastCGI buffers |
NGINX_FASTCGI_BUSY_BUFFERS_SIZE |
64k |
FastCGI busy buffers size |
NGINX_FASTCGI_CONNECT_TIMEOUT |
60s |
FastCGI connect timeout |
NGINX_FASTCGI_SEND_TIMEOUT |
60s |
FastCGI send timeout |
NGINX_FASTCGI_READ_TIMEOUT |
180s |
FastCGI read timeout |
NGINX_KEEPALIVE_TIMEOUT |
65s |
Keepalive timeout |
NGINX_KEEPALIVE_REQUESTS |
1000 |
Requests per keepalive |
NGINX_CLIENT_BODY_TIMEOUT |
60s |
Client body timeout |
NGINX_CLIENT_HEADER_TIMEOUT |
30s |
Client header timeout |
NGINX_SEND_TIMEOUT |
60s |
Send timeout |
NGINX_WORKER_PROCESSES |
auto |
Worker processes |
NGINX_WORKER_CONNECTIONS |
65535 |
Connections per worker |
NGINX_WORKER_RLIMIT_NOFILE |
65535 |
Max open files per worker |
NGINX_RESOLVER |
127.0.0.11 valid=30s |
DNS resolver for OCSP stapling (empty disables; 127.0.0.11 is Docker's embedded DNS) |
NGINX_CACHE |
false |
Enable NGINX FastCGI cache. When set to false, no cache directories or files are created |
NGINX_CACHE_MAX_SIZE |
512m |
Cache max size |
NGINX_CACHE_INACTIVE |
60m |
Cache inactive time |
NGINX_CACHE_VALID_OK |
60m |
Cache TTL for 200/301/302 responses |
NGINX_CACHE_VALID_NOT_FOUND |
1m |
Cache TTL for 404 responses |
NGINX_CACHE_KEY_ZONE_SIZE |
100m |
Shared memory zone size for cache keys (~8k keys/MB) |
NGINX_GZIP_COMP_LEVEL |
6 |
gzip compression level (1-9) |
NGINX_BROTLI_COMP_LEVEL |
4 |
brotli compression level (0-11) for dynamic content |
NGINX_HTTP3 |
false |
Enable HTTP/3 (QUIC) on port 443/udp |
NGINX_SERVER_NAME |
derived from SITEURL |
Override server_name directive |
NGINX_HSTS |
true |
Emit Strict-Transport-Security header |
NGINX_HSTS_MAX_AGE |
31536000 |
HSTS max-age (seconds) |
NGINX_HSTS_SUBDOMAINS |
true |
Add includeSubDomains to HSTS |
NGINX_HSTS_PRELOAD |
false |
Add preload to HSTS (only enable if you understand the commitment, see hstspreload.org) |
NGINX_BLOCK_SPAM |
false |
Block pharma/spam keywords in query strings (may cause false positives on legitimate searches) |
| Variable | Default | Description |
|---|---|---|
SSL_CERT_PATH |
/site/ssl/server.crt |
SSL certificate path |
SSL_PRIVATE_PATH |
/site/ssl/server.key |
SSL private key path |
SSL_TRUSTED_CERT_PATH |
/site/ssl/server.crt |
Trusted CA certificate for OCSP stapling |
NGINX_SSL_STAPLING |
off |
Enable OCSP stapling |
NGINX_SSL_STAPLING_VERIFY |
off |
Verify OCSP responses |
On the first start, if the files at SSL_CERT_PATH and SSL_PRIVATE_PATH do not exist, a self-signed certificate is generated automatically. Mount /site/ssl as a volume to persist it across container recreates and image updates.
Existing files are never overwritten, so providing your own certificate (Let's Encrypt, internal CA, etc.) just works by mounting it at the configured paths.
-
When
NGINX_CACHE=false(default): no cache zone is configured and no cache files are created. -
When
NGINX_CACHE=true:- The cache lives in
/site/cache/nginx/ - Cache files are generated during operation
- The
X-FastCGI-Cacheheader will be present in responses with values likeHIT,MISS, orBYPASS
- The cache lives in
The cache can be purged using the NGINX Cache Purge module. We recommend using the NGINX Helper
Two empty configuration files are included and loaded at startup. Mount your own versions to extend NGINX without modifying the image.
| File (inside container) | Scope | When to use |
|---|---|---|
/etc/nginx/conf.d/custom-nginx.conf |
http {} block (global) |
Custom upstreams, maps, rate-limit zones, etc. |
/etc/nginx/conf.d/custom-presshost.conf |
server {} block (site-level) |
Extra locations, reverse proxies, rewrites, etc. |
Example: reverse proxy at /i/ on the same domain:
# compose.yml
services:
presshost:
volumes:
- ./custom-presshost.conf:/etc/nginx/conf.d/custom-presshost.conf# custom-presshost.conf
location /i/ {
proxy_pass http://image-service:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}All logs are written under /site/logs and rotated daily by logrotate: nginx-access.log, nginx-error.log, php-fpm.log, php-error.log, php-slow.log, cron-presshost.log, cron-nginx.log and wp-debug.log (when WP_DEBUG_LOG=true).
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
WARN |
ERROR, WARN, INFO, DEBUG, TRACE. Default shows only errors and serious warnings. |
LOG_MAX_SIZE |
10M |
Logrotate max size per log file (rotates daily or when exceeded) |
LOG_MAX_AGE |
7 |
Number of rotated copies to keep (7 days) |
These variables are used during the interactive installation process via presshost command:
| Variable | Default | Description |
|---|---|---|
INSTALL_WORDPRESS_VERSION |
latest |
Specific WordPress version to install |
INSTALL_CLASSICPRESS_VERSION |
latest |
Specific ClassicPress version to install |
PressHost ships with built-in protections that keep PHP/NGINX healthy under load:
| Variable | Default | Description |
|---|---|---|
NGINX_RATE_LIMIT |
30r/s |
Per-IP request rate (zone global_limit) |
NGINX_RATE_BURST |
60 |
Burst tolerance before 429 |
NGINX_CONN_LIMIT |
50 |
Max simultaneous connections per IP |
NGINX_LOGIN_RATE_LIMIT |
5r/m |
Per-IP rate for wp-login.php (zone login_limit) |
NGINX_LOGIN_BURST |
10 |
Burst tolerance on wp-login.php before 429 |
PHP_FPM_REQUEST_TERMINATE_TIMEOUT |
130 |
Kill stuck workers after N seconds |
PHP_FPM_REQUEST_SLOWLOG_TIMEOUT |
5s |
Log slow requests |
PHP_FPM_EMERGENCY_RESTART_THRESHOLD |
10 |
Crashed workers in interval to trigger master restart |
PHP_FPM_EMERGENCY_RESTART_INTERVAL |
1m |
Interval for the emergency restart counter |
PHP_FPM_PROCESS_CONTROL_TIMEOUT |
10s |
Master/worker IPC timeout |
PHP-FPM runs with clear_env=yes. The init script forwards environment variables into the FPM pool using prefix rules:
| Forwarded | Examples |
|---|---|
DB_* |
DB_NAME, DB_PASSWORD, DB_HOST, … |
WP_* |
WP_DEBUG, WP_REDIS_HOST, WP_ROCKET_*, WP_AI_SUPPORT, … |
SMTP_* |
SMTP_HOST, SMTP_USER, … |
PRESS_* |
custom constants (see below) |
INSTALL_* |
installer variables |
| First-party list | salts (AUTH_KEY…NONCE_SALT), SITEURL, TZ, LOGS_PATH, UPLOADS_PATH, CACHE_PATH, APP_PATH, WPLANG, FS_METHOD, FORCE_SSL_*, AUTOSAVE_INTERVAL, MEDIA_TRASH, DISABLE_NAG_NOTICES, DISABLE_WP_CRON, DISALLOW_FILE_*, AUTOMATIC_UPDATER_DISABLED, SAVEQUERIES |
Not forwarded (never visible to PHP / phpinfo()): NGINX_*, PHP_*, VALKEY_*, HOME, PATH, and any unrelated container env vars.
Inside wp-config.php, every forwarded WP_* and PRESS_* variable is automatically defined as a constant (PRESS_FOO -> FOO, WP_FOO -> WP_FOO) with boolean/int coercion. Explicit define()s always win.
Examples:
| Environment Variable | Constant | Type |
|---|---|---|
PRESS_GOOGLE_KEY=abc123 |
define('GOOGLE_KEY', 'abc123') |
string |
PRESS_ENABLE_FEATURE=true |
define('ENABLE_FEATURE', true) |
boolean |
PRESS_MAX_ITEMS=50 |
define('MAX_ITEMS', 50) |
integer |
WP_AI_SUPPORT=1 |
define('WP_AI_SUPPORT', true) |
boolean |
WP_REDIS_HOST=valkey |
define('WP_REDIS_HOST', 'valkey') |
string |
services:
presshost:
image: ghcr.io/butialabs/presshost:latest
environment:
# ...
SSL_CERT_PATH: /site/ssl/live/your-domain.com/fullchain.pem
SSL_PRIVATE_PATH: /site/ssl/live/your-domain.com/privkey.pem
SSL_TRUSTED_CERT_PATH: /site/ssl/live/your-domain.com/chain.pem
NGINX_SSL_STAPLING: "on"
NGINX_SSL_STAPLING_VERIFY: "on"
volumes:
# ...
- /etc/certbot:/site/ssl:roNote: The
SSL_TRUSTED_CERT_PATHvariable should point to the intermediate certificate chain (chain.pem) for OCSP stapling to work correctly. Without this, you may see warnings like "ssl_stapling ignored, no OCSP responder URL in the certificate".
Note: Nginx automatically reloads daily at 00:00 (container timezone) to pick up renewed certificates. This ensures seamless certificate rotation without manual intervention.
Nginx caches the rendered HTML of anonymous (non-logged-in) requests and serves it directly.
environment:
NGINX_CACHE: "true"
NGINX_CACHE_MAX_SIZE: "1g" # total disk space for the cache
NGINX_CACHE_INACTIVE: "60m" # evict pages not hit in this windowCache is automatically bypassed for logged-in users, WooCommerce cart/checkout, wp-admin, POST requests, and WordPress preview mode. For on-demand purge from the WordPress admin, install the NGINX Helper plugin and point it at the FastCGI cache.
PressHost can drive CDN caching straight from the origin, no WordPress plugin required. Three independent features:
environment:
NGINX_CDN_CACHE: "true"Anonymous HTML responses get:
Cache-Control: public, max-age=60 (browser TTL)
CDN-Cache-Control: public, max-age=3600, stale-while-revalidate=60, stale-if-error=86400
Logged-in users, wp-admin, cart/checkout, POST requests and authenticated REST calls (Authorization header/X-WP-Nonce) get no-cache, no-store and CDN-Cache-Control: no-store. Error responses (4xx/5xx) never receive cache headers, so the CDN never caches error pages.
On Cloudflare, create one Cache Rule: "Eligible for cache" and Respect origin cache control. Freshness is handled by TTL (stale-while-revalidate softens updates), no purge plugin needed.
| Variable | Default | Description |
|---|---|---|
NGINX_CDN_CACHE |
false |
Emit CDN-Cache-Control/Cache-Control for HTML |
NGINX_CDN_EDGE_TTL |
3600 |
CDN max-age (seconds) |
NGINX_CDN_BROWSER_TTL |
60 |
Browser max-age (seconds) |
NGINX_CDN_SWR |
60 |
stale-while-revalidate (seconds) |
NGINX_CDN_STALE_IF_ERROR |
86400 |
Serve stale content if origin errors (seconds) |
At boot, the init fetches the current IP ranges from cloudflare.com/ips-v4 and ips-v6 and configures set_real_ip_from automatically (a bundled snapshot is used if the fetch fails). Without this, rate limits, logs and the IP seen by WordPress would all be the CDN's, all visitors would share one rate-limit bucket.
| Variable | Default | Description |
|---|---|---|
NGINX_CLOUDFLARE_REAL_IP |
false |
Auto-configure Cloudflare IP ranges |
NGINX_REAL_IP_HEADER |
X-Forwarded-For |
Header carrying the real IP (CF-Connecting-IP also works) |
NGINX_REAL_IP_FROM |
`` | Extra space-separated CIDRs (other CDNs/proxies) |
environment:
NGINX_ORIGIN_AUTH_SECRET: "a-long-random-string"When set, requests from public networks must carry the header X-Origin-Auth: <secret> or they get 403, preventing attackers from bypassing the CDN/WAF and hitting the origin directly. Configure the CDN to send the header (Cloudflare: Transform Rule -> Modify Request Header). Private networks (RFC1918, localhost) are always allowed so healthchecks keep working.
Stores WordPress object-cache data (database query results, transients) in Valkey so they survive across requests and processes.
environment:
WP_CACHE: "true"
VALKEY_ENABLED: "true"With VALKEY_ENABLED=true, the init auto-wires WP_REDIS_HOST=127.0.0.1 and WP_REDIS_PORT (unless you set them yourself). Then install and activate the Redis Object Cache plugin inside WordPress the plugin is still required to create the object-cache.php drop-in.
| Variable | Default | Description |
|---|---|---|
VALKEY_ENABLED |
false |
Run a local Valkey server inside the container |
VALKEY_BIND |
127.0.0.1 |
Bind address (localhost-only by default) |
VALKEY_PORT |
6379 |
Port |
VALKEY_MAXMEMORY |
128mb |
Memory limit (counted in the PHP-FPM auto-sizing reserve) |
VALKEY_MAXMEMORY_POLICY |
allkeys-lru |
Eviction policy |
VALKEY_SAVE |
`` (empty) | RDB save points, e.g. "900 1 300 10". Empty = no persistence (cache-only) |
VALKEY_PASSWORD |
`` | requirepass (also wired to WP_REDIS_PASSWORD) |
Never set
VALKEY_BINDto a non-localhost address withoutVALKEY_PASSWORD!
services:
presshost:
environment:
WP_CACHE: "true"
WP_REDIS_HOST: valkey
WP_REDIS_PORT: 6379
valkey:
image: valkey/valkey:8-alpine
container_name: valkey
restart: unless-stopped
command: valkey-server --maxmemory 256mb --maxmemory-policy allkeys-lru --save ""
networks:
- presshostPHP 8.4 includes a JIT compiler that can improve throughput on CPU-bound workloads (WooCommerce, page builders, image processing). It is disabled by default because a small number of plugins with legacy code may behave incorrectly with JIT enabled.
environment:
PHP_OPCACHE_JIT: "tracing" # recommended mode for WordPress
PHP_OPCACHE_JIT_BUFFER_SIZE: "128M"The container ships with the presshost CLI. Launch it with:
docker exec -it presshost presshostWhen no WordPress/ClassicPress is installed yet, the menu lets you:
- Install WordPress: downloads
wordpress.org/latest.zip(or a specific version), unpacks it to/site/press, generateswp-config.phpandwp-secrets.php(locally generated salts, no external API), runswp core install, and resets ownership of the install directory towww-data:www-data. - Install ClassicPress: same flow, pulling from the official ClassicPress release archive.
When a site is already installed, the menu offers:
- View installation info: shows detected type (WordPress/ClassicPress), version, site URL, title and the configured directories (
/site/press,/site/uploads,/site/cache). - Exit.
You can also invoke the installer non-interactively:
# Same flows scripted via env vars (no TTY required)
docker exec -e PRESSHOST_DEFAULT_ACTION=wordpress \
-e INSTALL_URL=https://example.com \
-e [email protected] \
-it presshost presshostpresshost fix-perms resets ownership to www-data:www-data and applies safe permissions (755 for directories, 644 for files, 640 for wp-config.php and wp-secrets.php) on /site/{press,uploads,cache,logs}. It must be invoked with the root account inside the container:
docker exec -u 0 presshost presshost fix-permssafe-chown <path> [path...] fixes ownership to APP_USER:APP_GROUP efficiently: healthy trees are an instant no-op (top-level probe), and dirty trees are walked chowning only wrong-owned entries in batches.
It runs automatically at startup when FIX_OWNERSHIP=true, in the background (APP_PATH, CACHE_PATH, LOGS_PATH by default; add UPLOADS_PATH with FIX_UPLOADS_OWNERSHIP=true), and is also used by presshost fix-perms (which always covers all four paths, run synchronously on demand). Manual use:
docker exec presshost safe-chown /site/press /site/uploadsMade with ❤️ by Butiá Labs