Skip to content
butialabsPublic

About

Docker image for WordPress and ClassicPress hosting with NGINX and PHP 8.4.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

150 Commits

Folders and files

Repository files navigation

PressHost 🚀

Docker image for WordPress and ClassicPress hosting with NGINX and PHP 8.4.

Docker Image License PHP Version NGINX s6-overlay

Features

  • ⚡ 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 ./presshost and perform the guided installation.

🚀

PressHost is already trusted by high-traffic websites, including:

Together, these sites handle more than 20 million pageviews per month, proving PressHost's reliability and performance at scale.

Quick Start

wget https://raw.githubusercontent.com/butialabs/presshost/main/compose.yml
nano compose.yml
docker compose up -d

Required variables

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_SITEURL and WP_HOME fall back to SITEURL when not set. server_name is also derived from the host portion of SITEURL.

Volumes

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

Installation

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.

Breaking changes on v3+

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)

All Environments

Database

Variable Default Description
DB_CHARSET utf8mb4 Database character set
DB_COLLATE utf8mb4_unicode_ci Database collation

Press

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

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

SMTP

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

Others

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)

Cron

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)

Building a derived image

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).

PHP

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

NGINX

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)

SSL

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.

NGINX Cache

  • 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-Cache header will be present in responses with values like HIT, MISS, or BYPASS

Cache Purging

The cache can be purged using the NGINX Cache Purge module. We recommend using the NGINX Helper

Custom NGINX Configuration

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;
}

Logging

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)

Installation

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

Anti-flood / DDoS hardening

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

Custom Constants and environment forwarding

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

Using signed SSL

Via Certbot / Let's Encrypt

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:ro

Note: The SSL_TRUSTED_CERT_PATH variable 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.

Performance Tuning

FastCGI Page Cache

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 window

Cache 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.

CDN integration (Cloudflare & others, plugin-free)

PressHost can drive CDN caching straight from the origin, no WordPress plugin required. Three independent features:

1. Cache headers for HTML (NGINX_CDN_CACHE=true)

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)

2. Real visitor IP behind Cloudflare (NGINX_CLOUDFLARE_REAL_IP=true)

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)

3. Origin pull protection (NGINX_ORIGIN_AUTH_SECRET)

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.

Redis/Valkey Object Cache

Stores WordPress object-cache data (database query results, transients) in Valkey so they survive across requests and processes.

Option A: Local Valkey (built into the image)

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_BIND to a non-localhost address without VALKEY_PASSWORD!

Option B: External Valkey (recommended for high traffic)

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:
      - presshost

OPcache JIT

PHP 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"

CLI and Tips:

Installer

The container ships with the presshost CLI. Launch it with:

docker exec -it presshost presshost

When no WordPress/ClassicPress is installed yet, the menu lets you:

  1. Install WordPress: downloads wordpress.org/latest.zip (or a specific version), unpacks it to /site/press, generates wp-config.php and wp-secrets.php (locally generated salts, no external API), runs wp core install, and resets ownership of the install directory to www-data:www-data.
  2. Install ClassicPress: same flow, pulling from the official ClassicPress release archive.

When a site is already installed, the menu offers:

  1. View installation info: shows detected type (WordPress/ClassicPress), version, site URL, title and the configured directories (/site/press, /site/uploads, /site/cache).
  2. 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 presshost

Fix permissions

presshost 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-perms

safe-chown

safe-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/uploads

Made with ❤️ by Butiá Labs

About

Docker image for WordPress and ClassicPress hosting with NGINX and PHP 8.4.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages