Skip to main content
A Laravel application runs on Sevalla as an application built from your Git repository: Ingot installs its Composer packages, builds its Vite assets during the build and serves it with FrankenPHP in the web process, and a job runs the migrations before each deployment. Its data, sessions and cache live in a Sevalla PostgreSQL database that the application reaches over an internal connection.

At a glance

Environment variables

Before you begin

  • A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See Git providers.
  • PHP 8.5 and Composer 2 on your computer, and Node.js 24 or later with npm for the front-end build. Laravel 13 needs PHP 8.3 or later; this guide pins PHP 8.5.

Create the project

Create a Laravel project named storefront and install its front-end packages:
composer create-project installs Laravel 13, writes a .env file with a new APP_KEY for local development, and migrates a local SQLite database. npm install installs Vite and Tailwind CSS and writes package-lock.json. Laravel’s .gitignore already keeps .env, vendor/, node_modules/ and the built assets in public/build out of the repository. Sevalla doesn’t use .env: you set the production values as environment variables.

Prepare the project for Sevalla

Pin the PHP version

Ingot builds on PHP 8.5, its default line, unless the project sets a version. Pin the 8.5 line in require.php, so the build doesn’t change when a new Ingot release moves its default line:
See Pin the PHP version.

What Ingot reads

The project needs no build or start configuration. Ingot reads these files:

Choose the database

Laravel 13 keeps sessions, the cache and queued jobs in the database by default (SESSION_DRIVER, CACHE_STORE and QUEUE_CONNECTION are database), and its migrations create their tables, so one database serves the whole application and every instance of the web process shares the same sessions. This guide uses PostgreSQL because Laravel runs each migration on PostgreSQL in a transaction, so a migration that fails in the job leaves the schema as it was, while MySQL commits each schema change as it runs it.

How Ingot builds and starts the project

  • Build. Ingot installs the PHP extensions Laravel uses (bcmath, intl, pcntl, pdo_mysql, pdo_pgsql and redis), runs composer install --no-dev --optimize-autoloader and php artisan storage:link, then npm ci and npm run build, which writes the Vite assets to public/build.
  • Start. The web process runs Ingot’s Laravel start script. It runs php artisan optimize with the deployment’s environment variables, so the cached configuration never holds values from the build, then starts FrankenPHP on the port in the PORT environment variable (8080 by default), serving public/.
  • HTTPS. FrankenPHP trusts the X-Forwarded-Proto header from Sevalla’s proxy, so Laravel sees HTTPS requests as HTTPS and generates https:// URLs for assets and redirects without trusted proxy settings.
  • Logs. Ingot sets LOG_CHANNEL=stderr, so Laravel writes its log to the runtime logs instead of storage/logs, which resets on every deployment.
  • Migrations. Ingot doesn’t run them. A job runs them once per deployment, before the new version starts.
Commit everything and push it to your Git provider:

Deploy to Sevalla

These steps build with Ingot. To build from your own Dockerfile instead, see Deploy with a Dockerfile.
1

Create the database

In the sidebar, click Databases, then click Create database. Under Database, choose PostgreSQL and the newest version. Enter orders-db as the Display name, choose a Data center and a Size, then click Create database. See Create a database for every field.
2

Create the application

In the sidebar, click Applications, then click Create > Application. Under Source, click Private repository, then choose the Git provider, the Repository and the Branch. Under Location, choose the same Data center as the database. Keep the Size at H1 or choose a larger one, then click Create application. The application opens on its Overview tab with the status Not deployed yet: creating an application doesn’t deploy it.
3

Connect the database

Click the Networking tab. Under Connected services, click Add internal connection. Choose orders-db as the Service, turn on Add environment variables to the application, and click Add internal connection. Sevalla adds DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD and DB_URL, the names Laravel’s config/database.php reads. See Connect an application.
4

Add the environment variables

Click the Environment variables tab, then click Add environment variables. Add these variables and click Add environment variables:Don’t set APP_ENV or APP_DEBUG: without them, Laravel runs as production with debug mode off.
5

Create the migration job

Click the Processes tab, then click Create process. Choose Job, enter Migrate as the Name and php artisan migrate --force as the Custom start command. Keep the Start policy at Before deployment and the Resource at H1, then click Create process. --force confirms the migration, which Laravel otherwise asks for in production.
6

Turn on the readiness probe

On the Processes tab, click ⋯ on the Web process, then click Update process. Under Health checks, turn on Readiness probe, set the Path to /up and keep the Port at the web process’s port, 8080 unless you changed it. Click Update process. During a deployment, each new instance gets traffic only once /up answers. See Health checks.
7

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
8

Turn on auto-deploy

To deploy every push to the branch automatically, click the Settings tab, then click Update auto-deploy in the Auto-deploy section. Turn on Auto-deploy and click Update auto-deploy. See Deployments.
The Create process sheet with Job selected, the Name and Custom start command fields, the Start policy set to Before deployment, and the H1 resourceThe Create process sheet with Job selected, the Name and Custom start command fields, the Start policy set to Before deployment, and the H1 resource

Verify the deployment

  1. On the Deployments tab, wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running. Open the deployment to read its build log. Ingot’s build plan comes first:
    Build log
    Later, the build log shows vite build writing the assets to public/build/assets, and, during the rollout, Migrate has been successful. If the job fails, the deployment fails and nothing changes for visitors. See Read the build plan.
  2. Click the domain under the application’s name, for example storefront-x7k2m.sevalla.app. Laravel’s welcome page opens with Let's get started and the Laravel version, styled by the CSS that Vite built. Its stylesheet link uses https://:
    The page also starts a session, which Laravel saves in the sessions table: it works only when the database connection and the migrations do.
  3. Add /up to the address. The page reads Application up: it’s what the readiness probe sees.
The application header with its name, Running status, repository, branch and the domain that opens the applicationThe application header with its name, Running status, repository, branch and the domain that opens the application

Check from a terminal

These checks need no dashboard. The API calls use API, H, APP_ID and DEPLOYMENT_ID as set in Deploy with the API.
  1. The deployment’s status, from Get deployment (getDeploymentV3):
  2. The rollout’s results in the build log, from Get application deployment logs (getApplicationDeploymentLogsV3). The messages carry terminal color codes, which gsub removes, and select drops the lines of the image build, which start with #:
  3. The health route that the readiness probe requests, with your domain:
  4. The stylesheet link, as in Verify the deployment, which shows that the Vite assets, the database session and HTTPS work:

Read the logs

Laravel writes to the log channel in LOG_CHANNEL, which Ingot sets to stderr, so every log call and every error the application reports appears on the application’s Logs tab. For example, Log::info('Order 42 confirmed') appears as:
Runtime logs
The web process’s runtime logs also show each start: php artisan optimize caching the configuration, routes, events and views, then FrankenPHP started. Laravel logs every level from debug up by default; set the LOG_LEVEL environment variable to info or warning to log less. See Logs.

Deploy with a Dockerfile

Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the PHP extensions, the system packages and every layer. Add a Dockerfile, a Caddyfile and a .dockerignore to the root of the repository:
Dockerfile
The Caddyfile configures FrankenPHP the way Ingot does:
Caddyfile
  • FrankenPHP listens on the port in PORT and serves public/. Files that start with a dot get 404, and so does every PHP file except index.php.
  • Requests from Sevalla’s proxy that arrived over HTTPS reach Laravel with HTTPS on, so it generates https:// URLs, and $request->ip() returns the visitor’s address from the CF-Connecting-IP header rather than the proxy’s. Laravel needs no trusted proxy settings.
  • Vite gives built assets a new name when they change, so browsers and the Sevalla CDN may cache them for a year.
.dockerignore
The .dockerignore keeps your local .env, installed packages, built assets, caches and tests out of the build, so the image gets what composer install and npm run build produce from the lock files. Commit and push the three files. Ingot doesn’t build from your Dockerfile or use your Caddyfile, and it adds the patterns in .dockerignore to its own (the build plan shows Ignored 14 patterns from .dockerignore), so you can keep the files in a repository that builds with Ingot. Then switch the application to the Dockerfile build strategy:
1

Choose the Dockerfile build strategy

On the application’s Settings tab, in the Build strategy section, click Update build strategy. Click Dockerfile, keep the Dockerfile path Dockerfile and the Docker context ., then click Update build strategy.
2

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
The deployment’s build log shows 📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, then Migrate has been successful, and its Dockerfile tab shows your file. When the deployment’s status is Deployed, the welcome page and /up answer as in Verify the deployment, with https:// asset URLs. The Migrate job, the readiness probe and the environment variables work unchanged. See Dockerfile.

Deploy changes

With Auto-deploy on, every push to the branch starts a deployment; otherwise, click Deploy now for each change. Each deployment builds the image, runs the Migrate job, then replaces the web process’s instances once the new ones answer on /up. For a schema change, create a migration with php artisan make:migration and commit it with the code. When you add a package, commit composer.lock or package-lock.json with it. See Deployments.

Use environment variables

Add configuration on the application’s Environment variables tab and read it in config/ files with env(), as Laravel’s own configuration does. Ingot’s start script and the Dockerfile’s php artisan config:cache cache the configuration when each instance starts, so call env() only in config/ files and use config() everywhere else, as Laravel recommends for a cached configuration. Variables apply at the next deployment. See Environment variables.

Add a custom domain

To serve the application from your own domain, such as shop.example.com, add the domain on the application’s Domains tab and create the DNS records that Sevalla shows, then set APP_URL to https://shop.example.com and deploy. Custom domains need a web process larger than H1, so scale the web process first. See Custom domains.

Troubleshoot the deployment

For more causes, see Troubleshoot Ingot builds and Troubleshoot builds and deployments. To add queues, the scheduler, uploads or Redis caching, see the Laravel guides.

Deploy with the API

The same deployment as Deploy to Sevalla, as calls to the Sevalla API that a script or an AI agent runs in order. Each step names the matching CLI command and Sevalla MCP server tool. The sequence needs:
  • An API key with the Company developer role, or the capabilities to create, read and update applications and databases, in SEVALLA_API_KEY.
  • The repository pushed to GitHub, and a GitHub account with access to it connected to Sevalla. Connecting one is a dashboard step, and the API has no call that reports it: if no connected account can read the repository, the step that creates the application fails with 400 and a message that starts with Cannot access repository and says where to connect one.
  • curl, jq and openssl.
Set the base URL, the authorization header and the repository:
1

Choose the location

List clusters (listClustersV3) returns every location your company can use, with its region ID in location. Pick one by region ID, here Frankfurt, Germany (eu-frankfurt-1); Locations lists the others. Display names differ between environments, so don’t select by them:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the sizes and the PostgreSQL version

List database resource types (listDatabaseResourceTypesV3), List process resource types (listProcessResourceTypesV3) and List database versions (listDatabaseVersionsV3) give the IDs of DB1 and H1 and the newest PostgreSQL version in the location:
CLI: sevalla resources db-types, sevalla resources process-types and sevalla resources db-versions --cluster $CLUSTER_ID. MCP: get_platform_options for the sizes, and sevalla_api_read with listDatabaseVersionsV3 for the version.
3

Create the database

Create database (createDatabaseV3). You choose the password, and the API never returns it, so keep it in a variable for the environment variables:
The database starts as creating. Poll Get database (getDatabaseV3) until it’s ready. On error, delete the database and create it again:
CLI: sevalla databases create --name orders-db --type postgresql --db-version $PG_VERSION --cluster $CLUSTER_ID --resource-type $DB1_ID --db-name storefront --db-user storefront --db-password $DB_PASSWORD. MCP: create_database generates a password that it never returns. Create the application first, then pass connect_to_application_id and env_var_name: "DB_URL": the tool connects the database and stores its URL, in the form postgresql://<user>:<password>@<host>:<port>/<database>, in DB_URL, which Laravel reads in place of the other DB_* variables. Then skip the connection step, and leave the connection’s variables out of the environment variables step.
4

Create the application

Create application (createApplicationV3) creates it with a web process and doesn’t deploy it. List application domains (listApplicationDomainsV3) returns its sevalla.app domain as the primary one, for APP_URL:
Sevalla adds the domain about 2 seconds after it creates the application, so the loop waits for it. Without the wait, the variable can be empty: APP_URL is then https://, and the Migrate job fails with Invalid URI.CLI: sevalla apps create --name storefront --cluster $CLUSTER_ID --source privateGit --git-type github --repository $REPO_URL --branch $BRANCH, then sevalla apps domains list --app-id $APP_ID. MCP: create_application with deploy: false, then get_application.
5

Size the web process and add the readiness probe

The API creates the web process at S1, not the dashboard’s H1. List processes (listProcessesV3) finds it, and Update process (updateProcessV3) sets H1 and the readiness probe on /up. Send timeoutSeconds: a probe set through the API without it waits 120 seconds, not the dashboard’s 1:
CLI: sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID; the CLI has no option for the probe. MCP: scale_process with size_id and apply_now: false for the size, and sevalla_api_update with updateProcessV3 and the same body for the probe.
6

Connect the database

Create internal connection (createApplicationInternalConnectionV3):
It prints the connection’s ID. If it prints nothing or null, the connection wasn’t created: run the call again, because without it the application can’t reach the database. Unlike the dashboard, the API doesn’t add the connection’s environment variables. The next step adds them.CLI: sevalla apps internal-connections create $APP_ID --target-id $DB_ID --target-type database. MCP: sevalla_api_create with createApplicationInternalConnectionV3 and the same body.
7

Add the environment variables

Create environment variable (createEnvironmentVariableV3) adds one variable per call. The loop adds the connection’s variables in the dashboard’s names and format, from Get database, then a new APP_KEY, DB_CONNECTION and APP_URL, all for runtime only:
It prints each key as it’s added. APP_KEY is 32 random bytes in Base64 with the base64: prefix, the format php artisan key:generate writes.CLI: sevalla apps env-vars create $APP_ID --key DB_CONNECTION --value pgsql, once per variable; the CLI makes each variable available during build and runtime, with no option for runtime only. MCP: set_environment_variables with runtime: true and buildtime: false for each variable.
8

Create the migration job

Create process (createProcessV3) needs the size, a scaling_strategy and, for a job, its job_start_policy:
CLI: sevalla apps processes create $APP_ID --name Migrate --type job --resource-type $H1_ID --command "php artisan migrate --force" --job-start-policy beforeDeployment. MCP: sevalla_api_create with createProcessV3 and the same body.
9

Deploy

Trigger deployment (triggerDeploymentV3):
CLI: sevalla apps deployments trigger $APP_ID --branch $BRANCH. MCP: trigger_deployment.
10

Wait for the deployment

Poll Get deployment (getDeploymentV3) until the status is final: success, failed, cancelled or skipped. The loop keeps polling through a failed request and stops after 30 minutes:
On failed, read the build log with Get application deployment logs and see Troubleshoot the deployment. CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID and sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment and get_deployment_logs.
11

Turn on auto-deploy

Update application (updateApplicationV3):
To build from the Dockerfile instead of Ingot, send {"build_type": "dockerfile", "dockerfile_path": "Dockerfile", "docker_context": "."} with the same call before you deploy. CLI: sevalla apps update $APP_ID --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
Then run the checks in Check from a terminal, with https://$APP_HOST as the domain.