Skip to main content
A CodeIgniter 4 application runs on Sevalla as an application built from your Git repository: Ingot builds it as a PHP application, installs the Composer packages and the PHP extensions the project requires, and starts FrankenPHP in the web process, which serves public/ on the port that Sevalla sets in the PORT environment variable. A job runs CodeIgniter’s migrations before each deployment, and the data lives in a Sevalla PostgreSQL database.

At a glance

Environment variables

CI_ENVIRONMENT isn’t set, so CodeIgniter runs in its production environment: it hides error details from visitors and logs only errors.

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 with the intl and pgsql extensions, and Composer 2, on your computer. CodeIgniter 4.7 needs PHP 8.2 or later.

Create the project

Create a CodeIgniter project from the app starter:
The app starter installs CodeIgniter 4.7 and its .gitignore, which keeps vendor/, the local .env file and the files in writable/ out of the repository. Add a products table. Create a migration:
Spark writes a file in app/Database/Migrations/ whose name starts with the current date and time. Replace its contents with a migration that creates the table and adds one product:
app/Database/Migrations/2026-10-30-075934_CreateProducts.php
Add a model for the table:
app/Models/ProductModel.php
Add a controller that returns the products as JSON:
app/Controllers/Products.php
Then add two routes at the end of app/Config/Routes.php: /products, and /catalog, which redirects to it:
app/Config/Routes.php
CodeIgniter builds the redirect’s address from its base URL, so /catalog shows whether the application knows its own domain.

Prepare the project for Sevalla

Pin the PHP version and the PostgreSQL extension

  • require.php pins the PHP line: Ingot builds on PHP 8.5 and follows its patch releases. The app starter’s ^8.2 would build on Ingot’s default line, which a new Ingot release can move. See Pin the PHP version.
  • CodeIgniter’s PostgreSQL driver, Postgre, uses PHP’s pgsql extension. CodeIgniter doesn’t require it in its own composer.json, so Ingot installs it only when your composer.json does.

Connect to the database from environment variables

CodeIgniter reads its database settings from app/Config/Database.php. In the class’s constructor, after parent::__construct();, point the default connection group at PostgreSQL with the variables that Sevalla adds when you connect the database:
app/Config/Database.php
Keep the rest of the constructor as it is. On your computer, put the same variables in a .env file, which CodeIgniter loads and Git ignores.

Set the base URL

CodeIgniter builds the URLs of site_url(), base_url() and redirects from app.baseURL, which is http://localhost:8080/ in app/Config/App.php. CodeIgniter reads an environment variable for any setting in a configuration class, with the dot written as an underscore, so you set app_baseURL on Sevalla to the application’s address and leave the file as it is.

Send the logs to the runtime logs

CodeIgniter writes its log to files in writable/logs/, which Sevalla resets on every deployment. Write it to PHP’s error log instead, which FrankenPHP prints to the runtime logs. In app/Config/Logger.php, import ErrorlogHandler in place of FileHandler, and replace the $handlers array:
app/Config/Logger.php
In production, the $threshold in the same file logs errors and the levels above them.

What Ingot reads

How Ingot builds and starts the project

The project needs no build or start configuration. Ingot doesn’t recognize CodeIgniter by name, and doesn’t need to:
  • Runs composer install --no-dev --optimize-autoloader and installs the intl and pgsql extensions.
  • Starts FrankenPHP, which serves public/ on PORT (8080 by default). A request for a path without a file of its own goes to index.php, which runs CodeIgniter’s router. Files that start with a dot, such as .htaccess, are never served.
  • Passes HTTPS and the visitor’s IP address from Sevalla’s proxy to PHP: FrankenPHP sets HTTPS for requests that reached Sevalla over HTTPS, and REMOTE_ADDR to the visitor’s address.
  • Lets the application write to its own folder, so CodeIgniter’s cache, sessions and debug files work in writable/. What it writes there lasts until the next deployment and stays on the instance that wrote it.
  • Doesn’t run migrations: you create a job for them.
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, click PostgreSQL and choose version 18. Enter storefront-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 storefront-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 first five of which app/Config/Database.php reads. See Connect an application.
4

Add the base URL

Click the Environment variables tab, then click Add environment variables. Enter app_baseURL as the Key and the application’s URL with a trailing slash as the Value, for example https://storefront-x7k2m.sevalla.app/. The domain is under the application’s name in the page header. Click Add environment variables.
5

Create the migration job

Click the Processes tab, then click Create process. Choose Job, enter Migrate as the Name and php spark migrate --all as the Custom start command. Keep the Start policy at Before deployment and the Resource at H1, then click Create process.
6

Deploy

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

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, click the deployment. Its Build log shows Ingot’s build plan: PHP 8.5 from composer.json, the pgsql and intl extensions and where each comes from, and public/ as the document root.
    Build log
    Later, during the rollout, the build log shows Migrate has been successful. If the job fails, the deployment fails and nothing changes for visitors.
  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. CodeIgniter’s welcome page opens, and its footer shows Environment: production. Add /products to the address: the page shows the product that the migration added.
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 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 migration job’s output, from Get application runtime logs (getApplicationRuntimeLogsV3), which searches the last hour by default:
  4. The products, read from PostgreSQL, with your domain:
  5. The redirect from /catalog, whose address CodeIgniter builds from app_baseURL:
  6. The environment, from the welcome page:

Read the logs

With the ErrorlogHandler from Send the logs to the runtime logs, every message CodeIgniter logs appears on the application’s Logs tab. For example, when the application can’t reach the database, /products returns 500 and the runtime logs show:
Runtime logs
In production, CodeIgniter logs error and the levels above it, critical, alert and emergency. To log warning, notice, info or debug messages from log_message() too, raise $threshold in app/Config/Logger.php. 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, and runs no PHP file but index.php:
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 CodeIgniter with HTTPS on, and REMOTE_ADDR is the visitor’s address from the CF-Connecting-IP header rather than the proxy’s.
.dockerignore
The .dockerignore keeps installed packages, your local .env, the files CodeIgniter wrote to writable/ on your computer and the tests out of the build. The index.html files in writable/ stay, so its folders exist in the image. 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 11 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, /products and /catalog answer as in Check from a terminal. The Migrate job 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. For a schema change, create a migration with php spark make:migration and commit it with the code. When you add a package, commit composer.lock with it. See Deployments.

Use environment variables

CodeIgniter reads an environment variable for any property of a configuration class in app/Config/: the class name in lower case, an underscore, then the property, as in app_baseURL for $baseURL in App.php or email_SMTPHost for $SMTPHost in Email.php. Add them on the application’s Environment variables tab instead of committing a .env file. Changes 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_baseURL 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 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

List database resource types (listDatabaseResourceTypesV3) and List process resource types (listProcessResourceTypesV3) give the IDs of DB1 and H1:
CLI: sevalla resources db-types and sevalla resources process-types. MCP: get_platform_options.
3

Create the database

Create database (createDatabaseV3) with PostgreSQL 18. 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 storefront-db --type postgresql --db-version 18 --cluster $CLUSTER_ID --resource-type $DB1_ID --db-name storefront --db-user storefront --db-password $DB_PASSWORD. MCP: not available for this step. create_database generates a password that it never returns, and app/Config/Database.php needs it in DB_PASSWORD: run this call with the API or the CLI.
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_baseURL:
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, and app_baseURL would be https:///.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

The API creates the web process at S1, not the dashboard’s H1. List processes (listProcessesV3) finds it, and Update process (updateProcessV3) sets H1:
CLI: sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID. MCP: scale_process with size_id and apply_now: false.
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 app_baseURL, all for runtime only:
It prints each of the 7 keys as it’s added. The build reads none of them.CLI: sevalla apps env-vars create $APP_ID --key app_baseURL --value https://$APP_HOST/, 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 spark migrate --all" --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:
CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.
11

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, the Migrate job 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.
12

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 $APP_HOST as the domain.