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 namedstorefront 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:
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 inrequire.php, so the build doesn’t change when a new Ingot release moves its default line:
Keep the content in Git
Statamic keeps entries, navigation, globals, taxonomies, users and blueprints as files incontent/, 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:
- Run the site locally with
php artisan serve, and sign in to the control panel athttp://localhost:8000/cp. - Edit the content. Statamic saves each change as a file in the project.
- Commit the files and push. The next deployment serves them.
Keep sessions in a cookie
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 runscomposer install --no-dev --optimize-autoloader. Its Composer scripts runphp artisan statamic:install, which publishes the control panel’s CSS and JavaScript topublic/vendor/statamic. Then Ingot runsnpm ciandnpm run build, which writes the site’s CSS and JavaScript topublic/build. - Start. The web process runs Ingot’s Laravel start script. It runs
php artisan optimizewith the deployment’s environment variables, then starts FrankenPHP on the port in thePORTenvironment variable (8080by default), servingpublic/. - HTTPS. FrankenPHP trusts the
X-Forwarded-Protoheader from Sevalla’s proxy, so Statamic sees HTTPS requests as HTTPS: the site and the control panel load their assets fromhttps://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/migrationshas 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.
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.


Verify the deployment
-
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 frompackage.json.Build log - Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running.
-
Click the domain under the application’s name, for example
storefront-x7k2m.sevalla.app. The Home page showsYour blank canvas is ready., styled by the CSS that Vite built. -
Add
/cpto the address. Statamic’s sign-in page opens over HTTPS. Sign in with the email and password of the user inusers/: the control panel’s dashboard opens.


Check from a terminal
These checks need no dashboard. The API calls useAPI, H, APP_ID and DEPLOYMENT_ID as set in Deploy with the API.
-
The deployment’s status, from Get deployment (
getDeploymentV3): -
The health route that the readiness probe requests, with your domain:
-
The Home page, from
content/collections/pages/home.md: -
The control panel’s stylesheet, which
php artisan statamic:installpublished during the build, linked over HTTPS: -
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_PASSWORDis the password of the user inusers/:A wrong password redirects back to/cp/auth/logininstead.
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 aDockerfile, 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
PORTand servespublic/. Files that start with a dot get404, and so does every PHP file exceptindex.php. - Requests from Sevalla’s proxy that arrived over HTTPS reach Statamic with
HTTPSon, so the site and the control panel usehttps://URLs, andREMOTE_ADDRis the visitor’s address from theCF-Connecting-IPheader 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
.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.
📦 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 inconfig/ 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 asshop.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
400and a message that starts withCannot access repositoryand says where to connect one. curl,jqandopenssl.
1
Choose the location
List clusters (CLI:
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:sevalla resources clusters. MCP: get_platform_options.2
Look up the size
List process resource types (CLI:
listProcessResourceTypesV3) gives the ID of H1:sevalla resources process-types. MCP: get_platform_options.3
Create the application
Create application (Sevalla adds the domain about 2 seconds after it creates the application, so the loop waits for it.CLI:
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 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 (CLI:
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: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 (CLI:
triggerDeploymentV3):sevalla apps deployments trigger $APP_ID --branch $BRANCH. MCP: trigger_deployment.7
Wait for the deployment
Poll Get deployment (CLI:
getDeploymentV3) until the status is final: success, failed, cancelled or skipped. The loop keeps polling through a failed request and stops after 30 minutes:sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.8
Read the build log
Get application deployment logs (CLI:
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:sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment_logs.9
Turn on auto-deploy
Update application (To build from the Dockerfile instead of Ingot, send
updateApplicationV3), so every content change you push deploys:{"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.$APP_HOST as the domain.