PHP on Miren
PHP isn't auto-detected, so you deploy it with a Dockerfile.miren. This guide uses
FrankenPHP — a modern PHP application server that runs your
app from a single image, no separate nginx + php-fpm wiring. It works for plain PHP and
for frameworks like Laravel and Symfony.
Ask your AI coding agent to "set up this Laravel app on Miren" after installing the
Miren agent skills. It adds the Dockerfile.miren, points the server
at 0.0.0.0:$PORT, wires up environment variables, and deploys — using this page as its
reference.
Do you need a Dockerfile?
Yes. Miren doesn't auto-detect PHP, so add a Dockerfile.miren to your project root.
Miren builds from it instead of guessing the stack — see
Using Dockerfile.miren.
Miren auto-detects and builds common stacks (Python, Node, Bun, Go, Ruby, Rust) without a Dockerfile. This language isn't one of them yet — if you'd like first-class support, request it.
The Dockerfile
Create Dockerfile.miren in your project root. FrankenPHP serves your public/
directory:
FROM dunglas/frankenphp:1-php8.3
WORKDIR /app
COPY . /app
EXPOSE 8080
For a Laravel or Composer app, install dependencies during the build:
FROM dunglas/frankenphp:1-php8.3
# Composer from its official pinned image — no piping a remote installer into php
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN install-php-extensions pdo_pgsql pdo_mysql zip
WORKDIR /app
# Copy the manifests first so the install layer is deterministic and cache-friendly;
# committing composer.lock pins exact dependency versions.
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts --no-interaction
COPY . /app
RUN composer dump-autoload --optimize --no-interaction
EXPOSE 8080
(install-php-extensions ships with the FrankenPHP image.)
.dockerignore
.git
vendor
Set up the app
Even with a Dockerfile.miren, Miren needs at least one service defined — it
doesn't use the image's CMD as the start command. FrankenPHP's php-server command
takes the listen address and document root; point it at Miren's injected $PORT and
0.0.0.0 in a Procfile:
web: frankenphp php-server --listen 0.0.0.0:$PORT --root /app/public
Then create .miren/app.toml naming your app and deploy from your project root:
name = "php-bench"
miren deploy
If no service is defined, the build succeeds but the deploy stops with
no services defined: please define at least one service in a Procfile or .miren/app.toml.
Environment variables
Set variables with miren env set — -e for plain values, -s for secrets (masked in
output and logs). Read them with getenv() or your framework's config (Laravel's
env()):
miren env set -e APP_ENV=production
miren env set -s APP_KEY
miren env set -s DATABASE_URL
Generate a Laravel APP_KEY locally with php artisan key:generate --show. You can
also declare variables in .miren/app.toml:
[[env]]
key = "APP_ENV"
value = "production"
Need a managed Postgres database? Add a miren-postgresql addon and Miren
injects DATABASE_URL for you. See
App Configuration — Environment Variables.
Agent quick reference
- Detection: none — requires
Dockerfile.miren - Base image:
dunglas/frankenphp:1-php8.3; useinstall-php-extensionsfor pdo_pgsql, etc. - Composer:
composer install --no-dev --optimize-autoloaderduring the build - Service is required:
Procfileweb: frankenphp php-server --listen 0.0.0.0:$PORT --root /app/public— the imageCMDis not used - Port: FrankenPHP
--listen 0.0.0.0:$PORT - Env vars:
miren env set -e/-s; read withgetenv()/ Laravelenv() - Database: optional
[addons.miren-postgresql]injectsDATABASE_URL
Next steps
- Using Dockerfile.miren — how custom builds work
- Addons — managed Postgres and other backing services
- App Configuration — customize
.miren/app.toml - Deployment — how deploys build and activate