> ## 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 a Vapor application

> Deploy a Vapor 4 application in Swift to Sevalla from a Dockerfile on the official swift images, with PostgreSQL through Fluent and the migrations in a job before each deployment.

A Vapor application runs on Sevalla as an application built from a Dockerfile in your Git repository: the official `swift` image compiles it into one executable, and the web process runs it on the image's slim variant, which has the Swift runtime libraries but no compiler. [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's default builder, has no Swift provider, so you choose the Dockerfile build strategy before the first deployment: with Ingot, the build stops with `Ingot couldn't tell what kind of application this is`. This guide deploys the Vapor 4 template with Fluent and a Sevalla PostgreSQL database, and runs the Fluent migrations in a job before each deployment.

## At a glance

| | |
| - | - |
| Source | A Git repository with a `Dockerfile` |
| Build | The [Dockerfile](#deploy-with-a-dockerfile): `swift:6.4-noble` compiles the application, `swift:6.4-noble-slim` runs it. Ingot has no Swift provider |
| Start command | `/app/storefront serve`, from the `ENTRYPOINT` and `CMD` of the Dockerfile |
| Port | The `PORT` environment variable, `8080` by default, read in `configure.swift` |
| Processes | Web process; `Migrate` job (`/app/storefront migrate --yes`, **Before deployment**) |
| Services | PostgreSQL 18 over an internal connection |
| Disk | None |
| Health check | None |
| Size | **H1** for the web process and the job |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DATABASE_URL` | Yes | Runtime | Yes | `DB_URL` from the internal connection, renamed: `postgres://<user>:<password>@<host>:<port>/<database>` |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port |

The Dockerfile sets `VAPOR_ENV` to `production` in the image, so you don't add it on Sevalla.

## Before you begin

* A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See [Git providers](https://docs.sevalla.com/git/overview.md).
* Swift 6.4 on your computer, and the Vapor toolbox 20, which creates projects: `brew install vapor` on macOS. See [Vapor's installation guide](https://docs.vapor.codes/install/macos/) for Linux.
* Docker, to run the template's PostgreSQL database and the image locally.

## Create the project

Create a project named `storefront` with Fluent and PostgreSQL, without the Leaf templating library:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
vapor new storefront --fluent --fluent.db postgres --no-leaf
cd storefront
```

The template is a JSON API on Vapor 4: a `Todo` model, its `CreateTodo` migration, and routes to list, create and delete todos under `/todos`. `GET /` answers `It works!`. `vapor new` also creates a Git repository with a first commit, a `Dockerfile` and a `docker-compose.yml` with a PostgreSQL 18 service for local development.

Resolve the dependencies, which writes the exact version of each package to `Package.resolved`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
swift package resolve
```

The project builds on Vapor 4.122.3, Fluent 4.13.0 and the Fluent PostgreSQL driver 2.14.0. Commit `Package.resolved`: the Dockerfile builds exactly these versions.

## Prepare the project for Sevalla

Replace `Sources/storefront/configure.swift` with this version, which reads the port and the database URL that Sevalla sets:

```swift Sources/storefront/configure.swift theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import NIOSSL
import Fluent
import FluentPostgresDriver
import Vapor

/// configures your application
func configure(_ app: Application) async throws {
    // uncomment to serve files from /Public folder
    // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))

    app.http.server.configuration.hostname = "0.0.0.0"
    app.http.server.configuration.port = Environment.get("PORT").flatMap(Int.init(_:)) ?? 8080

    if let databaseURL = Environment.get("DATABASE_URL") {
        var postgres = try SQLPostgresConfiguration(url: databaseURL)
        // Sevalla's internal connection is private, and TLS can't verify the database's certificate
        postgres.coreConfiguration.tls = .disable
        app.databases.use(.postgres(configuration: postgres), as: .psql)
    } else {
        app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
            hostname: Environment.get("DATABASE_HOST") ?? "localhost",
            port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
            username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
            password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
            database: Environment.get("DATABASE_NAME") ?? "vapor_database",
            tls: .prefer(try .init(configuration: .clientDefault)))
        ), as: .psql)
    }

    app.migrations.add(CreateTodo())

    // register routes
    try routes(app)
}
```

| Change | Why Sevalla needs it |
| - | - |
| `hostname` and `port` | Vapor listens on `127.0.0.1:8080` unless told otherwise, which a container can't receive traffic on. The server now listens on all network interfaces, on the port in `PORT`, which Sevalla sets to the web process's port (`8080` by default). |
| `DATABASE_URL` | The internal connection gives the database's address as one URL. `SQLPostgresConfiguration(url:)` reads it, with the `postgres://` or `postgresql://` scheme. |
| `tls = .disable` | Vapor's default TLS mode, `prefer`, verifies the database's certificate, which fails on the internal connection with `CERTIFICATE_VERIFY_FAILED`. The connection stays on Sevalla's private network. |
| The `else` branch | The template's settings, unchanged, so `docker compose` still runs the application locally with its own database |

The rest of the project already has what Sevalla needs:

| File | Why Sevalla needs it |
| - | - |
| `Package.swift` and `Package.resolved` | The dependencies and their exact versions. The Dockerfile resolves them with `--force-resolved-versions`, which fails when `Package.resolved` doesn't match `Package.swift`. |
| `Sources/storefront/Migrations/CreateTodo.swift` | The migration that the `Migrate` job runs with `migrate --yes` |
| `Dockerfile` and `.dockerignore` | Build the image. See [Deploy with a Dockerfile](#deploy-with-a-dockerfile). |

## Deploy with a Dockerfile

Ingot has no Swift provider, so Sevalla builds this application from a Dockerfile. Replace the `Dockerfile` and the `.dockerignore` that `vapor new` wrote with these:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM swift:6.4-noble AS build
WORKDIR /build
COPY Package.swift Package.resolved ./
RUN swift package resolve --force-resolved-versions
COPY Sources ./Sources
COPY Tests ./Tests
RUN swift build -c release --product storefront \
  && cp "$(swift build -c release --show-bin-path)/storefront" /build/storefront

FROM swift:6.4-noble-slim
WORKDIR /app
COPY --from=build /build/storefront ./
ENV VAPOR_ENV=production
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app/storefront"]
CMD ["serve"]
```

| Line or choice | Why, and what changed from the generated Dockerfile |
| - | - |
| `swift:6.4-noble` | The official Swift 6.4 image on Ubuntu 24.04, the release Vapor's template builds on. A line tag, not a digest, so each build gets Swift's 6.4 patch releases and Ubuntu's security fixes. |
| `swift package resolve --force-resolved-versions` | Downloads the versions in `Package.resolved`, in a layer that Docker reuses until `Package.swift` or `Package.resolved` changes |
| One `swift build` for the dependencies and the code | Compiling the 35 packages in `Package.resolved` takes most of the build, about 5 minutes on Sevalla, and runs on every deployment. A separate layer of compiled dependencies doesn't help: in a test, SwiftPM compiled every package again when that layer came from Sevalla's build cache. The generated file uses a BuildKit cache mount instead, which doesn't help either, because each build starts on a fresh builder. |
| `--product storefront` | Builds the application, not the tests. `Tests` is copied because `Package.swift` declares the test target. |
| `swift:6.4-noble-slim` | The Swift 6.4 runtime libraries without the compiler, on the same Ubuntu release, so the executable runs without `--static-swift-stdlib`. The image has `/bin/sh` for the [web terminal](https://docs.sevalla.com/applications/web-terminal.md) and custom start commands, and Swift's backtracer, which prints a crash's stack trace to the runtime logs. |
| No `apt-get` | The generated file upgrades every system package and installs jemalloc. Rebuilds of the line tags bring the security fixes, and the application runs on the system allocator. |
| `COPY` without `--chown`, then `USER 1000:1000` | The server runs as user `1000` (`ubuntu` in this image). The executable stays owned by root, so the server can't change it. |
| `ENV VAPOR_ENV=production` | Vapor reads its environment from `--env` or `VAPOR_ENV`. In the image, every command runs in production, the web process and the `Migrate` job alike: Vapor hides error details from responses and logs at the `notice` level. |
| `ENTRYPOINT` and `CMD` | The web process runs `/app/storefront serve` as process 1, and Vapor's `serve` command handles `SIGTERM` itself, so the image needs no init process. A job's custom start command replaces both, so it names the executable. |
| `EXPOSE 8080` | Documents Sevalla's default `PORT`. `configure.swift` reads `PORT`. |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
.build
.swiftpm
.env*
*.log
```

The `.dockerignore` keeps the Git history, Swift's local build folders, local environment files and logs out of the build. The image serves no files from `Public`: if you turn on `FileMiddleware` in `configure.swift`, add `COPY Public ./Public` to the final stage.

To run the image locally, use the template's `docker-compose.yml`, which builds it and starts PostgreSQL:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
docker compose build
docker compose up db -d
docker compose run --rm migrate
docker compose up app -d
curl -s -w '\n' http://localhost:8080/
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
It works!
```

The final image is about 560 MB, the executable 118 MB of it. Run `docker compose down` to stop both containers. Commit everything and push it to your Git provider:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
git add .
git commit -m "Prepare for Sevalla"
git branch -M main
git remote add origin https://github.com/acme/storefront.git
git push -u origin main
```

## Deploy to Sevalla

These steps build from the Dockerfile in [Deploy with a Dockerfile](#deploy-with-a-dockerfile). A new application builds with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), which can't build a Swift project, so choose the Dockerfile build strategy before you deploy.

**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](https://docs.sevalla.com/databases/create.md) 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. Choose the Dockerfile build strategy**

Click the **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**.

**4. Connect the database**

Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `orders-db` as the **Service** and turn on **Add environment variables to the application**. Clear **Available during build**, rename `DB_URL` to `DATABASE_URL`, the name `configure.swift` reads, then click **Add internal connection**. See [Connect an application](https://docs.sevalla.com/databases/connect.md#connect-an-application).

**5. Create the migration job**

Click the **Processes** tab, then click **Create process**. Choose **Job**, enter `Migrate` as the **Name** and `/app/storefront migrate --yes` 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](https://docs.sevalla.com/applications/deployments.md#deploy-automatically-on-push).

![The Create process sheet with Job selected, the Name and Custom start command fields, the Start policy set to Before deployment, and the H1 resource](https://assets.sevalla.com/screenshots/light/app-processes-job-create.png)

`--yes` answers Vapor's `Would you like to continue?` prompt, which a job can't answer. The job runs once per deployment, before the new version starts; when it fails, the deployment fails and the previous version keeps running.

## Verify the deployment

1. On the **Deployments** tab, click the deployment. Its **Build log** starts the build with `📦 Building and pushing docker image using Dockerfile...`, then shows `⏩ Running Migrate` and `✅ Migrate has been successful`. The first deployment takes about 7 minutes. The deployment's **Dockerfile** tab shows the file the image was built from.
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 page shows `It works!`

![The application header with its name, Running status, repository, branch and the domain that opens the application](https://assets.sevalla.com/screenshots/light/app-overview-address.png)

The application's [runtime logs](https://docs.sevalla.com/applications/logs.md) show `[ NOTICE ] Server started on http://0.0.0.0:8080`. If the deployment fails, read its build log and the `Migrate` process's runtime logs, then see [Troubleshoot](#troubleshoot).

To check the deployment from a terminal or an AI agent, read its `status` with [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until it's `success`, and its build log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`), as in [Deploy with the API](#deploy-with-the-api). Then create a todo and list them. Each todo gets a random `id`, which `jq` leaves out:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://storefront-x7k2m.sevalla.app/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Ship order 1001"}' | jq -c '{title}'
curl -s https://storefront-x7k2m.sevalla.app/todos | jq -c 'map(.title)'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"title":"Ship order 1001"}
["Ship order 1001"]
```

The todo is in PostgreSQL, in the `todos` table that the `Migrate` job created. [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) with `search` finds the job's result:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$APP_ID/runtime-logs?search=Migration%20successful" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Migration successful
```

On later deployments, the job logs `No new migrations.` until you add one.

## Shut down gracefully

When Sevalla stops an instance, during a deployment, a restart or a scale-down, it waits 5 seconds, sends `SIGTERM`, and stops the instance if it's still running 30 seconds after the shutdown began. See [How processes run](https://docs.sevalla.com/applications/processes.md#how-processes-run).

Vapor's `serve` command handles `SIGTERM`: the server stops accepting connections, waits up to 10 seconds for the requests in progress (`app.http.server.configuration.shutdownTimeout`), and the process exits with code `0`. In production, Vapor logs the shutdown at the `debug` level, so the runtime logs show no line for it. To see it, set the `LOG_LEVEL` environment variable to `debug`.

## 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. When only your code changes, the build log shows `CACHED` for the `swift package resolve` step, and `swift build` compiles the dependencies and your code again: a code change takes about as long to deploy as the first build. For a schema change, add a migration to `Sources/storefront/Migrations`, register it with `app.migrations.add` in `configure.swift`, and commit both. When you add a package, commit the updated `Package.resolved`. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Add configuration on the application's **Environment variables** tab and read it with `Environment.get`, as `configure.swift` reads `PORT` and `DATABASE_URL`. Variables apply at the next deployment. Vapor also reads `LOG_LEVEL`, for example `debug` or `info`. Variables marked **Available during buildtime** arrive in the Docker build as build arguments, which a stage reads only after an `ARG` line for each one. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## 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. Custom domains need a web process larger than **H1**, so [scale](https://docs.sevalla.com/applications/scaling.md) the web process first. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| The build log shows `Ingot couldn't tell what kind of application this is`, followed by `Ingot looks for a manifest such as package.json, requirements.txt or go.mod at the root.` | The application builds with Ingot, the default build strategy, which has no Swift provider | Choose the Dockerfile build strategy, as in [Deploy to Sevalla](#deploy-to-sevalla), then deploy |
| The deployment fails at `❌ Migrate has failed`, and the `Migrate` logs show `error:1000007d:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED`, then `Program crashed: Illegal instruction` | Vapor's default TLS mode tries TLS with the database and can't verify its certificate | Turn TLS off for `DATABASE_URL`, as in `configure.swift` in [Prepare the project for Sevalla](#prepare-the-project-for-sevalla) |
| The deployment fails at `❌ Migrate has failed` for another reason | The job can't reach the database, or `DATABASE_URL` is missing | On the **Logs** tab, choose the `Migrate` process to read its output. Check that the internal connection exists and that `DATABASE_URL` is **Available during runtime**. |
| The first Dockerfile build logs `ERROR: failed to configure registry cache importer:`, followed by the application's image and `:cache: not found` | The application has no build cache yet | Nothing: the build continues, and the next builds use the cache it saves |

A Swift program that stops on an error prints `Program crashed:` and a stack trace for each thread to the runtime logs, which takes Swift's backtracer about 20 seconds on **H1**. Read the lines before it for the error. For more causes, see [Troubleshoot builds and deployments](https://docs.sevalla.com/applications/troubleshooting/builds-and-deployments.md).

## Deploy with the API

These calls make the same deployment as [Deploy to Sevalla](#deploy-to-sevalla), in an order that a script or an AI agent runs as it is. Each step names the [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) operation, then the [CLI](https://docs.sevalla.com/developer-tools/cli.md) command and the [MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool that do the same. You need:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) with the **Company developer** role, or the `APP:CREATE`, `APP:READ`, `APP:UPDATE`, `DATABASE:CREATE`, `DATABASE:READ` and `DATABASE:UPDATE` permissions, in the `SEVALLA_API_KEY` environment variable. The CLI reads the key from `SEVALLA_API_TOKEN`.
* The repository on a Git provider that's connected to your Sevalla account. See [Git providers](https://docs.sevalla.com/git/overview.md). Creating the application checks it: when Sevalla can't read the repository, the call returns `400` with `Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access.`, followed by where to grant access.
* `curl`, `jq` and `openssl`.

**1. Choose the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns the locations your company can use, each with its `id`, the `display_name` that the dashboard shows and its region ID in `location`. Keep the ID of the one you want by its region ID, such as `eu-frankfurt-1` for Frankfurt, Germany. [Locations](https://docs.sevalla.com/platform/locations.md) lists them all. The database and the application must be in the same location:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CLUSTER_ID=$(curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Find the newest PostgreSQL version**

[List database versions](https://api-docs.sevalla.com/v3/resources/list-database-versions) (`listDatabaseVersionsV3`) returns the versions you can create in the location:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PG_VERSION=$(curl -s "https://api.sevalla.com/v3/resources/database-versions?cluster_id=$CLUSTER_ID&type=postgresql" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[0].versions | max_by(tonumber)')
echo "$PG_VERSION"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
18
```

CLI: `sevalla resources db-versions --cluster $CLUSTER_ID --type postgresql`. MCP: `create_database` picks the newest version when you leave `version` out.

**3. Find the DB1 size**

[List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) returns the database sizes by `name`. **DB1**, the smallest, is the dashboard's default:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_SIZE_ID=$(curl -s https://api.sevalla.com/v3/resources/database-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "db1") | .id')
```

CLI: `sevalla resources db-types`. MCP: `get_platform_options`.

**4. Create the database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`) needs a password: generate one and keep it, because [Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) doesn't return it. The call returns at once, with the status `creating`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_PASSWORD=$(openssl rand -hex 16)
DB_ID=$(curl -s -X POST https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"orders-db","type":"postgresql","version":"'"$PG_VERSION"'","cluster_id":"'"$CLUSTER_ID"'","resource_type_id":"'"$DB_SIZE_ID"'","db_name":"orders","db_user":"storefront","db_password":"'"$DB_PASSWORD"'"}' \
  | jq -r .id)
```

CLI: `sevalla databases create --name orders-db --type postgresql --db-version $PG_VERSION --cluster $CLUSTER_ID --resource-type $DB_SIZE_ID --db-name orders --db-user storefront --db-password $DB_PASSWORD`. MCP: `create_database` with `connect_to_application_id` and `env_var_name` set to `DATABASE_URL` also connects the application and stores the URL, with the scheme `postgresql://`, which `configure.swift` accepts as well as `postgres://`.

**5. Wait for the database**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) returns the status, `creating` and then `ready`, and the internal hostname and port that the application connects to. Poll it for up to 10 minutes:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 60); do
  DB=$(curl -s https://api.sevalla.com/v3/databases/$DB_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
  STATUS=$(echo "$DB" | jq -r '.status // empty')
  [ "$STATUS" = ready ] && break
  sleep 10
done
echo "$STATUS"
DB_HOST=$(echo "$DB" | jq -r .internal_hostname)
DB_PORT=$(echo "$DB" | jq -r .internal_port)
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
ready
```

CLI: `sevalla databases get $DB_ID`. MCP: `get_database`.

**6. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with a web process. It isn't deployed yet:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"storefront","cluster_id":"'"$CLUSTER_ID"'","source":"privateGit","git_type":"github","repo_url":"https://github.com/acme/storefront","default_branch":"main"}' \
  | jq -r .id)
```

CLI: `sevalla apps create --name storefront --cluster $CLUSTER_ID --source privateGit --git-type github --repository https://github.com/acme/storefront --branch main`. MCP: `create_application` with `deploy` set to `false`.

**7. Choose the Dockerfile build strategy**

The application builds with Ingot (`build_type` is `ingot`), which can't build a Swift project. [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches it to the Dockerfile at the root of the repository:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}' \
  | jq -c '{build_type, dockerfile_path, docker_context}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}
```

CLI: `sevalla apps update $APP_ID --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'`. MCP: `sevalla_api_update` with `updateApplicationV3` and the same body.

**8. Find the domain**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the domain that Sevalla gives the application. It appears a few seconds after the application is created, so ask until it's there:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 24); do
  HOST=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/domains \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[]? | select(.is_primary) | .name')
  [ -n "$HOST" ] && break
  sleep 5
done
echo "$HOST"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
storefront-x7k2m.sevalla.app
```

CLI: `sevalla apps domains list --app-id $APP_ID`. MCP: `get_application`.

**9. Connect the database**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`) connects the application to the database over the private network and returns the connection's `id`. Unlike the dashboard, it doesn't add environment variables:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/internal-connections \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_id":"'"$DB_ID"'","target_type":"database"}' | jq -c 'keys'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
["id"]
```

CLI: `sevalla apps internal-connections create $APP_ID --target-id $DB_ID --target-type database`. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**10. Add the database URL**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds `DATABASE_URL` in the format of the dashboard's internal connection, available at runtime only:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/env-vars \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -nc --arg value "postgres://storefront:$DB_PASSWORD@$DB_HOST:$DB_PORT/orders" '{key: "DATABASE_URL", value: $value, is_runtime: true, is_buildtime: false}')" \
  | jq -c '{key, is_runtime, is_buildtime}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"key":"DATABASE_URL","is_runtime":true,"is_buildtime":false}
```

CLI: `sevalla apps env-vars create $APP_ID --key DATABASE_URL --value postgres://storefront:$DB_PASSWORD@$DB_HOST:$DB_PORT/orders`; it always makes a variable available during the build and runtime. MCP: `set_environment_variables` with `buildtime` set to `false`.

**11. Find the web process**

[List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) returns the web process that the application was created with:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.data[] | select(.type == "web") | .id')
```

CLI: `sevalla apps processes list --app-id $APP_ID`. MCP: `get_application`.

**12. Find the H1 size**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) returns the sizes by `name`, in lowercase:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
H1_ID=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "h1") | .id')
```

CLI: `sevalla resources process-types`. MCP: `get_platform_options`.

**13. Create the migration job**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`) creates the `Migrate` job. The API needs the size, a `scaling_strategy` and the `job_start_policy`, which the dashboard fills in for you:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"Migrate","type":"job","entrypoint":"/app/storefront migrate --yes","resource_type_id":"'"$H1_ID"'","scaling_strategy":{"type":"manual","config":{"instanceCount":1}},"job_start_policy":"beforeDeployment"}' \
  | jq -c '{display_name, type, entrypoint, job_start_policy, resource_type_name}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"display_name":"Migrate","type":"job","entrypoint":"/app/storefront migrate --yes","job_start_policy":"beforeDeployment","resource_type_name":"h1"}
```

CLI: `sevalla apps processes create $APP_ID --name Migrate --type job --command "/app/storefront migrate --yes" --resource-type $H1_ID --job-start-policy beforeDeployment`. MCP: `sevalla_api_create` with `createProcessV3` and the same body.

**14. Set the size**

The API creates the web process at **S1**, where the dashboard's default is **H1**. [Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets the size:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resource_type_id":"'"$H1_ID"'"}' \
  | jq -r .resource_type_name
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
h1
```

CLI: `sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID`. MCP: `scale_process` with `size_id`.

**15. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) turns on automatic deployments for every push to the branch:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_deploy":true}' | jq .auto_deploy
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
true
```

CLI: `sevalla apps update $APP_ID --data '{"auto_deploy":true}'`. MCP: `sevalla_api_update` with `updateApplicationV3`.

**16. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) deploys the latest commit on the branch:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/deployments \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"branch":"main"}' | jq -r .id)
```

CLI: `sevalla apps deployments trigger $APP_ID --branch main`. MCP: `trigger_deployment`.

**17. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`: `waiting` and `inProgress` while it runs, then `success`, `failed`, `cancelled` or `skipped`. Poll it until the status is final, for up to 30 minutes. The first deployment takes about 7 minutes:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 120); do
  STATUS=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')
  case $STATUS in success|failed|cancelled|skipped) break ;; esac
  sleep 15
done
echo "$STATUS"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
success
```

CLI: `sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID`. MCP: `get_deployment`.

**18. Read the build log**

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, oldest line first: the checkout, the Docker build, the `Migrate` job and the rollout. When the status is `failed`, the last lines say why. Without `limit`, it returns the latest 1,000 lines. With `search`, it finds the job's result. The line has terminal color codes, which `gsub` removes:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID/logs?limit=5000&search=Migrate%20has" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message | gsub("\u001b\\[[0-9;]*m"; "")'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
✅ Migrate has been successful
```

CLI: `sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID --limit 5000`. MCP: `get_deployment_logs`.

**19. Check the application**

Request the home page from the application's domain:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -w '\n' https://$HOST/
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
It works!
```

Then run the other checks in [Verify the deployment](#verify-the-deployment).


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