Skip to content

Environment Variables Reference

Environment Variables Reference

Complete reference for all environment variables supported by Cbox PHP Base Images powered by Cbox Init.

Quick Start Variables

These are the most commonly used variables. Just set what you need!

Variable Default Description
PUID (container default) User ID for application files — fixes permission issues
PGID (container default) Group ID for application files
LARAVEL_SCHEDULER false Enable Laravel scheduler
LARAVEL_HORIZON false Enable Laravel Horizon
LARAVEL_REVERB false Enable Laravel Reverb WebSockets
LARAVEL_QUEUE false Enable queue workers
PHP_MEMORY_LIMIT 256M PHP memory limit
PHP_MAX_EXECUTION_TIME 30 Max script execution time

User/Group Mapping (PUID/PGID)

The most common Docker problem: files created inside the container are owned by www-data (UID 33), but your host user has a different UID (typically 1000). This causes permission denied errors on bind mounts, and files created in the container can't be edited on the host.

PUID/PGID solves this by remapping the container's www-data user to match your host user.

Variable Default Description
PUID (container default) Set the UID that www-data runs as
PGID (container default) Set the GID that www-data runs as
APP_USER www-data Application user name
APP_GROUP www-data Application group name

Find your host UID/GID

id -u    # Your UID (typically 1000)
id -g    # Your GID (typically 1000)

Basic usage

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    volumes:
      - ./:/var/www/html
    environment:
      - PUID=1000
      - PGID=1000

Common scenarios

Linux with bind mounts — the most common case. Without PUID/PGID, Laravel's storage/ and bootstrap/cache/ will be owned by UID 33, and your editor/IDE can't write to them:

environment:
  - PUID=1000
  - PGID=1000

NFS volumes — NFS preserves the original UID/GID. Match them to avoid permission issues:

environment:
  - PUID=1001
  - PGID=1001

CI/CD pipelines — GitHub Actions and GitLab CI run as UID 1001 or similar:

environment:
  - PUID=1001
  - PGID=1001

What happens when you set PUID/PGID

  1. The container's www-data user is remapped to the specified UID/GID
  2. Ownership of /var/www/html is updated to match
  3. Framework directories (storage/, bootstrap/cache/, var/) are chowned automatically
  4. PHP-FPM runs worker processes as the remapped user
  5. Files created by PHP have the correct ownership on your host

Startup strictness knobs

Variable Default Description
CBOX_PREFLIGHT_STRICT false Abort boot when a framework-writable path (Laravel storage/, Symfony var/, WordPress uploads) is not writable - default only warns, naming the path, owner and runtime UID
CBOX_INIT_SCRIPTS_STRICT false Abort boot when a /docker-entrypoint-init.d/ script fails - default warns and continues

Important notes

  • Requires root mode — PUID/PGID only works in root images (the default). Rootless images skip PUID/PGID mapping since the container already runs as www-data.
  • Docker Desktop (macOS/Windows) — Docker Desktop handles permission translation automatically via its VM layer, so PUID/PGID is usually not needed. It won't break anything if set, though.
  • Only set what you need — if you only need to change the UID, just set PUID. If you only need the GID, just set PGID.

Laravel Shorthand Variables

These user-friendly variables are automatically mapped to Cbox Init process controls by the entrypoint script.

Process Control

Variable Maps To Description
LARAVEL_SCHEDULER CBOX_INIT_PROCESS_SCHEDULER_ENABLED Enable php artisan schedule:work
LARAVEL_HORIZON CBOX_INIT_PROCESS_HORIZON_ENABLED Enable Laravel Horizon
LARAVEL_REVERB CBOX_INIT_PROCESS_REVERB_ENABLED Enable Laravel Reverb
LARAVEL_QUEUE CBOX_INIT_PROCESS_QUEUE_DEFAULT_ENABLED Enable default queue worker
LARAVEL_QUEUE_HIGH CBOX_INIT_PROCESS_QUEUE_HIGH_ENABLED Enable high priority queue
CBOX_QUEUE_AUTOSCALER CBOX_INIT_PROCESS_AUTOSCALER_ENABLED Enable queue autoscaler (cboxdk/laravel-queue-autoscale)

Queue Scaling

Variable Default Description
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE 2 Number of default queue worker instances
CBOX_INIT_PROCESS_QUEUE_HIGH_SCALE 1 Number of high priority queue worker instances

Laravel Startup Hooks

Variable Default Description
LARAVEL_OPTIMIZE_ENABLED false Run config:cache, route:cache, view:cache on startup
LARAVEL_MIGRATE_ENABLED false Run php artisan migrate --force on startup
LARAVEL_MIGRATE_ALLOW_FAILURE false Continue container startup if migrations fail
LARAVEL_MIGRATE_ISOLATED auto --isolated lock so replicas don't race migrations. auto = use when artisan supports it, and drop it if the cache lock itself can't bootstrap (first deploy without a cache table); true/false to force
LARAVEL_MIGRATE_RETRIES 5 Retries (3s apart) when migration fails on a not-yet-reachable database

PHP Configuration

Variable Default Description
PHP_MEMORY_LIMIT 256M Memory limit
PHP_MAX_EXECUTION_TIME 30 Max execution time
PHP_MAX_INPUT_TIME 60 Max input time
PHP_POST_MAX_SIZE 100M Max POST size
PHP_UPLOAD_MAX_FILESIZE 100M Max upload size
PHP_MAX_FILE_UPLOADS 20 Max simultaneous uploads
PHP_MAX_INPUT_VARS 1000 Max input variables
PHP_DATE_TIMEZONE UTC Default timezone
PHP_DISPLAY_ERRORS Off Display errors (use On for dev)
PHP_DISPLAY_STARTUP_ERRORS Off Display startup errors
PHP_ERROR_REPORTING E_ALL & ~E_DEPRECATED & ~E_STRICT Error reporting level
PHP_LOG_ERRORS On Log errors
PHP_ERROR_LOG /dev/stderr Error log destination
PHP_SESSION_COOKIE_SECURE (not set) Restrict session cookies to HTTPS (1 recommended for prod)
PHP_REALPATH_CACHE_TTL 600 Path cache TTL in seconds
PHP_OPCACHE_PRELOAD (unset) php-fpm and php-fpm-nginx only - path to an opcache.preload script, loaded once at startup. This is the only supported way to use the FFI extension the standard, chromium and dev tiers ship (slim has no FFI): FFI is compiled with ffi.enable=preload, so preloaded code may call into C while request code may not. A path that does not exist fails the container at boot with a named cause instead of PHP's opaque startup fatal. Not wired in php-cli: opcache.enable_cli is off by default there, so a preload would be built and thrown away once per process
PHP_OPCACHE_PRELOAD_USER www-data php-fpm and php-fpm-nginx only - user the preload script runs as. PHP refuses to preload as root without it, and says so in terms of the ini rather than of the container. Only written for root images; rootless images already run as a non-root user and PHP ignores it there
PHP_OPEN_BASEDIR (empty - no restriction) open_basedir for the FPM pool. Empty since 1.6: the restriction disables PHP's realpath cache, measured at -39% throughput on the Laravel fixture (382 vs 625 rps). Set it for LFI defense-in-depth; include the kernel-statistics paths from the curated list below if you use cboxdk/system-metrics
PHP_OPENTELEMETRY false Load the OpenTelemetry auto-instrumentation extension. Off by default since 1.6: the extension enables the Zend observer API, which taxes every PHP function call even with no OTel SDK installed - measured at -18.5% throughput on a Laravel app (0% on tight-loop code). Enable it when you actually ship OTel traces. If what you want is Laravel telemetry without taxing every function call, cboxdk/laravel-telemetry does it in userland at the framework's own hooks - measured impact: none
PHP_TELEMETRY_NATIVE false Load cboxdk/telemetry-native (cbox_telemetry) - CPU profiler, native connect/cURL timing, runtime counters and a signal-safe crash recorder, the native half of cboxdk/laravel-telemetry. Off by default because it is pre-1.0 and because its crash recorder installs fatal-signal handlers - not a performance gate: upstream's FPM measurements put loading it inside the harness's noise floor. Shipped in every tier from slim up, on PHP 8.3+ only
PHP_TELEMETRY_NATIVE_AUTO false Open a telemetry unit of work at RINIT, so a profile covers framework boot instead of starting in middleware (cbox_telemetry.auto=1). Needs PHP_TELEMETRY_NATIVE=true. FPM only - in a queue worker or Octane server RINIT fires once per process, so an automatic unit would span hours and mean nothing; there the consumer calls begin()/finish() per job

Performance vs. security: should you enable open_basedir?

The default is off, and that is a measured trade, not an oversight: setting open_basedir disables PHP's realpath cache entirely, which costs ~39% throughput on a real Laravel application (382 → 666 req/s at 2 CPUs on our benchmark fixture - the cache saves repeated filesystem lookups across the hundreds of files a framework request touches). On a single-file endpoint the cost hides at ~3%, which is exactly why this class of regression goes unnoticed.

What enabling it buys: a defense-in-depth line against local file inclusion in your own application code - with the restriction on, a compromised include/file_get_contents cannot read paths like /proc/1/environ (which holds every secret in the container's environment). The container boundary does NOT protect the app from itself; open_basedir does.

Enable it when the application executes less-trusted code (plugins, user templates), serves user uploads near executable paths, or compliance requires it - and accept the realpath cost. Leave it off when raw framework throughput matters and your LFI surface is handled at the application layer.

Copy-paste re-enable, including the kernel-statistics paths that keep cboxdk/system-metrics (and laravel-telemetry) working under the restriction:

environment:
  PHP_OPEN_BASEDIR: "/var/www/html:/tmp:/var/tmp:/proc/stat:/proc/loadavg:/proc/meminfo:/proc/uptime:/proc/cpuinfo:/proc/diskstats:/proc/mounts:/proc/net/:/proc/self/cgroup:/proc/1/cgroup:/sys/fs/cgroup:/sys/class/dmi/id/:/etc/os-release:/etc/lsb-release:/etc/debian_version:/etc/redhat-release:/etc/system-release"

/proc/1/environ and /proc/*/cmdline are deliberately absent from that list - they are the secrets the restriction exists to protect. Verified behavior: with the list applied, file_get_contents('/proc/1/environ') fails while /proc/meminfo (metrics) succeeds.

One definition, and why that took three attempts

open_basedir is set in exactly one place: the entrypoint writes zz-env-overrides.conf from PHP_OPEN_BASEDIR, which the image always sets. The static pool defines it nowhere.

It has to be that way. PHP-FPM takes the FIRST definition of a php_admin_value, not the last — the opposite of php.ini's conf.d rule — and pool files load alphabetically. So while fpm-pool.conf (installed as zz-custom.conf) carried a narrow default, no override could ever win, and the comment beside it claiming the variable "loads after this pool and wins" was simply false. Setting the variable did nothing, on both the php-fpm and php-fpm-nginx images.

Empty is a value. With PHP_OPEN_BASEDIR="" no directive is written at all and the pool runs unrestricted. That is how the dev tier turns it off: a package sandbox reads fixtures, writes caches and dumps profiler traces at paths nobody can enumerate in advance, and a restriction widened once per surprise is one that ends up at /. No other tier does this, and the dev tier is never a base for a deployed application.

Verify it in a request rather than in a file — the CLI has no open_basedir at all, so php -i will tell you nothing:

docker exec -e SCRIPT_FILENAME=/var/www/html/probe.php -e REQUEST_METHOD=GET \
    <container> cgi-fcgi -bind -connect 127.0.0.1:9000

What the default lets through, and what it does not

The default is the application's own directories plus the read-only kernel statistics cboxdk/system-metrics reads — /proc/stat, /proc/loadavg, /proc/meminfo, /proc/uptime, /proc/cpuinfo, /proc/diskstats, /proc/mounts, /proc/net/, /proc/self/cgroup, /sys/fs/cgroup and /sys/class/dmi/id/.

Without them cboxdk/laravel-telemetry collects nothing from a web request and says so nowhere: the collector degrades to empty and reports a healthy application with no metrics.

/proc/1/environ is deliberately absent, and it is why this is a list rather than plain /proc: PID 1's environment is every secret the container holds, and a file-read primitive in an application that could reach it would have the database password. system-metrics reads it as one of several container-detection probes and degrades cleanly without it. /proc/self/environ and /proc/*/cmdline are absent for the same reason.

OPcache

Variable Default Description
PHP_OPCACHE_ENABLE 1 Enable OPcache
PHP_OPCACHE_MEMORY_CONSUMPTION 256 OPcache memory (MB)
PHP_OPCACHE_INTERNED_STRINGS_BUFFER 16 Interned strings buffer (MB)
PHP_OPCACHE_MAX_ACCELERATED_FILES 20000 Max cached files
PHP_OPCACHE_REVALIDATE_FREQ 0 Revalidation frequency
PHP_OPCACHE_VALIDATE_TIMESTAMPS 0 Validate timestamps (1 for dev)
PHP_OPCACHE_JIT tracing JIT mode: tracing, function, off
PHP_OPCACHE_JIT_BUFFER_SIZE 128M JIT buffer size

PHP-FPM worker auto-tuning (boot seed)

By default the worker pool is auto-tuned by cbox-init from the container's cgroup memory and CPU limits — it reads memory.max, reserves headroom for nginx/opcache/system, and derives pm.max_children (and the other PM values). A 512 MB container yields ~4 workers, 768 MB ~6, etc. This replaces guessing a static pm.max_children and prevents OOM-kills.

Variable Default Description
PHP_FPM_AUTOTUNE_PROFILE medium Sizing profile: dev, light, medium, heavy, bursty. Set to empty ("") to disable auto-tuning and use the static values below.
PHP_FPM_AUTOTUNE_STRICT 0 1 = refuse to boot when the memory limit cannot fit the profile, instead of clamping. Useful as a deploy-time misconfiguration check.

Small containers: when the memory limit cannot fit the profile, cbox-init (3.1.2+) clamps the worker count with a warning naming the smallest limit that would run the profile, and boots anyway — the runtime tuner (below) then refines the number from live measurements. Set PHP_FPM_AUTOTUNE_STRICT=1 if you'd rather fail hard at deploy time; on very small containers, consider a lighter profile plus CBOX_FPM_TUNE=true.

To size manually, set PHP_FPM_MAX_CHILDREN (this pins the count and turns auto-tuning off), or disable with PHP_FPM_AUTOTUNE_PROFILE="" and set the pool values explicitly:

Variable Fallback Description
PHP_FPM_PM dynamic Process manager: dynamic, static, ondemand
PHP_FPM_MAX_CHILDREN (auto) Max concurrent child processes. Setting this pins sizing and disables auto-tuning.
PHP_FPM_START_SERVERS 2 Initial child count (dynamic mode)
PHP_FPM_MIN_SPARE 1 Min idle processes (dynamic mode)
PHP_FPM_MAX_SPARE 6 Max idle processes (dynamic mode)
PHP_FPM_MAX_REQUESTS 500 Requests per child before recycling (0 = unlimited)
PHP_FPM_REQUEST_TERMINATE_TIMEOUT 60s Max request execution time before kill

Runtime PHP-FPM tuning (fpm-tune, cbox-init 3.1+)

The boot profile above is a seed — it sizes the pool once, before traffic. cbox-init 3.1 embeds fpm-tune as a runtime loop: it measures live per-worker memory (PSS, which does not double-count shared OPcache), and when the right size has moved it rewrites a pool drop-in (zz-fpm-tune.conf) and reloads php-fpm gracefully with SIGUSR2 — never a restart, the master PID stays. Every change is validated against a throwaway copy, written atomically, and rolled back if the master does not come back.

Variable Default Description
CBOX_FPM_TUNE false Enable the runtime tuner (true/false)
CBOX_INIT_FPM_TUNE_ENABLED false Same switch, long spelling
CBOX_INIT_FPM_TUNE_MODE apply apply = write drop-ins + reload; advisory = observe and recommend only
CBOX_INIT_FPM_TUNE_INTERVAL 30s Measurement interval
CBOX_INIT_FPM_TUNE_METRICS_ADDR (empty) e.g. :9110 to expose fpm_tune_* Prometheus metrics (worker PSS, budget, recommended vs configured workers)
# Example: runtime tuning on, with metrics
environment:
  CBOX_FPM_TUNE: "true"
  CBOX_INIT_FPM_TUNE_METRICS_ADDR: ":9110"

These map onto cbox-init's native CBOX_INIT_GLOBAL_FPM_TUNE_* overrides, which also work directly and win over the fpm_tune block in cbox-init.yaml. Works in both root and rootless variants. Do not run a separate standalone fpm-tune daemon against the same pools — the embedded loop holds a lock on its state file.

Reloads are connection-lossless. php-fpm's graceful SIGUSR2 reload keeps the listening socket open while workers respawn, so nginx needs no retry configuration: measured on these images, 4,900+ requests (sequential and saturated-concurrent) through 20 forced reloads produced zero non-200 responses.

PHP-FPM process manager & worker sizing

The full pm surface is env-driven. Defaults preserve current behavior; cbox-init's boot autotune exports the sizing values unless you set them explicitly (explicit env always wins).

Variable Default Applies to Description
PHP_FPM_PM dynamic all Process manager: dynamic (warm floor), ondemand (spawn per burst, idle workers die - bursty/low-traffic pods), static (fixed pool)
PHP_FPM_MAX_CHILDREN autotuned all Worker ceiling (running count for static)
PHP_FPM_MAX_REQUESTS autotuned all Recycle a worker after N requests
PHP_FPM_START_SERVERS autotuned dynamic Workers at boot
PHP_FPM_MIN_SPARE / PHP_FPM_MAX_SPARE autotuned dynamic Idle-worker floor/ceiling
PHP_FPM_PROCESS_IDLE_TIMEOUT 10s ondemand Idle worker lifetime
PHP_FPM_MAX_SPAWN_RATE 32 dynamic, ondemand Max workers spawned per second under burst
PHP_FPM_LISTEN_BACKLOG 511 all Accept backlog (kernel somaxconn caps it)
PHP_FPM_REQUEST_TERMINATE_TIMEOUT 60s all Hard kill for runaway requests
PHP_FPM_REQUEST_SLOWLOG_TIMEOUT 5s all Stack-trace slow requests to stderr
PHP_FPM_MEMORY_LIMIT 256M all Per-worker memory_limit (pool admin value; CLI keeps php.ini's)

Mode-specific directives are written as a drop-in (zz-pm-mode.conf) so no mode ever boots with another mode's directives. With CBOX_FPM_TUNE=true the runtime tuner respects the mode: for ondemand/static it resizes only pm.max_children, and its advice output will tell you when the chosen mode fights the observed workload (e.g. an ondemand pool paying cold-start latency on every burst).

PHP-FPM listen transport (TCP vs unix socket)

Both transports are first-class - pick per deployment:

Variable Default Description
PHP_FPM_LISTEN unix (php-fpm-nginx) / tcp (standalone php-fpm) unix = the nginx->FPM hop over a unix socket (+21% transport throughput, cleanest p99.9 - the multi-service default since 1.6.1); tcp = classic 127.0.0.1:9000, the standalone default and the right choice when FPM is scraped/proxied from outside the container
PHP_FPM_SOCKET_PATH /run/php/php-fpm.sock Socket location in unix mode

When to use which:

  • tcp (default) - with FastCGI keepalive on (the 1.6 default) this is also the FASTEST configuration measured: 15.2k hello rps at 2 CPUs vs the socket's 13.3k. And it is the only transport that works from OUTSIDE the container (sidecar exporters, separate nginx, cgi-fcgi debugging).
  • unix - the keepalive-free alternative for single-container setups: +12-24% over PLAIN tcp (without keepalive), and the right choice where connection pooling is undesirable. nginx follows automatically (NGINX_FASTCGI_PASS derived), the health probe switches with it, and fpm-exporter/fpm-tune autodiscover the socket. Keepalive is deliberately NOT defaulted on the socket - measured counterproductive there (12.2k).
environment:
  PHP_FPM_LISTEN: "unix"

Works in root and rootless variants (rootless falls back to /tmp/php-fpm.sock with a warning if the socket directory is not writable).

PHP-FPM Metrics Exporter (horizontal-scaling signals)

cboxdk/fpm-exporter ships in every image, supervised as a disabled-by-default process. It autodiscovers PHP-FPM pools over FastCGI (no nginx dependency) and exposes phpfpm_* metrics — plus Laravel metrics (queue sizes, app info) when configured.

Variable Default Description
CBOX_FPM_EXPORTER false Enable the exporter (true/false)
CBOX_MONITOR_LISTEN_ADDR :9114 Exporter listen address (read natively by fpm-exporter)

These are the metrics that drive horizontal scaling. Where fpm_tune_* (port 9110) answers the vertical question — how many workers fit in this container — the exporter answers the horizontal one: is this container saturated?

phpfpm_listen_queue          # requests waiting for a worker — THE scale-out signal
phpfpm_active_processes      # workers busy right now
phpfpm_idle_processes        # headroom
phpfpm_max_children_reached  # pool hit its ceiling (counter)
phpfpm_accepted_connections  # throughput

A pool that keeps listen_queue > 0 while fpm_tune reports no memory budget left is the unambiguous "add replicas" signal (HPA/KEDA on phpfpm_listen_queue, or fpm_tune_capacity_exhausted as the guard).

# Enable and scrape
environment:
  CBOX_FPM_EXPORTER: "true"

One scrape, one story (cbox-init 3.2+): enabling the exporter also folds its series into cbox-init's main metrics endpoint on :9090 via metrics_federate, and the embedded fpm-tune's fpm_tune_* series ride on that endpoint natively whenever the tuner runs. Prometheus scrapes ONE port and sees the whole story:

:9090/metrics
├── cbox_init_*   # is everything running?          (supervision)
├── fpm_tune_*    # does the workload fit here?     (vertical)
├── phpfpm_*      # is this container saturated?    (horizontal — listen_queue)
└── cbox_init_federate_up{name="fpm-exporter"}  # 1 = source healthy

An exporter that is down degrades to cbox_init_federate_up 0 — never a failed scrape. The standalone listeners remain for direct access: :9114 fpm-exporter, :9110 fpm-tune (opt-in via CBOX_INIT_FPM_TUNE_METRICS_ADDR). All loopback-scoped inside the container by default — expose deliberately.

Live config reload

Variable Default Description
CBOX_INIT_WATCH true Watch the generated config and reload changed processes without a container restart.

Beyond watch mode, cbox-init exposes live process control over its Unix socket — no restart needed:

cbox-init reload-config          # re-read config from disk and apply
cbox-init scale queue-default 8  # change a worker's replica count
cbox-init restart horizon        # restart one supervised process
cbox-init start|stop <process>   # start/stop a daemon on the fly

This is how you run and manage standalone daemons (queue workers, scheduler, Horizon, Reverb, or your own) — via cbox-init, with or without nginx/php-fpm (the php-cli tier ships the same supervisor for daemon-only containers).


Nginx Configuration

Server Settings

Variable Default Description
NGINX_HTTP_PORT 80 HTTP port
NGINX_HTTPS_PORT 443 HTTPS port
NGINX_WEBROOT /var/www/html/public Document root
NGINX_INDEX index.php index.html Index files
NGINX_SERVER_TOKENS off Hide Nginx version
NGINX_WORKER_PROCESSES container CPU limit Worker count. Sized from the cgroup quota at boot (nginx's own auto reads the HOST's cores - 64 workers in a 2-CPU pod). Set a number, or auto for nginx's behavior

Client Settings

Variable Default Description
NGINX_CLIENT_MAX_BODY_SIZE 100M Max request body
NGINX_CLIENT_BODY_TIMEOUT 60s Body read timeout
NGINX_CLIENT_HEADER_TIMEOUT 60s Header read timeout

Security Headers

All security headers are fully configurable via environment variables. Set to empty string to disable.

Variable Default Description
NGINX_HEADER_X_FRAME_OPTIONS SAMEORIGIN Clickjacking protection
NGINX_HEADER_X_CONTENT_TYPE_OPTIONS nosniff MIME sniffing protection
NGINX_HEADER_X_XSS_PROTECTION 1; mode=block XSS filter
NGINX_HEADER_CSP (see below) Content-Security-Policy
NGINX_HEADER_REFERRER_POLICY strict-origin-when-cross-origin Referrer information
NGINX_HEADER_COOP (disabled) Cross-Origin-Opener-Policy (opt-in)
NGINX_HEADER_COEP (disabled) Cross-Origin-Embedder-Policy (opt-in)
NGINX_HEADER_CORP (disabled) Cross-Origin-Resource-Policy (opt-in)
NGINX_HEADER_PERMISSIONS_POLICY (see below) Browser feature permissions

Default CSP:

default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self'

Default Permissions-Policy:

accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()

Cross-Origin Isolation Headers (COOP/COEP/CORP):

These headers are disabled by default because they break most applications that use:

  • External APIs (payment gateways, analytics, social login)
  • CDN resources (fonts, scripts, images)
  • Third-party embeds (YouTube, maps, widgets)

Enable for maximum security (advanced use cases only):

environment:
  - NGINX_HEADER_COOP=same-origin
  - NGINX_HEADER_COEP=require-corp
  - NGINX_HEADER_CORP=same-origin

Disable a header (set to empty):

environment:
  - NGINX_HEADER_CSP=       # Disable Content-Security-Policy

See Security Hardening for customization examples.

Gzip Compression

Variable Default Description
NGINX_GZIP on Enable gzip (on/off)
NGINX_GZIP_VARY on Add Vary: Accept-Encoding
NGINX_GZIP_PROXIED any Compress proxied requests
NGINX_GZIP_COMP_LEVEL 6 Compression level (1-9)
NGINX_GZIP_MIN_LENGTH 1000 Min size to compress (bytes)
NGINX_GZIP_TYPES (see below) MIME types to compress
NGINX_GZIP_STATIC on Serve pre-compressed .gz files from disk (on/off)

Default gzip types:

text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/x-javascript image/svg+xml

Disable gzip:

environment:
  - NGINX_GZIP=off

Pre-compressed assets (gzip_static, enabled by default): when your build pipeline emits .gz files next to the originals (Vite, Webpack, Rollup, and esbuild all have plugins for this), nginx serves app.css.gz directly from disk for a request to app.css — no per-request compression CPU at all. When no .gz file exists, nginx falls back to dynamic gzip transparently.

Brotli Compression

Variable Default Description
NGINX_BROTLI on Enable brotli (on/off)
NGINX_BROTLI_COMP_LEVEL 6 Compression level (0-11)
NGINX_BROTLI_TYPES (mirrors NGINX_GZIP_TYPES) MIME types to compress
NGINX_BROTLI_STATIC on Serve pre-compressed .br files from disk (on/off)

Brotli typically compresses 15-20% smaller than gzip at the same CPU budget and is supported by every modern browser. The ngx_brotli module is compiled into all php-fpm-nginx images (statically linked — no extra runtime dependencies). Content negotiation is automatic: clients sending Accept-Encoding: br get brotli, everyone else gets gzip.

Pre-compressed .br assets work exactly like .gz ones: emit them from your build pipeline and nginx serves them straight from disk, preferring .br over .gz when the client supports both.

Open File Cache

Variable Default Description
NGINX_OPEN_FILE_CACHE max=10000 inactive=20s Cache config (off to disable)
NGINX_OPEN_FILE_CACHE_VALID 30s Cache validation interval
NGINX_OPEN_FILE_CACHE_MIN_USES 2 Min uses before caching
NGINX_OPEN_FILE_CACHE_ERRORS on Cache file errors

Disable file cache:

environment:
  - NGINX_OPEN_FILE_CACHE=off

FastCGI Settings

Variable Default Description
NGINX_FASTCGI_PASS 127.0.0.1:9000 PHP-FPM address
NGINX_FASTCGI_BUFFERS 8 8k FastCGI buffers
NGINX_FASTCGI_BUFFER_SIZE 8k Buffer size
NGINX_FASTCGI_BUSY_BUFFERS_SIZE 16k Busy buffers size
NGINX_FASTCGI_CONNECT_TIMEOUT 60s Connect timeout
NGINX_FASTCGI_SEND_TIMEOUT 60s Send timeout
NGINX_FASTCGI_READ_TIMEOUT 60s Read timeout
NGINX_FASTCGI_KEEP_CONN off Pool nginx→FPM connections. A MICRO-REQUEST optimization: measured +25-29% on sub-ms endpoints (tcp), but -12% on a real Laravel app and worse over the unix socket - enable for high-rps micro-APIs and CPU-throttled runtimes (Cloud Run), not for framework apps
NGINX_FASTCGI_KEEPALIVE 32 Pooled-connection count. Do NOT shrink below ~4x nginx workers: a small pool interacts with pm.max_requests worker recycling into second-long tail stalls (measured: 8 conns -> CO-corrected p99 534ms; 32 conns -> 8.4ms)

Logging

Variable Default Description
NGINX_ACCESS_LOG /var/log/nginx/access.log Access log path (off or false to disable)
NGINX_LOG_FORMAT combined Access log format: combined, combined_no_query, or json
NGINX_ERROR_LOG /var/log/nginx/error.log Error log path
NGINX_ERROR_LOG_LEVEL warn Error log level

Disable access logging (reduces disk I/O in high-traffic scenarios):

environment:
  - NGINX_ACCESS_LOG=false
  # or
  - NGINX_ACCESS_LOG=off

Access log formats (NGINX_LOG_FORMAT):

  • combined (default) — nginx's standard format, full request line including query string. Unchanged for backwards compatibility.
  • combined_no_query — same layout, but logs the path only. Query strings (magic-link tokens, OAuth code=... callbacks, search terms) never reach disk or downstream log shippers. The privacy/GDPR-friendly choice.
  • json — structured logging for Loki, Datadog, ELK, and friends. Path only (no query string), plus timing fields for latency analysis.

Example log lines per format for GET /index.php?token=SECRET:

# combined
172.18.0.1 - - [26/Aug/2026:09:09:07 +0000] "GET /index.php?token=SECRET HTTP/1.1" 200 1024 "-" "curl/8.7.1"

# combined_no_query
172.18.0.1 - - [26/Aug/2026:09:09:07 +0000] "GET /index.php HTTP/1.1" 200 1024 "-" "curl/8.7.1"

# json
{"time":"2026-08-26T09:09:07+00:00","remote_addr":"172.18.0.1","method":"GET","path":"/index.php","status":200,"body_bytes_sent":1024,"request_time":0.127,"upstream_response_time":"0.127","referer":"","user_agent":"curl/8.7.1","host":"example.com"}

Custom formats remain possible by volume-mounting your own server config.

Server Header

Variable Default Description
NGINX_SERVER_HEADER (unset) Replace the Server response header, or none to remove it entirely

The default Server: nginx (version hidden via server_tokens off) can be rebranded or stripped — something plain add_header cannot do:

environment:
  - NGINX_SERVER_HEADER=Acme API    # Server: Acme API
  # or
  - NGINX_SERVER_HEADER=none        # no Server header at all

This is powered by the headers-more module, which is loaded in all php-fpm-nginx images. Custom configs mounted into /etc/nginx/conf.d/ can therefore also use more_set_headers / more_clear_headers directly, e.g. to strip noisy upstream headers.

Static Files

Variable Default Description
NGINX_STATIC_EXPIRES 1y Static file cache duration
NGINX_STATIC_CACHE_CONTROL public, immutable Cache-Control header
NGINX_STATIC_ACCESS_LOG off Static file access logging
NGINX_TRY_FILES /index.php?$query_string try_files fallback

Reverse Proxy Configuration

Configure Cbox to run behind Cloudflare, HAProxy, Traefik, Nginx, Fastly, Tailscale, or other reverse proxies.

Variable Default Description
NGINX_TRUSTED_PROXIES (empty) Space-separated list of trusted proxy IPs/CIDRs
NGINX_REAL_IP_HEADER X-Forwarded-For Header containing real client IP
NGINX_REAL_IP_RECURSIVE on Recursive IP extraction from proxy chain

Common Proxy Configurations

# Docker/Kubernetes internal networks
NGINX_TRUSTED_PROXIES: "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"

# Cloudflare
NGINX_TRUSTED_PROXIES: "173.245.48.0/20 103.21.244.0/22 ..."
NGINX_REAL_IP_HEADER: "CF-Connecting-IP"

# Tailscale
NGINX_TRUSTED_PROXIES: "100.64.0.0/10"

# Traefik/HAProxy (Docker network)
NGINX_TRUSTED_PROXIES: "172.16.0.0/12"

Headers Forwarded to PHP

When proxies are configured, these headers are available in PHP:

PHP Variable Description
$_SERVER['REMOTE_ADDR'] Real client IP (after proxy extraction)
$_SERVER['HTTP_X_FORWARDED_FOR'] Full proxy chain
$_SERVER['HTTP_X_FORWARDED_PROTO'] Original protocol (http/https)
$_SERVER['HTTP_X_FORWARDED_HOST'] Original hostname
$_SERVER['HTTP_X_REAL_IP'] Real client IP

See Reverse Proxy & mTLS Guide for detailed setup.


SSL Configuration

Variable Default Description
SSL_MODE off SSL mode: off, on, full
SSL_CERTIFICATE_FILE /etc/ssl/certs/cbox-selfsigned.crt Certificate path
SSL_PRIVATE_KEY_FILE /etc/ssl/private/cbox-selfsigned.key Private key path
SSL_PROTOCOLS TLSv1.2 TLSv1.3 SSL protocols
SSL_CIPHERS HIGH:!aNULL:!MD5 SSL ciphers
SSL_HSTS_HEADER max-age=31536000; includeSubDomains HSTS header value

SSL Modes

  • off - HTTP only
  • on - HTTPS enabled (HTTP still available)
  • full - HTTPS with HTTP to HTTPS redirect

mTLS (Mutual TLS) Configuration

Enable client certificate authentication for zero-trust networks, service mesh, or API authentication.

Variable Default Description
MTLS_ENABLED false Enable mTLS client verification
MTLS_CLIENT_CA_FILE /etc/ssl/certs/client-ca.crt CA certificate for client verification
MTLS_VERIFY_CLIENT optional optional, on (required), or optional_no_ca
MTLS_VERIFY_DEPTH 2 Maximum certificate chain depth

mTLS Client Info in PHP

When mTLS is enabled, client certificate details are available:

PHP Variable Description
$_SERVER['SSL_CLIENT_VERIFY'] SUCCESS, FAILED, or NONE
$_SERVER['SSL_CLIENT_S_DN'] Client subject DN (e.g., /CN=service-name)
$_SERVER['SSL_CLIENT_I_DN'] Client issuer DN
$_SERVER['SSL_CLIENT_SERIAL'] Certificate serial number
$_SERVER['SSL_CLIENT_FINGERPRINT'] Certificate fingerprint

Example mTLS Setup

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    environment:
      SSL_MODE: "on"
      MTLS_ENABLED: "true"
      MTLS_VERIFY_CLIENT: "optional"
    volumes:
      - ./certs/client-ca.crt:/etc/ssl/certs/client-ca.crt:ro
      - ./certs/server.crt:/etc/ssl/certs/cbox-selfsigned.crt:ro
      - ./certs/server.key:/etc/ssl/private/cbox-selfsigned.key:ro

See Reverse Proxy & mTLS Guide for complete setup.



Laravel .env Decryption

Automatically decrypt .env.encrypted files at container startup.

Variable Default Description
LARAVEL_ENV_ENCRYPTION_KEY (empty) Decryption key (e.g., base64:xxx)
LARAVEL_ENV_ENCRYPTION_KEY_FILE (empty) Path to file containing decryption key
LARAVEL_ENV_FORCE_DECRYPT false Overwrite existing .env file

Using environment variable:

environment:
  - LARAVEL_ENV_ENCRYPTION_KEY=base64:your-encryption-key-here

Using Docker secrets:

environment:
  - LARAVEL_ENV_ENCRYPTION_KEY_FILE=/run/secrets/laravel_env_key
secrets:
  - laravel_env_key

Cbox Init Management API

The Management API provides runtime control over container processes. It is disabled by default for security.

Variable Default Description
CBOX_INIT_API_ENABLED false Enable the Management API
CBOX_INIT_API_HOST 127.0.0.1 API bind address. Loopback-only by default (cbox-init 3.0+); set 0.0.0.0 + CBOX_INIT_API_AUTH to reach it via a published port
CBOX_INIT_API_PORT 9180 Port the API listens on
CBOX_INIT_API_AUTH (empty) Bearer token for API authentication
services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    ports:
      - "9180:9180"
    environment:
      CBOX_INIT_API_ENABLED: "true"
      CBOX_INIT_API_AUTH: "my-secret"

See Cbox Init Integration for endpoints, CLI commands, and examples.


Cbox Init Global Config

Variable Default Description
CBOX_INIT_METRICS_ENABLED true Enable Prometheus metrics endpoint
CBOX_INIT_METRICS_PORT 9090 Metrics endpoint port
CBOX_INIT_LOG_LEVEL info Log level: debug, info, warn, error
CBOX_INIT_LOG_FORMAT json Log format: json, text
CBOX_INIT_SHUTDOWN_TIMEOUT 30 Seconds to wait for graceful process shutdown
CBOX_INIT_CONFIG /etc/cbox-init/cbox-init.yaml Path to cbox-init config file

Application Warmup Hooks (cbox-init 3.0+)

Run app-specific pre-flight work — Statamic stache:warm, Symfony cache:warmup, custom seeding — before the services start and health checks begin succeeding. Hooks are executed by cbox-init itself: supervised, with a timeout, and logged with structured fields (name, duration, exit code).

Define hooks entirely via environment variables, no YAML mount needed:

CBOX_INIT_HOOK_PRE_START_<N>_NAME           # Optional (defaults to pre-start-<N>)
CBOX_INIT_HOOK_PRE_START_<N>_COMMAND        # Required, comma-separated argv or JSON array
CBOX_INIT_HOOK_PRE_START_<N>_TIMEOUT        # Seconds
CBOX_INIT_HOOK_PRE_START_<N>_ALLOW_FAILURE  # true = log the failure, continue startup
# Example: Statamic stache warm + best-effort event cache
environment:
  CBOX_INIT_HOOK_PRE_START_0_NAME: "stache-warm"
  CBOX_INIT_HOOK_PRE_START_0_COMMAND: "php,please,stache:warm"
  CBOX_INIT_HOOK_PRE_START_0_TIMEOUT: "300"
  CBOX_INIT_HOOK_PRE_START_1_COMMAND: "php,artisan,event:cache"
  CBOX_INIT_HOOK_PRE_START_1_ALLOW_FAILURE: "true"

Notes:

  • A hook that fails (or times out) aborts container startup unless ALLOW_FAILURE is true — right for migrations, opt out for best-effort warmup (a cold cache is degraded, not down).
  • For a full shell line, wrap it: COMMAND: "/bin/sh,-c,php please stache:warm && php please static:warm".
  • Hooks needing the web server up (HTTP-based warming) belong in hooks.post-start in a mounted cbox-init.yaml instead.
  • Simpler alternative for scripts: drop a *.sh file into /docker-entrypoint-init.d/ — it runs before cbox-init starts, but without cbox-init's supervision, timeout, or structured logging.

Development (Dev Images Only)

Variable Default Description
XDEBUG_MODE off Xdebug mode: debug, develop, coverage, profile, or comma-separated
XDEBUG_CONFIG (empty) Xdebug config string (e.g., client_host=host.docker.internal client_port=9003)
PHP_IDE_CONFIG (empty) IDE server mapping (e.g., serverName=docker)
# Example: Enable step debugging
environment:
  XDEBUG_MODE: "debug"
  XDEBUG_CONFIG: "client_host=host.docker.internal client_port=9003"
  PHP_IDE_CONFIG: "serverName=docker"

Other Settings

Variable Default Description
WORKDIR /var/www/html Working directory
CBOX_INIT_CONFIG /etc/cbox-init/cbox-init.yaml Cbox Init config path
REVERB_PORT 8080 Reverb port for healthcheck (override if using non-default port)

Example Configurations

Development

environment:
  - PUID=1000
  - PGID=1000
  - PHP_DISPLAY_ERRORS=On
  - PHP_OPCACHE_VALIDATE_TIMESTAMPS=1

Production Laravel

environment:
  - PUID=1000
  - PGID=1000
  - LARAVEL_SCHEDULER=true
  - PHP_MEMORY_LIMIT=512M

High-Traffic API

environment:
  - PHP_MEMORY_LIMIT=1G
  - PHP_MAX_EXECUTION_TIME=120
  - NGINX_FASTCGI_READ_TIMEOUT=120s
  - LARAVEL_QUEUE=true

Laravel with Horizon

environment:
  - LARAVEL_HORIZON=true
  - LARAVEL_SCHEDULER=true

Laravel with Reverb (WebSockets)

environment:
  - LARAVEL_REVERB=true
ports:
  - "8000:80"
  - "8080:8080"