> ## 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 Litestar application

> Deploy a Litestar 2.25 API to Sevalla with Uvicorn: PostgreSQL through Advanced Alchemy, migrations in a job, a health check, and Ingot or your own Dockerfile.

A Litestar API runs on Sevalla as an application built from your Git repository: [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's builder, detects Litestar and starts it with Uvicorn in the web process, and a job runs the database migrations with Litestar's CLI before each deployment. Its data lives in a Sevalla PostgreSQL database that the application reaches over an internal connection, through Advanced Alchemy, Litestar's SQLAlchemy integration.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Python 3.14, pip), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `uvicorn app:app --host 0.0.0.0 --port "$PORT"`, which Ingot detects from `app = Litestar(...)` in `app.py` |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process; `Migrate` job (`litestar database upgrade --no-prompt`, **Before deployment**) |
| Services | PostgreSQL 18 over an internal connection |
| Disk | None |
| Health check | `GET /healthz` returns `200` with `ok` (readiness probe) |
| Size | **H1** for the web process and the job, **DB1** for the database |

### Environment variables

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

The internal connection also adds `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD`, which the project doesn't read.

## 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).
* Python 3.14 on your computer.

## Create the project

Create a virtual environment and install Litestar with its CLI, Uvicorn and Advanced Alchemy, and the database drivers:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
mkdir storefront && cd storefront
python3 -m venv .venv
source .venv/bin/activate
pip install "litestar[standard,sqlalchemy]" "psycopg[binary]" aiosqlite
```

| Package | What it does |
| - | - |
| `litestar[standard,sqlalchemy]` | Litestar 2.25 with the `litestar` command, Uvicorn, and Advanced Alchemy 1.12, which brings SQLAlchemy 2.1 and Alembic |
| `psycopg[binary]` | The PostgreSQL driver, psycopg 3, which SQLAlchemy uses with `asyncio` too |
| `aiosqlite` | The SQLite driver the project uses on your computer, without a PostgreSQL server |

Keep local files out of the repository:

```text .gitignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.venv/
__pycache__/
.env
*.db
```

Create the application, a JSON API with a `Product` model:

```python app.py theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import os
import re
from dataclasses import dataclass

from advanced_alchemy.extensions.litestar import (
    AlembicAsyncConfig,
    AsyncSessionConfig,
    SQLAlchemyAsyncConfig,
    SQLAlchemyPlugin,
    base,
)
from litestar import Litestar, get, post
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped


class Product(base.BigIntBase):
    __tablename__ = "product"
    name: Mapped[str]


@dataclass
class NewProduct:
    name: str


def database_url() -> str:
    url = os.environ.get("DB_URL", "sqlite+aiosqlite:///storefront.db")
    # Sevalla's URLs start with postgres:// or postgresql://; use psycopg 3.
    return re.sub(r"^postgres(ql)?://", "postgresql+psycopg://", url)


db_config = SQLAlchemyAsyncConfig(
    connection_string=database_url(),
    session_config=AsyncSessionConfig(expire_on_commit=False),
    alembic_config=AlembicAsyncConfig(script_location="migrations"),
)


@get("/")
async def index(db_session: AsyncSession) -> dict[str, int]:
    count = await db_session.scalar(select(func.count()).select_from(Product))
    return {"products": count}


@post("/products")
async def create_product(data: NewProduct, db_session: AsyncSession) -> dict[str, int | str]:
    product = Product(name=data.name)
    db_session.add(product)
    await db_session.commit()
    return {"id": product.id, "name": product.name}


@get("/healthz")
async def healthz() -> str:
    return "ok"


app = Litestar(
    route_handlers=[index, create_product, healthz],
    plugins=[SQLAlchemyPlugin(config=db_config)],
)
```

* `DB_URL` is the variable Sevalla adds when you connect the database. Its URL starts with `postgres://`, which SQLAlchemy doesn't accept, so `database_url()` switches the scheme to `postgresql+psycopg://`, SQLAlchemy's name for psycopg 3. Without `DB_URL`, the project uses SQLite on your computer.
* `SQLAlchemyPlugin` gives every handler a database session in its `db_session` argument, and adds the `litestar database` commands, which run Alembic with the same connection.

Create the migrations folder, generate the first migration from the model, apply it to the local database and start the development server:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
litestar database init --no-prompt
litestar database make-migrations --no-prompt -m "Create product table"
litestar database upgrade --no-prompt
litestar run
```

`litestar database init` writes `alembic.ini` and the `migrations` folder, and `litestar run` finds the application in `app.py`. Open `http://127.0.0.1:8000`: the response is `{"products":0}`, and the interactive API docs are at `http://127.0.0.1:8000/schema`. Press `Ctrl+C` to stop the server.

## Prepare the project for Sevalla

### Pin the Python version

Ingot builds on Python 3.14, its default line, unless the project sets a version. Pin the version you develop with in `.python-version`, so the build doesn't change when a new Ingot release moves its default line:

```text .python-version theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
3.14
```

See [Pin the Python version](https://docs.sevalla.com/guides/python/overview.md#pin-the-python-version).

### List the dependencies

Ingot installs the packages in `requirements.txt` with pip, into a virtual environment at `/app/.venv` that's on the `PATH`, so list them with the versions you tested:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
pip freeze > requirements.txt
```

### Check the start command

The project needs no start command. Ingot finds `app = Litestar(...)` in `app.py` and Uvicorn in `requirements.txt`, and starts the web process with `uvicorn app:app --host 0.0.0.0 --port "$PORT"`, which the build plan shows in its **Start** row.

* Uvicorn listens on all interfaces, on the port in the `PORT` environment variable. Sevalla sets `PORT` to the web process's port, `8080` by default.
* Requests reach the application through Sevalla's proxy. Ingot's image sets `FORWARDED_ALLOW_IPS=*`, so Uvicorn trusts the `X-Forwarded-Proto` and `X-Forwarded-For` headers, and Litestar sees the `https` scheme and the visitor's address. See [Request headers](https://docs.sevalla.com/applications/http-headers.md).
* Uvicorn runs one worker process in each instance. On a larger size, set the `WEB_CONCURRENCY` environment variable to run more, for example `2`. See [Scaling](https://docs.sevalla.com/applications/scaling.md).
* Migrations don't run here. A [job](https://docs.sevalla.com/applications/processes.md#jobs) runs them once per deployment, with `litestar database upgrade --no-prompt`. Without `--no-prompt`, the command asks for confirmation, and with no terminal to answer it the job fails.
* Ingot recommends the same job. It doesn't create it, but the deployment's **Dockerfile** tab lists it under `Run these from the same image too:` as `litestar --app app:app database upgrade --no-prompt`, and [Get Dockerfile](https://api-docs.sevalla.com/v3/applications/get-dockerfile) returns it in `processes`. `--app app:app` names the application, which Litestar's CLI also finds in `app.py` on its own, so both commands migrate the same database.

Ingot looks for the application in `main.py`, `app.py`, `app/main.py` and other common files. To start it another way, set the web process's start command; see [Set the start command](https://docs.sevalla.com/applications/builds/ingot.md#set-the-start-command).

| File | Why Sevalla needs it |
| - | - |
| `app.py` | Creates the Litestar application that Ingot finds and Uvicorn serves, `app:app`. |
| `requirements.txt` | Ingot installs the exact versions in it, and reads Litestar and Uvicorn from it to choose the start command. |
| `.python-version` | Ingot builds on the Python line it names, 3.14. |
| `alembic.ini`, `migrations/` | The migrations that the `Migrate` job applies. |

Commit everything and push it to your Git provider:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
git init
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 with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md). To build from your own Dockerfile instead, see [Deploy with a Dockerfile](#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](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. 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_URL` and the other connection variables to the application. See [Connect an application](https://docs.sevalla.com/databases/connect.md#connect-an-application).

**4. Create the migration job**

Click the **Processes** tab, then click **Create process**. Choose **Job**, enter `Migrate` as the **Name** and `litestar database upgrade --no-prompt` as the **Custom start command**. Keep the **Start policy** at **Before deployment** and the **Resource** at **H1**, then click **Create process**.

**5. Deploy**

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

**6. 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)

## Verify the deployment

1. On the **Deployments** tab, click the deployment. Its **Build log** shows Ingot's [build plan](https://docs.sevalla.com/applications/builds/ingot.md#read-the-build-plan): Python 3.14 from `.python-version`, pip from `requirements.txt`, and Litestar with the Uvicorn command for `app:app`.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Litestar · Python 3.14 ─────────────────────────────────────────────────────────────╮
   │ Python           3.14                                           .python-version "3.14" │
   │ Package manager  pip                                            requirements.txt       │
   │ Framework        Litestar                                       app.py                 │
   │ Start            uvicorn app:app --host 0.0.0.0 --port "$PORT"  Litestar               │
   ├─ Image ────────────────────────────────────────────────────────────────────────────────┤
   │ Build            python:3.14-slim-trixie                                               │
   │ Runtime          python:3.14-slim-trixie                                               │
   │ User             1000:1000                                                             │
   │ Init             tini                                                                  │
   │ Port             $PORT, default 8080                                                   │
   ╰───────────────────────────────────────────────────────────────────────── Ingot v0.5.0 ─╯
   ```

   After the build, the log shows `Migrate has been successful`, then `Web has been deployed successfully`. 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. Count the products, add one and count again, with your application's domain. The responses come from the PostgreSQL database:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://storefront-x7k2m.sevalla.app/; echo
   curl -s -X POST https://storefront-x7k2m.sevalla.app/products -H "Content-Type: application/json" -d '{"name":"Coffee mug"}'; echo
   curl -s https://storefront-x7k2m.sevalla.app/; echo
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   {"products":0}
   {"id":1,"name":"Coffee mug"}
   {"products":1}
   ```

   The interactive API docs are at `/schema`, and the OpenAPI document at `/schema/openapi.json`.
4. To check the deployment through the API, use `APP_ID` and `DEPLOYMENT_ID` from [Deploy with the API](#deploy-with-the-api). [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the status, and [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, which `search` filters:

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

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   success
   ✅ Migrate has been successful
   🚀 Web has been deployed successfully.
   ```

   The messages carry terminal color codes around the text. The job's own output is in the runtime logs: [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) with `search=Running%20upgrade` returns a line that ends with `Running upgrade  -> 6bb629e5f505, Create product table`, with your migration's revision. The CLI has `sevalla apps deployments get`, `sevalla apps deployments logs` and `sevalla apps logs runtime`, and the MCP server `get_deployment`, `get_deployment_logs` and `search_runtime_logs`.

If the deployment fails, read its build log, then see [Troubleshoot](#troubleshoot).

## Add a health check

Without a readiness probe, a new instance receives traffic as soon as its container starts, before Uvicorn listens. Point the probe at `/healthz`, the handler in `app.py` that returns `ok` without touching the database, so a slow database doesn't take every instance out of service:

**1. Open the web process**

On the application's **Processes** tab, click **⋯** on the **Web** process, then click **Update process**.

**2. Turn on the readiness probe**

Under **Health checks**, turn on **Readiness probe** and set the **Path** to `/healthz`. Keep the **Port** at the web process's port, `8080` unless you changed it.

**3. Save and deploy**

Click **Update process**, then click **Deploy** in the undeployed changes bar at the bottom of the page.

The deployment reaches **Deployed** once the new instance passes the probe. See [Health checks](https://docs.sevalla.com/applications/processes.md#health-checks). [Deploy with the API](#deploy-with-the-api) sets the same probe. [Get process](https://api-docs.sevalla.com/v3/applications/get-process) (`getProcessV3`) returns it in `readiness_probe`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -c .readiness_probe
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"httpGet":{"path":"/healthz","port":8080,"scheme":"HTTP"},"initialDelaySeconds":0,"periodSeconds":10,"timeoutSeconds":1,"successThreshold":1,"failureThreshold":3}
```

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. Add a `Dockerfile` and a `.dockerignore` to the root of the repository:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM python:3.14-slim-trixie

ENV PYTHONUNBUFFERED=1 \
    PORT=8080 \
    FORWARDED_ALLOW_IPS="*"

RUN useradd --uid 1000 --create-home app
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt

COPY . .
RUN python -m compileall -q .

USER 1000:1000
EXPOSE 8080
CMD ["sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port \"$PORT\""]
```

| Line or choice | Why |
| - | - |
| `python:3.14-slim-trixie` | The Python line that `.python-version` pins, on Debian 13, as Ingot builds it. A line tag picks up each patch release and security fix when you deploy again; a digest would stay on one release. |
| One stage | pip installs prebuilt wheels, `psycopg[binary]` with its own PostgreSQL client library, so the build needs no compiler and leaves nothing to clean up. |
| `requirements.txt` before the code | Docker reuses the installed packages until `requirements.txt` changes, so a code change rebuilds only the last layers. |
| `pip install` into the image's Python | The image runs one application, so it needs no virtual environment, and the `Migrate` job finds the `litestar` command on the image's `PATH`. `--root-user-action=ignore` silences pip's warning about installing as root. |
| `compileall` | Compiles the code and the migrations at build time, because the application can't write to `/app` when it runs. |
| `useradd` and `USER 1000:1000` | Runs Uvicorn as an unprivileged user, the same user ID as Ingot's images. The code stays owned by root, so the application can't change it. |
| `FORWARDED_ALLOW_IPS="*"` | Uvicorn trusts the `X-Forwarded-Proto` and `X-Forwarded-For` headers from Sevalla's proxy, so Litestar sees the `https` scheme and the visitor's address. Ingot sets the same. |
| `sh -c "exec uvicorn ..."` | Uvicorn doesn't read `PORT` itself, so a shell puts it on the command line. `exec` replaces the shell with Uvicorn, which runs as PID 1, handles `SIGTERM` and finishes the requests in progress before it exits. Uvicorn reads `WEB_CONCURRENCY` for the number of worker processes. |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
.venv
**/__pycache__
.env*
*.db
Dockerfile
.dockerignore
```

Commit and push both files. Ingot doesn't use the `Dockerfile`, so you can keep it in a repository that builds with Ingot, but it adds the `.dockerignore` patterns to its own: its build plan shows an **Ignored** row with `7 patterns from .dockerignore`. 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 Update build strategy sheet with Dockerfile selected, the Dockerfile path, the Docker context and the Docker registry credential](https://assets.sevalla.com/screenshots/light/app-settings-build-strategy-dockerfile.png)

The build log shows `📦 Building and pushing docker image using Dockerfile...` where Ingot's build plan was, then `Migrate has been successful`: the job runs `litestar database upgrade --no-prompt` in the new image. The API returns the same products from the same database, and when an instance stops, its runtime logs end with `Shutting down` and `Finished server process`. The deployment's **Dockerfile** tab shows your Dockerfile. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md), and [Build from the Dockerfile with the API](#build-from-the-dockerfile-with-the-api) to switch with the API.

## 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, run `litestar database make-migrations -m "<what changed>"` on your computer and commit the new file in `migrations/versions` with the code. After you install a package, run `pip freeze > requirements.txt` again. 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 `os.environ`, as `database_url()` reads `DB_URL`. Variables apply at the next deployment. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## Add a custom domain

To serve the API from your own domain, such as `api.example.com`, add the domain on the application's **Domains** tab and create the DNS records that Sevalla shows. Litestar accepts any host unless you add its `AllowedHostsConfig`, so no code change is needed. 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 fails with `Litestar needs a production server, and uvicorn isn't installed` | `requirements.txt` lists Litestar without Uvicorn, for example after `pip install litestar`. | Install `litestar[standard]`, run `pip freeze > requirements.txt` and push. |
| The `Migrate` job fails with `Aborted.` after ``Are you sure you want migrate the database to the `head` revision? [y/n]:`` | The job runs `litestar database upgrade` without `--no-prompt`, and nothing answers the question. | Set the job's **Custom start command** to `litestar database upgrade --no-prompt`. |
| The `Migrate` job fails with `AttributeError: 'Config' object has no attribute 'db_url'` | The job runs `alembic upgrade head`, the command for projects that use Alembic on its own. The `env.py` that `litestar database init` writes takes the database from Advanced Alchemy, which only the `litestar database` commands pass to it. | Set the job's **Custom start command** to `litestar database upgrade --no-prompt`. |
| The application or the job crashes with `sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres` | The code passes Sevalla's `postgres://` URL to SQLAlchemy unchanged. | Switch the scheme to `postgresql+psycopg://`, as `database_url()` does. |
| The deployment fails at `Migrate has failed` | The migration raised an error, or the job can't reach the database. | Read the job's output on the application's **Logs** tab, with **Process** set to `Migrate`. Check that the internal connection exists and that `DB_URL` is set. |

For more causes, see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds) and [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) and [Add a health check](#add-a-health-check), 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 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`.

The sequence creates a database and an application. The API allows 20 creations of each per hour for a company.

**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:

```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 sizes**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) and [List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) return the sizes by `name`, in lowercase. Keep **H1** for the processes and **DB1**, the smallest, for the database:

```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')
DB1_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 process-types` and `sevalla resources db-types`. MCP: `get_platform_options`.

**3. Create the database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`) starts creating a PostgreSQL 18 database. You choose the password here, because the API never returns it and `DB_URL` needs it:

```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":"18","cluster_id":"'"$CLUSTER_ID"'","resource_type_id":"'"$DB1_ID"'","db_name":"storefront","db_user":"storefront","db_password":"'"$DB_PASSWORD"'"}' \
  | jq -r .id)
```

CLI: `sevalla databases create --name orders-db --type postgresql --db-version 18 --cluster $CLUSTER_ID --resource-type $DB1_ID --db-name storefront --db-user storefront --db-password $DB_PASSWORD`. MCP: `create_database`, which generates a password it never returns. Call it once the application exists, with `connect_to_application_id` set to the application and `env_var_name` set to `DB_URL`: it also adds the internal connection and `DB_URL`, in place of the **Connect the database** and **Add the database URL** steps. Its URL starts with `postgresql://`, which `database_url()` switches to psycopg 3 too.

**4. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with a web process and auto-deploy off. It builds with Ingot (`build_type` is `ingot`) and 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`; by default it starts a deployment before the database, the variable and the job exist.

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

**6. Set the size and the readiness probe**

The API creates the web process at **S1**, where the dashboard's default is **H1**. [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) finds the web process, and [Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets the size and adds the readiness probe from [Add a health check](#add-a-health-check), with the dashboard's default timings. Send `timeoutSeconds`: a probe without it gets 120 seconds, where the dashboard sets 1.

```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')
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"'","readiness_probe":{"httpGet":{"path":"/healthz","port":8080,"scheme":"HTTP"},"initialDelaySeconds":0,"periodSeconds":10,"timeoutSeconds":1,"successThreshold":1,"failureThreshold":3}}' \
  | jq -c '{resource_type_name, readiness: .readiness_probe.httpGet.path, timeout: .readiness_probe.timeoutSeconds}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"resource_type_name":"h1","readiness":"/healthz","timeout":1}
```

CLI: `sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID` sets the size; the CLI can't set probes. MCP: `scale_process` with `size_id` for the size, and `sevalla_api_update` with `updateProcessV3` and the same body for the probe.

**7. Wait for the database**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) until its `status` is `ready`, for up to 10 minutes, then keep its internal hostname and port:

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

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

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

**8. Connect the database**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`) lets the application reach the database on Sevalla's private network, and returns the connection's `id`:

```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 -r .id
```

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.

**9. Add the database URL**

Unlike the dashboard, the API doesn't add the connection's variables. [Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds `DB_URL` in the dashboard's format, available during runtime only. Without `is_buildtime`, a variable is available during the build too:

```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 '{"key":"DB_URL","value":"postgres://storefront:'"$DB_PASSWORD"'@'"$DB_HOST"':'"$DB_PORT"'/storefront","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":"DB_URL","is_runtime":true,"is_buildtime":false}
```

CLI: `sevalla apps env-vars create $APP_ID --key DB_URL --value "postgres://storefront:$DB_PASSWORD@$DB_HOST:$DB_PORT/storefront"`, which makes it available during build and runtime. MCP: `set_environment_variables` with `buildtime` set to `false`.

**10. Create the migration job**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`). A job needs `job_start_policy`, and every process needs `resource_type_id` and `scaling_strategy`:

```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":"litestar database upgrade --no-prompt","job_start_policy":"beforeDeployment","resource_type_id":"'"$H1_ID"'","scaling_strategy":{"type":"manual","config":{"instanceCount":1}}}' \
  | jq -c '{display_name, type, job_start_policy, resource_type_name}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"display_name":"Migrate","type":"job","job_start_policy":"beforeDeployment","resource_type_name":"h1"}
```

CLI: `sevalla apps processes create $APP_ID --name Migrate --type job --command "litestar database upgrade --no-prompt" --job-start-policy beforeDeployment --resource-type $H1_ID`. MCP: `sevalla_api_create` with `createProcessV3` and the same body.

**11. 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`.

**12. 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 '{}' | jq -r .id)
```

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

**13. 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` or `failed`. Poll it every 15 seconds, for up to 30 minutes, until the status is final:

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

**14. 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 build plan from [Verify the deployment](#verify-the-deployment), the build, `Migrate has been successful` and the rollout, which ends with `Web has been deployed successfully.` When the status is `failed`, the last lines say why. Without `limit`, the call returns the latest 1,000 lines, so a longer log loses its start; `5000` is the most it allows.

```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" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

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

**15. Check the API**

Request the health route and count the products, on the domain from the **Find the domain** step. Then add a product with the checks in [Verify the deployment](#verify-the-deployment), with `$HOST` in place of `storefront-x7k2m.sevalla.app`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://$HOST/healthz; echo
curl -s https://$HOST/; echo
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
ok
{"products":0}
```

### Build from the Dockerfile with the API

After you push the `Dockerfile` from [Deploy with a Dockerfile](#deploy-with-a-dockerfile), [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches the build strategy. Then repeat the **Deploy** and **Wait for the deployment** steps:

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


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