Deployment
Configuration for FrankenPHP, nginx+FPM and Apache, plus the checklist before you go live.
The same application runs on all three. Nothing in app/ or src/ changes — only the server configuration and which adapter public/index.php selects, which it does by itself.
Before you go live
APP_DEBUG=false. Debug mode puts stack traces, file paths and SQL into responses.APP_URLset to the real origin, so generated absolute URLs and link previews are right.- Configuration supplied by the environment, not a
.envon the server. ./orbit migraterun as a deploy step — never at boot.- Document root pointed at
public/, so.env,storage/andvendor/are unreachable. storage/writable by the web server user;chmod 750is usually right.- HTTPS terminated, and
TRUSTED_PROXIESset if you are behind a load balancer. composer install --no-dev --optimize-autoloader.
FrankenPHP (worker mode)
The fastest option, and the one that shares its process model with ./orbit serve.
# Caddyfile
example.test {
root * /srv/app/public
encode zstd gzip
php_server {
worker /srv/app/public/index.php
}
}$ frankenphp run --config /etc/caddy/Caddyfilepublic/index.php detects FrankenPHP and uses the worker adapter, which boots the application once and then serves requests in a loop, collecting cycles between them.
The application is booted once per worker process, so new code is not picked up until the workers restart. Reload FrankenPHP as part of your deploy, after migrations have run.
nginx + PHP-FPM
server {
listen 443 ssl http2;
server_name example.test;
root /srv/app/public;
index index.php;
# Real files first; everything else goes to the front controller.
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on;
}
# Dotfiles are never served. The framework refuses them too — two
# independent protections, because this line gets lost in a refactor.
location ~ /\. {
deny all;
}
client_max_body_size 8m;
}Static files are served by nginx directly, before PHP is invoked, so ServeStaticFiles never runs here. That is the intended arrangement — it is considerably faster.
Apache
public/.htaccess ships with the framework:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</IfModule>
<FilesMatch "^\.">
Require all denied
</FilesMatch>The virtual host needs AllowOverride All, or the rewrite rules are ignored and every URL 404s:
<VirtualHost *:443>
ServerName example.test
DocumentRoot /srv/app/public
<Directory /srv/app/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>Configuration in production
The real environment wins over .env, so inject settings the way your platform prefers:
# systemd unit
[Service]
Environment=APP_DEBUG=false
Environment=APP_URL=https://example.test
Environment=DB_DATABASE=/var/lib/app/app.sqlite
EnvironmentFile=/etc/app/secrets.env# docker-compose
services:
app:
environment:
APP_DEBUG: "false"
APP_URL: "https://example.test"
secrets:
- db_passwordA stale .env left on a server cannot override these, which is the point of that rule.
Behind a load balancer
TRUSTED_PROXIES=10.0.0.1,10.0.0.2X-Forwarded-Proto is believed only from these addresses. Without the list it is ignored entirely — otherwise anyone could claim their plaintext request was HTTPS and unlock Secure-only cookies.
A deploy sequence
$ git pull
$ composer install --no-dev --optimize-autoloader
$ ./orbit migrate # before the new code starts serving
$ systemctl reload frankenphp # or php8.3-fpmMigrations first, so the new schema exists before any process that expects it starts serving. Run them from exactly one host — the ledger prevents double-application, but two concurrent runs can still race.
Housekeeping
# Expired sessions and stale login attempts
0 3 * * * cd /srv/app && php orbit sessions:gcNeither is required for correctness — expired sessions are refused on read, and old attempts fall outside the throttle window — but both directories otherwise grow forever.
Which target should you choose?
| Situation | Choice |
|---|---|
| New deployment, containers, want speed | FrankenPHP. Boots once, same model as your dev server. |
| Existing nginx infrastructure | nginx + FPM. Boring, well understood, per-request isolation. |
| Shared or legacy hosting | Apache. Works with .htaccess and no root access. |
| Anything at all | Not ./orbit serve. It serves connections sequentially. |
Because the application is identical across all of them, moving later is a configuration change rather than a rewrite — which is the whole point of the constraint the framework is built around.