Deploying PHP on Hop3¶
A PHP app on Hop3 is your project plus its composer.json: Hop3 installs the PHP runtime and Composer for you, runs composer install to fetch your dependencies, runs any framework warm-up step (migrations, cache priming), then starts a PHP web process bound to a port Hop3 hands it. Nginx sits in front and proxies your hostname to that process. You write almost no PHP-specific glue — the same composer install / $PORT / nginx pattern carries every framework below.
This page covers what is shared by every PHP deployment. Read it first, then follow the framework guide that matches your stack.
How Hop3 builds and runs PHP¶
Hop3 detects a PHP app from its composer.json. You list the runtime and extensions you need in [build].packages (e.g. php, composer, plus extensions like php-pgsql, php-mbstring, php-xml, php-intl), and [build].before-build runs composer install --no-dev --optimize-autoloader to produce a vendor/ directory.
The app is run as a long-lived web process bound to the dynamic port Hop3 injects as $PORT. Hop3 picks the port — your process must bind exactly to it on 0.0.0.0 — and nginx terminates the public hostname and proxies requests to that port. Any framework warm-up (database migrations, config/route/view caching) goes in [run].before-run, which runs once before the process starts.
A representative hop3.toml shared across PHP frameworks:
[metadata]
id = "my-php-app"
version = "1.0.0"
[build]
before-build = ["composer install --no-dev --optimize-autoloader"]
packages = ["php", "php-pgsql", "php-mbstring", "php-xml", "composer"]
[run]
start = "php -S 0.0.0.0:$PORT -t public"
before-run = "..." # framework warm-up: migrations, cache priming
[env]
APP_ENV = "production"
[healthcheck]
path = "/up"
The start line is the one real difference between frameworks: a Symfony skeleton serves its public/ document root with the built-in server (php -S 0.0.0.0:$PORT -t public), while Laravel uses php artisan serve --host=0.0.0.0 --port=$PORT. Both are fine for getting an app live; for heavier production traffic, move to PHP-FPM behind nginx or a runtime like Laravel Octane / FrankenPHP.
Notes that apply to every PHP app¶
- Document root is
public/. PHP frameworks put a single front controller (index.php) inpublic/; never expose the project root. The built-in server's-t public(or Laravel'sartisan serve) enforces this. - Databases via addons. Create and attach a Postgres or MySQL addon and Hop3 injects
DATABASE_URLinto the app's environment. Parse it in your framework config (Laravel reads it inconfig/database.php; Symfony's Doctrine readsDATABASE_URLdirectly). Declare the addon inhop3.tomlwith a[[provider]]block, then run migrations frombefore-run. - Secrets are config, not files. Declare app-internal secrets in
hop3.toml[env]so Hop3 generates them on the first deploy:APP_KEY = { generate = "base64", length = 32, prefix = "base64:" }(Laravel) orAPP_SECRET = { generate = "hex", length = 16 }(Symfony) — never committed. Set non-secret config likeAPP_ENV=production/prodandAPP_DEBUG=false/0in[env]too; usehop3 config setfor secrets you supply yourself. - Logs go to stderr. Point your framework's log channel at stderr (
LOG_CHANNEL=stderrfor Laravel) sohop3 app logscaptures them. - Drop
composer.lockbefore the first deploy if your local PHP version differs from the server's — it avoids "platform requirements" install failures. Drop a straypackage.jsontoo unless you actually build front-end assets, so Hop3 doesn't try to run a Node build it doesn't need. - Writable directories. Laravel needs
storage/andbootstrap/cache/writable; Symfony needsvar/. If you hit permission errors, fix them inbefore-runrather than by hand on the server. - Front-end assets (Vite, Webpack Encore) are an opt-in extra step: add
npm install && npm run buildtobefore-buildandnodejstopackages.
Choose a framework¶
| Framework | Description |
|---|---|
| Laravel | Full-stack PHP framework; deployed with php artisan serve, migrations and config caching in before-run, Postgres/Redis via addons. |
| Symfony | Enterprise PHP framework; serves public/ via the built-in PHP server, Doctrine reads DATABASE_URL, cache warmed in before-run. |