Deploy Laravel with PHP-FPM, queues and scheduled jobs on a VPS. A modern alternative to Forge plus separate server management.
Choose your runtime: FPM or Octane
Classic PHP-FPM behind nginx is the battle-tested default: each request gets a fresh application boot, which is forgiving of sloppy state but pays the bootstrap cost every time. Laravel Octane keeps the framework booted in a persistent process and serves requests at several times the throughput.
For containers, Octane on FrankenPHP is the sweet spot: FrankenPHP is a single binary that speaks HTTP directly (no separate nginx layer), understands early hints and HTTP/3, and makes the Dockerfile dramatically simpler:
FROM dunglas/frankenphp:php8.4
RUN install-php-extensions pdo_pgsql pdo_mysql redis pcntl opcache
COPY . /app
WORKDIR /app
RUN composer install --no-dev --optimize-autoloader \
&& php artisan config:cache && php artisan route:cache && php artisan view:cache
EXPOSE 8000
CMD ["php", "artisan", "octane:frankenphp", "--host=0.0.0.0", "--port=8000"]
The full service topology
A production Laravel app is typically four services, all from the same image or templates:
Web: the Octane container above, behind the reverse proxy with automatic HTTPS
Queue worker: same image, command php artisan queue:work --tries=3 --max-time=3600
Scheduler: php artisan schedule:run executed every minute as a scheduled task (replaces the crontab entry)
Database and Redis: managed services on the same server; MySQL or Postgres for data, Redis for queues, cache and sessions
Configuration and storage
Move every .env value into platform environment variables; APP_KEY especially must be set (generate once with php artisan key:generate --show) or encrypted cookies and sessions break on every deploy. Cache config at build time as in the Dockerfile above, and remember that config:cache means env() calls only work inside config files, a classic Laravel deployment gotcha.
Point FILESYSTEM_DISK at S3-compatible storage for user uploads: container disks are replaced on every deploy by design. MinIO on the same server or Cloudflare R2 both work with the standard league/flysystem-aws-s3-v3 driver.
Octane-specific care
Because Octane reuses the booted framework across requests, per-request state must actually be per-request: avoid static properties that accumulate, be careful with singletons that capture request data, and read the Octane documentation’s short list of patterns to avoid. Most apps need zero changes; apps with creative service providers need a review.
Why this beats the Forge model
Forge provisions mutable PHP servers well, but you pay $12 or more a month for the tool, servers drift as packages update in place, and reproducing a server after a disaster is a checklist rather than a command. Container deploys make every release an immutable artifact: identical between staging and production, instantly rollback-able, and rebuildable from the repo on any fresh VPS.
On costs: Forge plus a $12 DigitalOcean droplet is about $24 a month before backups. The containerized equivalent on Peon is the same droplet plus $2, with database backups to S3 and unlimited team members included.
Top comments (0)