Skip to main content
A Statamic site runs on Sevalla as an application built from your Git repository: Statamic is a Laravel application, so Ingot builds it with its Laravel conventions, installs the Composer packages, builds the Vite assets and serves the site with FrankenPHP in the web process, on the port that Sevalla sets in the PORT environment variable. The content, the users and the blueprints are files in the repository, so every deployment and every instance serves the same site, and the site needs no database.

At a glance

Environment variables

Don’t set APP_ENV or APP_DEBUG: without them, Laravel runs as production with debug mode off, and Statamic doesn’t watch the content files for changes.

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. Statamic 6 needs PHP 8.3 or later; this guide pins PHP 8.5.

Create the project

Create a Statamic site named storefront and install its front-end packages:
composer create-project installs Statamic 6 on Laravel 13 and writes a .env file with a new APP_KEY for local development. The site starts with one page, Home, in content/collections/pages/home.md, which reads Your blank canvas is ready. Statamic’s .gitignore already keeps .env, vendor/, node_modules/, the built assets in public/build and the control panel’s assets in public/vendor/statamic out of the repository. Create the user you sign in to the control panel with. php please make:user without arguments asks for each value; with an email address, it creates the user from its options:
Statamic saves the user as a file, users/admin@shop.example.com.yaml, with a hash of the password, not the password itself. Commit it with the site: Sevalla serves the users that are in the repository.

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.

Keep the content in Git

Statamic keeps entries, navigation, globals, taxonomies, users and blueprints as files in content/, users/ and resources/. Sevalla builds each deployment’s image from your repository and starts every instance from that image, so the files in Git are what the site serves, on every instance and after every deployment. Edit the content on your computer and push it:
  1. Run the site locally with php artisan serve, and sign in to the control panel at http://localhost:8000/cp.
  2. Edit the content. Statamic saves each change as a file in the project.
  3. Commit the files and push. The next deployment serves them.
Changes you save in the control panel of the site on Sevalla are written to the instance’s own files. With Ingot’s image, the change shows only on that instance and is gone after the next deployment or restart. With the Dockerfile’s image, the content folders are read-only, and the save fails.
To let editors work in the control panel of the live site instead, keep the content in a database with Statamic’s Eloquent driver, and the uploaded assets in object storage. This guide keeps the content in Git, which needs no database, keeps every change in your Git history, and lets you run more than one instance. Statamic’s .env.example keeps Laravel’s sessions in files (SESSION_DRIVER=file). On Sevalla, each instance has its own files, and a deployment replaces them, so every deployment would sign everyone out of the control panel, and each instance would keep its own sessions. Set SESSION_DRIVER to cookie on Sevalla: Laravel then keeps the session in the browser, encrypted with APP_KEY, and a sign-in survives deployments and works on every instance.

What Ingot reads

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

How Ingot builds and starts the project

  • Build. Ingot installs the PHP extensions Laravel uses and gd, then runs composer install --no-dev --optimize-autoloader. Its Composer scripts run php artisan statamic:install, which publishes the control panel’s CSS and JavaScript to public/vendor/statamic. Then Ingot runs npm ci and npm run build, which writes the site’s CSS and JavaScript to public/build.
  • Start. The web process runs Ingot’s Laravel start script. It runs php artisan optimize with the deployment’s environment variables, 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 Statamic sees HTTPS requests as HTTPS: the site and the control panel load their assets from https:// URLs.
  • Logs. Ingot sets LOG_CHANNEL=stderr, so Laravel and Statamic write their log to the runtime logs.
  • No database. The Dockerfile that Ingot generates, on the deployment’s Dockerfile tab, recommends a migration job, because database/migrations has Laravel’s own migrations. This site keeps its content and users in files and its sessions in a cookie, so it doesn’t use a database: skip the job.
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 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 a Data center. 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.
2

Add the environment variables

Click the Environment variables tab, then click Add environment variables. Add these variables and click Add environment variables:
3

Turn on the readiness probe

Click 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.
4

Deploy

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

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. Each content change you push then goes live with its own deployment. See Deployments.
The Add environment variables sheet with the Availability checkboxes and rows of keys and valuesThe Add environment variables sheet with the Availability checkboxes and rows of keys and values

Verify the deployment

  1. On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Laravel and PHP 8.5 from composer.json, the extensions and where each comes from, and the Vite build from package.json.
    Build log
  2. Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running.
  3. Click the domain under the application’s name, for example storefront-x7k2m.sevalla.app. The Home page shows Your blank canvas is ready., styled by the CSS that Vite built.
  4. Add /cp to the address. Statamic’s sign-in page opens over HTTPS. Sign in with the email and password of the user in users/: the control panel’s dashboard opens.
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
If the deployment fails, read its build log, then see Troubleshoot the deployment.

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 health route that the readiness probe requests, with your domain:
  3. The Home page, from content/collections/pages/home.md:
  4. The control panel’s stylesheet, which php artisan statamic:install published during the build, linked over HTTPS:
  5. A sign-in to the control panel. The first request gets the sign-in page and its CSRF token, the second sends the email and the password; STATAMIC_PASSWORD is the password of the user in users/:
    A wrong password redirects back to /cp/auth/login instead.

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 Statamic with HTTPS on, so the site and the control panel use https:// URLs, and REMOTE_ADDR is the visitor’s address from the CF-Connecting-IP header rather than the proxy’s.
  • 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 and published assets, local 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 17 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, and its Dockerfile tab shows your file. When the deployment’s status is Deployed, the site, /up and the control panel’s sign-in answer as in Check from a terminal. Because the content folders belong to root, a save in the control panel fails with 500, and the runtime logs show file_put_contents(/app/content/collections/pages/home.md): Failed to open stream: Permission denied: edit the content in Git, as in Keep the content in Git. See Dockerfile.

Deploy changes

With Auto-deploy on, every push to the branch starts a deployment; otherwise, click Deploy now for each change. A content change is a commit like a code change: each deployment builds the image with the content in the repository, then replaces the web process’s instances once the new ones answer on /up. 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 and Statamic’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. Variables apply at the next deployment. To turn the control panel off on Sevalla, set CP_ENABLED to false, which config/statamic/cp.php reads. See Environment variables.

Add a custom domain

To serve the site 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.

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, 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 size

List process resource types (listProcessResourceTypesV3) gives the ID of H1:
CLI: sevalla resources process-types. MCP: get_platform_options.
3

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.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.
4

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.
5

Add the environment variables

Create environment variable (createEnvironmentVariableV3) adds one variable per call. The loop adds a new APP_KEY, APP_URL and SESSION_DRIVER, all for runtime only:
APP_KEY is 32 random bytes in Base64 with the base64: prefix, the format php artisan key:generate writes. The build reads none of the variables.CLI: sevalla apps env-vars create $APP_ID --key SESSION_DRIVER --value cookie, 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.
6

Deploy

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

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:
CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.
8

Read the build log

Get application deployment logs (getApplicationDeploymentLogsV3) returns the build log, oldest line first: the build plan from Verify the deployment, the build and the rollout. Without limit, it returns only the latest 1,000 lines. When the status is failed, the last lines say why; see Troubleshoot the deployment:
CLI: sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment_logs.
9

Turn on auto-deploy

Update application (updateApplicationV3), so every content change you push deploys:
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 $APP_HOST as the domain.