> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy PHP applications

> How PHP applications build and run on Sevalla with Ingot: FrankenPHP, PHP versions, Composer, extensions, the document root, and processes for queues, schedulers and migrations.

A PHP application runs on Sevalla as an [application](https://docs.sevalla.com/applications/overview.md): Sevalla builds an image from your Git repository, then runs it as processes. The web process serves HTTP with FrankenPHP, a PHP server built on Caddy, background workers run queue consumers and schedulers, and jobs run migrations before each deployment.

## Framework guides

> **[Laravel](https://docs.sevalla.com/guides/php/laravel/overview.md)**
>
> Deploy Laravel with PostgreSQL, then add queues, the scheduler, object storage and Redis caching.

> **[Symfony](https://docs.sevalla.com/guides/php/symfony.md)**
>
> Deploy a Symfony application from a Git repository.

> **[WordPress](https://docs.sevalla.com/guides/php/wordpress.md)**
>
> Deploy WordPress from a Git repository with a MySQL database.

## How PHP applications run on Sevalla

| Part | On Sevalla |
| - | - |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's builder, by default. It recognizes a PHP application by `composer.json`, or by an `index.php` in `public/`, `webroot/` or the build path. It builds on FrankenPHP's official image, and the final image is Debian 13 with PHP, FrankenPHP, the extensions and the libraries they load, without the compilers the PHP image keeps. |
| Package manager | Composer 2. Ingot runs `composer install --no-dev --optimize-autoloader` from `composer.lock`, with your Composer scripts. Without `composer.lock`, the build log warns that Composer chooses dependency versions during the build. For private packages, commit an `auth.json` (Ingot deletes it after the install) or set `COMPOSER_AUTH` as a build-time [environment variable](https://docs.sevalla.com/applications/environment-variables.md). |
| PHP version | PHP 8.5, Ingot's default line, unless the project sets another. Pin it with `require.php` in `composer.json`: see [Pin the PHP version](#pin-the-php-version). |
| Extensions | Ingot installs the extensions the application needs: see [PHP extensions](#php-extensions). |
| Web server | FrankenPHP serves the document root on `0.0.0.0` and the port in the `PORT` environment variable, which Sevalla sets to the web process's port (`8080` by default). The document root is the public path a Laravel application sets, otherwise `public/` (Laravel, Symfony), `webroot/` (CakePHP), or the build path, whichever holds an `index.php`. |
| Start command | For Laravel, a script that creates the folders Laravel writes to, runs `php artisan optimize` with the deployment's environment variables, then starts FrankenPHP. For everything else, FrankenPHP with Ingot's configuration. The build plan's **Start** row shows a start command you set. See [Set the start command](https://docs.sevalla.com/applications/builds/ingot.md#set-the-start-command). |
| Concurrency | FrankenPHP runs one server process that handles each request on a PHP thread from a pool, so one instance serves requests in parallel without PHP-FPM. To serve more traffic, add instances or choose a larger size: see [Scaling](https://docs.sevalla.com/applications/scaling.md). |
| Requests | Only the front controller (`index.php`) runs for Laravel and Symfony: a request for any other `.php` file gets `404`, so a script uploaded to the public folder never runs. Files that start with a dot, such as `.env`, are never served. |
| Proxy headers | Ingot's FrankenPHP configuration trusts forwarded headers only on requests from private networks, which is where Sevalla's proxy connects from. On those, it takes HTTPS from `X-Forwarded-Proto` and the visitor's IP address from `CF-Connecting-IP`, or from `X-Forwarded-For` when that header is missing, and gives PHP `HTTPS` and `REMOTE_ADDR` to match. Laravel and Symfony generate `https://` URLs and see the visitor's address without trusted proxy settings. |
| `php.ini` | PHP's production `php.ini`, with uploads up to `64M` and OPcache sized for frameworks. For Laravel and Symfony, OPcache doesn't check files for changes, because the code in an image never changes. A `.user.ini` in the build path or the document root adds your own settings. |
| Front-end assets | When `package.json` has a `build` script, Ingot installs the JavaScript packages, runs the script (for example Vite) during the build, and keeps what it builds but not `node_modules`. |
| Migrations | A job with the start policy **Before deployment**, for example `php artisan migrate --force` or `php bin/console doctrine:migrations:migrate --no-interaction`. If it fails, the deployment fails and the previous version keeps running. Ingot never runs migrations itself. |
| Queues | A background worker, for example `exec php artisan queue:work` for Laravel or `exec php bin/console messenger:consume async` for Symfony. Sevalla runs a start command with `/bin/sh -l -c`; `exec` makes the worker the instance's main process, so it receives the stop signal on deployments and, with the `pcntl` extension, finishes its job first. Ingot installs `pcntl` for Laravel and Symfony. See [Stop the worker cleanly on deployments](https://docs.sevalla.com/guides/php/laravel/queues.md#stop-the-worker-cleanly-on-deployments) and [Deploy a Symfony application](https://docs.sevalla.com/guides/php/symfony.md). |
| Scheduled tasks | A background worker running `exec php artisan schedule:work` for Laravel's scheduler, or a cron job for a single command on a schedule. |
| Databases | MySQL, MariaDB, PostgreSQL, Redis or Valkey on Sevalla, reached over an [internal connection](https://docs.sevalla.com/databases/connect.md#connect-an-application). The variables it adds, such as `DB_HOST` and `REDIS_URL`, are the names Laravel's configuration reads. |
| Files | The filesystem resets on every deployment. Keep uploads in [object storage](https://docs.sevalla.com/object-storage/overview.md), or attach a [disk](https://docs.sevalla.com/applications/disks.md). |
| Other build strategies | [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md), [Buildpacks](https://docs.sevalla.com/applications/builds/buildpacks.md) (`heroku/php`), [Railpack](https://docs.sevalla.com/applications/builds/railpack.md) and the deprecated [Nixpacks](https://docs.sevalla.com/applications/builds/nixpacks.md), on the application's **Settings** tab |

Every process (web, worker, cron job and job) runs from the same image, each with its own start command. Ingot recommends processes it finds a use for, such as a migration job when `database/migrations` has migrations, and lists them in the generated Dockerfile's header; it doesn't create them. See [Run workers and jobs](https://docs.sevalla.com/applications/builds/ingot.md#run-workers-and-jobs).

## Pin the PHP version

Ingot builds on PHP 8.5, its default line, unless the project sets a version. Pin the line you develop with in `require.php` in `composer.json`, so the build doesn't change when a new Ingot release moves its default line:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
composer require "php:~8.5.0"
```

Ingot takes the version from the first of these that's set: the `INGOT_PHP_VERSION` environment variable, `versions.php` in `ingot.json`, `config.platform.php` in `composer.json`, then `require.php`. A range such as `~8.5.0` or `^8.3` builds on the newest release of a line it allows, preferring Ingot's default line, so every new build picks up the line's security fixes. Ingot 0.4.1 knows PHP 8.2 to 8.5. The build plan's **PHP** row shows the version and where it came from.

Ingot also checks `composer.lock`, because Composer refuses to install a package that doesn't support the PHP it runs on:

* When only `require.php` sets the version, or nothing does, and a locked package doesn't support the line Ingot would choose, Ingot builds on the newest line that every locked package supports and names the package in the **PHP** row.
* When you set the version with the variable, `ingot.json` or `config.platform.php`, and a locked package doesn't support it, the build fails with `PHP 8.5 doesn't satisfy composer.lock: <package> requires php "<range>"`. Run `composer update` to lock newer packages, or choose a version they support.
* Write `~8.5.0` or `8.5.*` rather than `8.5`: Composer reads `8.5` as exactly 8.5.0, so Ingot installs with `--ignore-platform-req=php+` and warns about it.

See [Set the language version](https://docs.sevalla.com/applications/builds/ingot.md#set-the-language-version).

## PHP extensions

FrankenPHP's image includes the extensions most applications need, among them `ctype`, `curl`, `dom`, `fileinfo`, `mbstring`, `openssl`, `pdo_sqlite`, `sodium`, `tokenizer`, `xml`, `zlib` and OPcache. Ingot installs more from these sources, and the build plan's **Extensions** row names each one with its source:

| Source | Extensions |
| - | - |
| `ext-*` in `require` of `composer.json`, and in the packages of `composer.lock` | Each one named, such as `"ext-gd": "*"` for `gd` |
| Laravel | `bcmath`, `intl`, `pcntl`, `pdo_mysql`, `pdo_pgsql` and `redis` |
| Symfony | `intl` and `pcntl`, plus `pdo_mysql` and `pdo_pgsql` with Doctrine |
| Packages that use an extension without requiring it | `gd` for `intervention/image` and `dompdf/dompdf`; `exif` and `gd` for `spatie/image` and `spatie/laravel-medialibrary` |
| The database your defaults select | `DB_CONNECTION` in Laravel's `.env.example`, `DATABASE_URL` in Symfony's `.env`, for example `pdo_sqlsrv` for SQL Server |
| WordPress (`wp-settings.php`) | `exif`, `gd`, `intl`, `mysqli` and `zip` |
| Code without a framework | `mysqli`, `pdo_mysql`, `pdo_pgsql` or `pgsql` when the code calls them |

To add an extension Ingot doesn't detect, add it to `require` in `composer.json`, for example `"ext-imagick": "*"`. Composer then refuses to install without it on your computer too, which keeps your computer and the image in step.

## Build with a Dockerfile

Ingot needs no Dockerfile. To control every layer of the image, the [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) build strategy builds your own. A Dockerfile for PHP on Sevalla installs the extensions with `install-php-extensions` in a stage based on FrankenPHP's official image and lists the Debian packages their libraries come from, installs the dependencies with `composer install --no-dev` from `composer.lock` in a build stage, builds front-end assets in a Node.js stage, and copies PHP, those packages and the application into a `debian:trixie-slim` final image that runs as a non-root user. FrankenPHP listens on `PORT` and trusts Sevalla's proxy for HTTPS, as Ingot's configuration does, and nothing secret is written into the image. [Deploy Laravel](https://docs.sevalla.com/guides/php/laravel/deploy.md#deploy-with-a-dockerfile), [Symfony](https://docs.sevalla.com/guides/php/symfony.md#deploy-with-a-dockerfile) and [WordPress](https://docs.sevalla.com/guides/php/wordpress.md#deploy-with-a-dockerfile) each have a complete example.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.