# PHP Dockerfile Generator

URL: /dockerizer/php

Generate a production Dockerfile for any PHP application — FrankenPHP or Apache, OPcache tuned and running as an unprivileged user.

## Default configuration

- `appName` (App name): php — Used for the image tag, the compose service and the OCI labels.

- `port` (Port): 8080 — 8080 rather than 80 so the web server runs entirely as an unprivileged user. Publish it as 80 with `-p 80:8080`.

- `documentRoot` (Document root): public — The directory holding index.php, relative to the project root. Only this directory is web-reachable.

- `server` (Web server): frankenphp — FrankenPHP embeds PHP in a Caddy server: one process, HTTP/2 and HTTP/3, and worker mode if you want it. Apache with mod_php is the classic single-process alternative.

- `phpVersion` (PHP version): 8.4

- `phpExtensions` (PHP extensions): pdo_mysql intl zip gd opcache — Space-separated. Installed with install-php-extensions, which pulls the build dependencies and removes them again afterwards.

- `phpMemoryLimit` (memory_limit): 256M

- `phpUploadMaxFilesize` (upload_max_filesize): 32M

- `phpPostMaxSize` (post_max_size): 32M

- `phpMaxExecutionTime` (max_execution_time): 60

- `phpTimezone` (date.timezone): UTC

- `database` (Database service): none — Adds the database to docker-compose.yml with a healthcheck, a named volume and a DATABASE_URL wired into the app.

- `redis` (Redis service): false — Adds Redis to docker-compose.yml and exposes REDIS_URL to the app.

- `cacheMounts` (BuildKit cache mounts): true — Persists the package manager store between builds. Repeat builds skip the download entirely.

- `multiArch` (Multi-architecture build): false — Adds BUILDPLATFORM/TARGETARCH so `docker buildx build --platform linux/amd64,linux/arm64` cross-compiles natively.

- `healthcheck` (Healthcheck): true — Adds a HEALTHCHECK so orchestrators can restart an unresponsive container.

- `ociLabels` (OCI labels): false — Adds org.opencontainers.image.* metadata to the final image.

- `buildSecret` (Build secret mount): false — Reads a private registry token via --mount=type=secret so it never lands in an image layer.

## Generated files

### Dockerfile

Multi-stage build: dependencies, compilation and the runtime image are separate, so only what the app needs at runtime ships.

```docker
# syntax=docker/dockerfile:1
# Generated by Easypanel Dockerizer — https://easypanel.io/dockerizer

# --- Base ---------------------------------------------
FROM dunglas/frankenphp:1-php8.4-alpine AS base
WORKDIR /app

# install-php-extensions resolves the build dependencies for each extension and removes them afterwards
COPY --from=mlocati/php-extension-installer:latest /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pdo_mysql intl zip gd opcache

# Production php.ini, copied over the image defaults
COPY php.ini /usr/local/etc/php/conf.d/zz-app.ini

# --- Dependencies -------------------------------------
FROM base AS vendor
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Manifests only — application edits must not re-resolve the tree
COPY composer.json composer.lock* ./

# --no-scripts and --no-autoloader because the application is not in place yet
RUN --mount=type=cache,id=composer-cache,target=/root/.composer/cache,sharing=locked \
    composer install --no-dev --no-interaction --prefer-dist --no-progress --optimize-autoloader --no-scripts --no-autoloader

# --- Runtime ------------------------------------------
FROM base AS runtime
COPY Caddyfile /etc/frankenphp/Caddyfile

# Caddy keeps its certificate and config state here; www-data has to own it
RUN chown -R www-data:www-data /data /config

# Vendor tree first, so application edits do not invalidate it
COPY --from=vendor --chown=www-data:www-data /app/vendor ./vendor
COPY --chown=www-data:www-data . .
RUN --mount=type=bind,from=composer:2,source=/usr/bin/composer,target=/usr/bin/composer \
    composer dump-autoload --optimize --classmap-authoritative --no-dev
USER www-data
EXPOSE 8080
STOPSIGNAL SIGTERM

# Lets Docker, Compose and Easypanel see when the app is wedged
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD ["php", "-r", "exit(@file_get_contents('http://127.0.0.1:8080/') === false ? 1 : 0);"]

# No CMD: the FrankenPHP image starts Caddy itself, and the Caddyfile above points it at /app/public.

```

### .dockerignore

Keeps the build context small and stops secrets and local dependencies from reaching an image layer.

```bash
# Version control
.git
.gitignore
.github

# Secrets — never bake these into an image layer
.env
.env.*
!.env.example
*.pem
*.key

# Editor and OS noise
.vscode
.idea
.DS_Store
Thumbs.db

# Docs and local tooling
README.md
LICENSE
docs
.editorconfig
docker-compose*.yml
Dockerfile*
.dockerignore

# Composer installs inside the image
vendor

# Framework caches and user uploads
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*
var/cache
var/log

```

### php.ini

Production PHP settings with OPcache and JIT enabled, timestamp validation off and errors sent to stderr.

```ini
; Production php.ini overrides. Copied over the image defaults.
date.timezone = UTC
memory_limit = 256M
max_execution_time = 60
post_max_size = 32M
upload_max_filesize = 32M

expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /dev/stderr

; OPcache — the single biggest PHP performance win in production.
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 64M

realpath_cache_size = 4096K
realpath_cache_ttl = 600

```

### Caddyfile

FrankenPHP's Caddy config — serves the front controller directly, with automatic compression and HTTP/2.

```nginx
{
	frankenphp
	order php_server before file_server
	auto_https off
}

:8080 {
	root * /app/public
	encode zstd br gzip

	# Refuse dotfiles outright — .env must never be reachable.
	@dotfiles path_regexp \/\.
	respond @dotfiles 404

	php_server {
		index index.php
	}

	header {
		X-Content-Type-Options nosniff
		X-Frame-Options SAMEORIGIN
		-Server
	}
}

```

### docker-compose.yml

Runs the image locally with its backing services, wired together and health-gated.

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "8080:8080"

```

## Frequently asked questions

### My app has no public/ directory. What do I set?

Set the document root to `.` — but understand the trade-off: everything in your project, including .env and composer.json, becomes web-reachable. Moving index.php into public/ is worth the ten minutes.

### What if I have no composer.json?

The vendor stage will fail. Either add one — even an empty `{}` works — or drop the vendor stage and the dump-autoload line from the generated Dockerfile.

## Related generators

- [Laravel](/dockerizer/laravel)

- [Symfony](/dockerizer/symfony)

- [Static Site](/dockerizer/static)

- [Node.js](/dockerizer/nodejs)