Production is a small, production-focused Docker image for running PHP applications with Nginx + PHP-FPM already configured.
Production 2.0.2 is a maintenance release focused on Laravel FastCGI reliability and predictable non-root file permissions. It keeps the Alpine 3.24 security refresh and SBOM/SLSA provenance publication introduced in 2.0.1.
It is designed to keep deployment simple: your application is prepared during the build/deployment stage, mounted or copied into the container, and Production provides the web runtime.
Production is available in two variants:
- Generic — for general PHP applications;
- Laravel — for Laravel applications, with Laravel-oriented PHP extensions and Nginx configuration.
Production 2.0 supports:
- PHP 8.3, 8.4, and 8.5;
linux/amd64andlinux/arm64;- a non-root runtime user;
- Docker health checks;
- stdout/stderr logging;
- graceful shutdown;
- read-only root filesystem deployments;
- web workloads, queues, schedulers, migrations, and other CLI commands.
Official Docker Hub repository:
joaopinto14/production
Images are published by the release workflow after the complete stable release validation succeeds.
- Quick start
- Which image should I use?
- Stable tags
- How it works
- Using the Generic image
- Using the Laravel image
- Queues, Scheduler, and Artisan
- Configuration
- Health check
- Logs
- Security and permissions
- PHP extensions
- What is not included
- Docker Compose
- Multi-architecture support
- Testing
- Migrating from 1.1.3 to 2.0.0
If your application has an index.php file in its root directory:
docker run --rm \
-p 8080:8080 \
-v "$PWD:/var/www/html:ro" \
joaopinto14/production:2.0.2-php8.5Open:
http://localhost:8080
The default document root is:
/var/www/html
For a Laravel project with vendor/ already installed:
Prepare Laravel's writable directories on the host once:
sudo chown -R 10001:10001 storage bootstrap/cachedocker run --rm \
-p 8080:8080 \
-v "$PWD:/var/www/html:ro" \
-v "$PWD/storage:/var/www/html/storage" \
-v "$PWD/bootstrap/cache:/var/www/html/bootstrap/cache" \
joaopinto14/production:2.0.2-laravel-php8.5Open:
http://localhost:8080
The Laravel variant automatically uses:
/var/www/html/public
as its document root.
Use the Generic variant for PHP applications that do not need Laravel-specific configuration or extensions.
joaopinto14/production:2.0.2-php8.3
joaopinto14/production:2.0.2-php8.4
joaopinto14/production:2.0.2-php8.5
Recommended example for PHP 8.5:
joaopinto14/production:2.0.2-php8.5
Use the Laravel variant for Laravel applications.
joaopinto14/production:2.0.2-laravel-php8.3
joaopinto14/production:2.0.2-laravel-php8.4
joaopinto14/production:2.0.2-laravel-php8.5
Recommended example for PHP 8.5:
joaopinto14/production:2.0.2-laravel-php8.5
The Laravel variant adds commonly required extensions for MySQL/MariaDB, PostgreSQL, SQLite, Redis, intl, bcmath, pcntl, and zip.
Production 2.0.2 publishes immutable version tags and convenient stable aliases.
| Purpose | Tag |
|---|---|
| Generic PHP 8.3 | joaopinto14/production:2.0.2-php8.3 |
| Generic PHP 8.4 | joaopinto14/production:2.0.2-php8.4 |
| Generic PHP 8.5 | joaopinto14/production:2.0.2-php8.5 |
| Laravel PHP 8.3 | joaopinto14/production:2.0.2-laravel-php8.3 |
| Laravel PHP 8.4 | joaopinto14/production:2.0.2-laravel-php8.4 |
| Laravel PHP 8.5 | joaopinto14/production:2.0.2-laravel-php8.5 |
Stable aliases:
joaopinto14/production:php8.3
joaopinto14/production:php8.4
joaopinto14/production:php8.5
joaopinto14/production:latest
joaopinto14/production:laravel-php8.3
joaopinto14/production:laravel-php8.4
joaopinto14/production:laravel-php8.5
joaopinto14/production:laravel
latest points to Generic PHP 8.5. laravel points to Laravel PHP 8.5.
Production uses a simple runtime architecture:
HTTP request :8080
│
▼
Nginx
│
▼
PHP-FPM
│
▼
PHP application
PID 1 is Production's own lightweight runtime manager:
production-runtime
├── Nginx
└── PHP-FPM
The runtime manager:
- starts Nginx and PHP-FPM;
- forwards signals correctly;
- performs graceful shutdown;
- stops the container if Nginx or PHP-FPM exits unexpectedly.
Production 2.0 does not use Supervisor, systemd, or Python.
When the container starts with its default command:
production-runtimeit starts:
Nginx + PHP-FPM
If you provide another command, Production runs that command directly without starting Nginx or PHP-FPM.
Example:
docker run --rm \
-v "$PWD:/var/www/html" \
joaopinto14/production:2.0.2-laravel-php8.5 \
php artisan aboutThis lets you use the same Laravel image for the web application, queue workers, scheduler, migrations, and other CLI tasks.
The Generic variant uses this document root by default:
DOCUMENT_ROOT=/var/www/html
A simple project may look like this:
my-project/
├── index.php
├── assets/
└── ...
Run it with:
docker run --rm \
-p 8080:8080 \
-v "$PWD:/var/www/html:ro" \
joaopinto14/production:2.0.2-php8.5If your application uses a public/ or web/ directory:
docker run --rm \
-p 8080:8080 \
-e DOCUMENT_ROOT=/var/www/html/public \
-v "$PWD:/var/www/html:ro" \
joaopinto14/production:2.0.2-php8.5The Laravel variant is configured for the standard Laravel project layout.
Default document root:
/var/www/html/public
Before starting the runtime image, the application should already be prepared.
A typical production dependency installation is:
composer install --no-dev --optimize-autoloaderFrontend assets should also be built before deployment when your application requires them.
Production intentionally does not include Composer or Node.js in the runtime image.
app/
bootstrap/
config/
public/
resources/
routes/
storage/
vendor/
artisan
composer.json
The following directories must be writable by the application runtime user:
storage/
bootstrap/cache/
docker run --rm \
-p 8080:8080 \
-e TIMEZONE=Europe/Lisbon \
-e PHP_MEMORY_LIMIT=256M \
-e UPLOAD_MAX_SIZE=32M \
-v "$PWD:/var/www/html:ro" \
-v "$PWD/storage:/var/www/html/storage" \
-v "$PWD/bootstrap/cache:/var/www/html/bootstrap/cache" \
joaopinto14/production:2.0.2-laravel-php8.5In the Laravel variant, Nginx only forwards the Laravel front controller:
/public/index.php
to PHP-FPM.
A file such as:
/public/test.php
is not executed directly.
This reduces the application's exposed PHP surface and keeps requests routed through Laravel's front controller.
The same Laravel image can be used for different application services.
docker run --rm \
-v "$PWD:/var/www/html" \
joaopinto14/production:2.0.2-laravel-php8.5 \
php artisan queue:workdocker run --rm \
-v "$PWD:/var/www/html" \
joaopinto14/production:2.0.2-laravel-php8.5 \
php artisan schedule:workdocker run --rm \
-v "$PWD:/var/www/html" \
joaopinto14/production:2.0.2-laravel-php8.5 \
php artisan migrate --forcedocker run --rm \
-v "$PWD:/var/www/html" \
joaopinto14/production:2.0.2-laravel-php8.5 \
php artisan aboutIn CLI mode, Nginx and PHP-FPM are not started.
Production is intentionally configured through a small set of environment variables.
| Variable | Default | Purpose |
|---|---|---|
TIMEZONE |
UTC |
Runtime and PHP timezone |
DOCUMENT_ROOT |
Generic: /var/www/html / Laravel: /var/www/html/public |
Directory served by Nginx |
PHP_MEMORY_LIMIT |
128M |
PHP memory limit |
UPLOAD_MAX_SIZE |
8M |
PHP upload limits and Nginx request body limit |
docker run --rm \
-p 8080:8080 \
-e TIMEZONE=Europe/Lisbon \
-e PHP_MEMORY_LIMIT=512M \
-e UPLOAD_MAX_SIZE=64M \
-v "$PWD:/var/www/html:ro" \
joaopinto14/production:2.0.2-php8.5If an invalid timezone is provided, the container exits instead of silently starting with an incorrect configuration.
Production exposes:
GET /healthz
on port:
8080
Manual test:
curl http://localhost:8080/healthzExpected response:
ok
/healthz verifies that the web runtime is responding. Database connectivity and other application dependencies should be checked by the application itself when required.
Runtime logs are written to stdout/stderr.
Use:
docker logs -f <container-name>This integrates naturally with Docker, Docker Swarm, Kubernetes, and external logging platforms.
Nginx access logs are emitted in JSON format.
You do not need to mount /var/log to collect Production runtime logs.
Production runs as a non-root user.
The internal HTTP port is:
8080
The image is designed to work with hardening options such as:
--read-only
--cap-drop ALL
--security-opt no-new-privileges:true
docker run --rm \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--tmpfs /run/production:rw,nosuid,nodev,noexec,size=16m \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
-p 8080:8080 \
-v "$PWD:/var/www/html:ro" \
joaopinto14/production:2.0.2-php8.5For Laravel, storage/ and bootstrap/cache/ still need writable storage.
Production 2.0.2 does not run chown -R or chmod -R on your application at startup. The container starts directly as the non-root www user and never needs a privileged permission-fixing phase.
The official images use a stable runtime identity:
www UID = 10001
www GID = 10001
This makes bind-mount permissions predictable across Production releases. When a Laravel project is mounted from the host, the host remains responsible for the mounted files and directories.
Laravel normally needs write access only to:
storage/
bootstrap/cache/
Prepare those directories on the host before starting the container:
sudo chown -R 10001:10001 storage bootstrap/cacheThe rest of the application can remain read-only to the runtime user.
You can verify the runtime identity directly from the image:
UID_RUNTIME=$(docker run --rm --entrypoint id joaopinto14/production:2.0.2-php8.5 -u)
GID_RUNTIME=$(docker run --rm --entrypoint id joaopinto14/production:2.0.2-php8.5 -g)
echo "$UID_RUNTIME:$GID_RUNTIME"Expected output:
10001:10001
The Generic variant includes the common PHP runtime extensions used by many applications:
ctype
curl
fileinfo
mbstring
openssl
pdo
session
tokenizer
xml
opcache
The Laravel variant includes everything in Generic and adds:
bcmath
dom
intl
pcntl
pdo_mysql
pdo_pgsql
pdo_sqlite
redis
zip
This makes the Laravel variant ready to work with:
- MySQL/MariaDB;
- PostgreSQL;
- SQLite;
- Redis through PhpRedis.
To keep the runtime small and production-focused, Production does not include:
Composer
Node.js
npm
Git
Supervisor
Python
GCC
make
Laravel framework
application vendor/
Production is a runtime image, not a development or build image.
The recommended workflow is:
Source code
↓
Build / Composer / Node.js
↓
Prepared application
↓
Production runtime
services:
web:
image: joaopinto14/production:2.0.2-php8.5
ports:
- "8080:8080"
environment:
TIMEZONE: Europe/Lisbon
PHP_MEMORY_LIMIT: 256M
UPLOAD_MAX_SIZE: 32M
volumes:
- ./:/var/www/html:roservices:
web:
image: joaopinto14/production:2.0.2-laravel-php8.5
ports:
- "8080:8080"
environment:
TIMEZONE: Europe/Lisbon
PHP_MEMORY_LIMIT: 256M
UPLOAD_MAX_SIZE: 32M
volumes:
- ./:/var/www/html:ro
- ./storage:/var/www/html/storage
- ./bootstrap/cache:/var/www/html/bootstrap/cache
queue:
image: joaopinto14/production:2.0.2-laravel-php8.5
command: php artisan queue:work
restart: unless-stopped
volumes:
- ./:/var/www/html
scheduler:
image: joaopinto14/production:2.0.2-laravel-php8.5
command: php artisan schedule:work
restart: unless-stopped
volumes:
- ./:/var/www/htmlIn production, secrets and application configuration should be supplied using the mechanism appropriate for your deployment platform.
Production 2.0.2 is built and validated for:
linux/amd64
linux/arm64
When the images are published as multi-platform Docker manifests, Docker automatically selects the correct architecture for the host.
The stable release targets are configured to publish BuildKit attestations together with the Docker Hub images:
SBOM SPDX software bill of materials
Provenance SLSA provenance (mode=max)
These attestations are registry metadata attached to the published image index. They do not add packages or tools to the Production runtime filesystem. Docker Scout can use this metadata alongside its own image analysis for package, vulnerability, and supply-chain visibility.
Attestations are intentionally enabled only for registry release targets. Local and test builds remain compatible with Docker engines using image stores that do not support attestations.
Production 2.0 includes an extensive automated test suite.
./tests/test-fast.shUseful during normal development.
./tests/test-all.shThe full suite validates, among other things:
- all six image variants;
- image contracts;
- health checks;
- Nginx;
- PHP-FPM;
- runtime configuration;
- concurrent PHP requests;
- logging;
- crash handling;
- signal handling;
- hardened runtime operation;
- image-size regression budgets.
./tests/test-release.shRelease validation additionally includes:
- clean no-cache builds;
- a real Generic PHP application;
- a real Laravel application;
- PHP 8.3, 8.4, and 8.5;
- restart stability;
- zombie-process detection;
linux/amd64builds;linux/arm64builds.
Production 2.0.0 is a major evolution from 1.1.3 and contains breaking changes that should be reviewed before upgrading.
| Area | 1.1.3 | 2.0.0 |
|---|---|---|
| PHP | PHP 8.3 | PHP 8.3, 8.4, and 8.5 |
| Variants | One image | Generic + Laravel |
| Internal port | 80 |
8080 |
| Process manager | Supervisor | Production's lightweight runtime manager |
| Runtime user | Privileged startup | www non-root |
| Document root variable | INDEX_PATH |
DOCUMENT_ROOT |
| PHP memory variable | MEMORY_LIMIT |
PHP_MEMORY_LIMIT |
| Runtime extension installation | PHP_EXTENSIONS |
Removed; extensions are installed at image build time |
| Extra processes | SUPERVISOR_CONF |
Separate containers / explicit CLI commands |
| Permissions | chown -R at startup |
Permissions prepared during build/deployment |
| Logs | Files under /var/log |
stdout/stderr |
| Health check | / on port 80 |
/healthz on port 8080 |
| Architectures | No validated release matrix | linux/amd64 + linux/arm64 |
docker run -p 80:80 joaopinto14/production:1.1.3docker run -p 8080:8080 joaopinto14/production:2.0.0-php8.5If you use a reverse proxy, update the backend/service port to:
8080
environment:
INDEX_PATH: /var/www/html/publicenvironment:
DOCUMENT_ROOT: /var/www/html/publicFor the Laravel variant, you usually do not need to set this variable because the default is already:
/var/www/html/public
environment:
MEMORY_LIMIT: 256Menvironment:
PHP_MEMORY_LIMIT: 256MUPLOAD_MAX_SIZE and TIMEZONE remain available.
In 1.1.3, additional PHP extensions could be installed at container startup:
environment:
PHP_EXTENSIONS: pdo_mysqlThis behavior has been removed in 2.0.0.
PHP extensions are now installed when the image is built, making startup:
- faster;
- deterministic;
- reproducible;
- compatible with non-root and read-only runtime operation.
The Laravel variant already includes MySQL, PostgreSQL, SQLite, and Redis support.
Production 1.1.3 used Supervisor to manage processes.
Production 2.0.0 replaces Supervisor with a very small POSIX runtime manager that only manages:
Nginx
PHP-FPM
As a result, this configuration was removed:
SUPERVISOR_CONF
Queue workers and schedulers should run as separate containers or services.
A single container could use Supervisor to manage additional application processes.
services:
web:
image: joaopinto14/production:2.0.0-laravel-php8.5
queue:
image: joaopinto14/production:2.0.0-laravel-php8.5
command: php artisan queue:work
scheduler:
image: joaopinto14/production:2.0.0-laravel-php8.5
command: php artisan schedule:workProduction 2.0.0 runs as:
www
Applications should no longer depend on root privileges during startup.
This improves runtime security but means mounted volume permissions must be correct before the container starts.
Starting with Production 2.0.2, the official images use a stable runtime identity:
www = 10001:10001
For Laravel bind mounts, prepare storage/ and bootstrap/cache/ for that UID/GID on the host.
Production 1.1.3 could recursively change ownership under /var/www/html during startup.
Production 2.0.0 no longer does this.
Application permissions must be prepared before startup.
For Laravel, make sure these directories are writable:
storage/
bootstrap/cache/
Instead of relying on log files inside the container, use:
docker logs -f <container-name>This is now the standard way to consume Production runtime logs.
http://container/
on port 80.
http://container:8080/healthz
Expected response:
ok
Production 1.1.3 used a single image approach.
Production 2.0.0 lets you choose the runtime that matches your application.
joaopinto14/production:2.0.0-php8.5
joaopinto14/production:2.0.0-laravel-php8.5
This keeps the Generic image smaller and avoids installing Laravel-specific extensions in applications that do not need them.
joaopinto14/production:2.0.0-php8.3
joaopinto14/production:2.0.0-php8.4
joaopinto14/production:2.0.0-php8.5
joaopinto14/production:2.0.0-laravel-php8.3
joaopinto14/production:2.0.0-laravel-php8.4
joaopinto14/production:2.0.0-laravel-php8.5
services:
web:
image: joaopinto14/production:1.1.3
ports:
- "80:80"
volumes:
- ./:/var/www/html
environment:
INDEX_PATH: /var/www/html/public
MEMORY_LIMIT: 256M
UPLOAD_MAX_SIZE: 32M
PHP_EXTENSIONS: pdo_mysql
TIMEZONE: Europe/Lisbonservices:
web:
image: joaopinto14/production:2.0.0-laravel-php8.5
ports:
- "8080:8080"
volumes:
- ./:/var/www/html:ro
- ./storage:/var/www/html/storage
- ./bootstrap/cache:/var/www/html/bootstrap/cache
environment:
PHP_MEMORY_LIMIT: 256M
UPLOAD_MAX_SIZE: 32M
TIMEZONE: Europe/LisbonThe following 1.1.3 settings are no longer used:
INDEX_PATH
MEMORY_LIMIT
PHP_EXTENSIONS
SUPERVISOR_CONF
They are replaced, when applicable, by:
DOCUMENT_ROOT
PHP_MEMORY_LIMIT
Production 2.0 follows one simple rule:
The build prepares the application. Production runs the application.
Tools that are only needed to build or develop the project stay outside the final runtime image.
This keeps the image smaller, more deterministic, and better suited for production deployments.
See LICENSE.md for license information.