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

> Deploy a Sinatra 4 application to Sevalla from a Git repository: a config.ru for Puma, Ruby 4.0 pinned in the Gemfile, and a build with Ingot or a Dockerfile.

A Sinatra application runs on Sevalla as an application: [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's builder, installs the gems from your Git repository with Bundler, finds the `config.ru` file, and starts the web process with Puma on the port that Sevalla sets in the `PORT` environment variable.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Ruby 4.0, Bundler), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `bundle exec puma --bind "tcp://0.0.0.0:$PORT"`, which Ingot sets when it finds `config.ru` and Puma |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process |
| Services | None |
| Disk | None |
| Health check | None |
| Size | **H1** |

### Environment variables

The application reads no environment variables of its own. Sevalla sets `PORT` to the web process port, and Ingot sets `RACK_ENV` to `production`.

## 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).
* Ruby 4.0 on your computer.

## Create the project

Create a folder with a `Gemfile`, then add Sinatra and Puma, the web server:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
mkdir storefront && cd storefront
bundle init
bundle add sinatra puma
```

`bundle add` installs Sinatra 4.2 and Puma 8.0 and writes `Gemfile.lock`. Create the application as a class, so `config.ru` can run it:

```ruby app.rb theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
require "sinatra/base"

class Storefront < Sinatra::Base
  get "/" do
    "Hello from Sinatra on Sevalla"
  end
end
```

```ruby config.ru theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
require_relative "app"

run Storefront
```

## Prepare the project for Sevalla

Ingot builds on Ruby 4.0, its default line, unless the project sets a version. Pin the line in the `Gemfile`, under the `source` line, so the build doesn't change when a new Ingot release moves its default:

```ruby Gemfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
source "https://rubygems.org"

ruby "~> 4.0.0"
```

Then run `bundle install` to record the Ruby version in `Gemfile.lock`. See [Pin the Ruby version](https://docs.sevalla.com/guides/ruby/overview.md#pin-the-ruby-version).

| File | Why Sevalla needs it |
| - | - |
| `Gemfile` | `ruby "~> 4.0.0"` sets the Ruby line. Ingot detects Sinatra and Puma from the gems. |
| `Gemfile.lock` | Ingot installs exactly these gems, with the Bundler release that wrote the file. |
| `config.ru` | Tells Puma which application to run. Without it, Ingot doesn't know how to start the application. |

Ingot finds `config.ru` and Puma in `Gemfile.lock`, and starts the web process with `bundle exec puma --bind "tcp://0.0.0.0:$PORT"`: Puma listens on all network interfaces and on the port in `PORT`, which Sevalla sets to the web process's port (`8080` by default). Ingot sets `RACK_ENV=production`, so Sinatra runs in production mode. Puma runs up to 5 threads in 1 process; on a larger size, set the `WEB_CONCURRENCY` environment variable to the number of processes, for example `2`. The project needs no other configuration.

Run `bundle exec puma` and open `http://localhost:9292` to check the application locally. Then 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 application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose a **Data center**. Keep the **Size** at **H1** or choose a larger one, then click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**: creating an application doesn't deploy it.

**2. Deploy**

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

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

## 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): Ruby 4.0 from the `Gemfile`, Sinatra from `Gemfile.lock`, and the start command from `config.ru`.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Sinatra · Ruby 4.0 ───────────────────────────────────────────────────────╮
   │ Ruby       4.0                                            Gemfile "~> 4.0.0" │
   │ Bundler    4.0.20                                         Gemfile.lock       │
   │ Framework  Sinatra                                        Gemfile.lock       │
   │ Start      bundle exec puma --bind "tcp://0.0.0.0:$PORT"  config.ru          │
   ├─ Image ──────────────────────────────────────────────────────────────────────┤
   │ Build      ruby:4.0-trixie                                                   │
   │ Runtime    ruby:4.0-slim-trixie                                              │
   │ User       1000:1000                                                         │
   │ Init       tini                                                              │
   │ Port       $PORT, default 8080                                               │
   ╰─────────────────────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

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 `Hello from Sinatra on Sevalla`.

![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)

If the deployment fails, read its build log, then see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds). If it deploys but the domain doesn't respond, read the application's [runtime logs](https://docs.sevalla.com/applications/logs.md): Puma prints `Listening on` with the address it binds.

### 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](#deploy-with-the-api).

1. The deployment's status, from [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`):

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s "$API/applications/$APP_ID/deployments/$DEPLOYMENT_ID" -H "$H" | jq -r .status
   ```

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

2. Puma's start, from [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`), which searches the last hour by default:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s "$API/applications/$APP_ID/runtime-logs?search=Listening" -H "$H" | jq -r '.[].message'
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   * Listening on http://0.0.0.0:8080
   ```

3. The application, with your domain:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://storefront-x7k2m.sevalla.app/
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   Hello from Sinatra on Sevalla
   ```

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Use one when you want full control of the image: the base image, the system packages and every layer. Puma reads its settings from `config/puma.rb`, so first make it listen on `PORT` there, with Puma's own default for your computer:

```ruby config/puma.rb theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
port ENV.fetch("PORT", 9292)
```

Then 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 ruby:4.0-slim-trixie AS base

WORKDIR /app

# jemalloc keeps the memory of Puma's threads from fragmenting.
# hadolint ignore=DL3008
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y libjemalloc2 && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

ENV RACK_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development:test" \
    LD_PRELOAD="libjemalloc.so.2" \
    PORT="8080"


FROM base AS build

# hadolint ignore=DL3008
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git


FROM base

RUN groupadd --system --gid 1000 app && \
    useradd app --uid 1000 --gid 1000 --create-home --shell /bin/bash

# Gems from the build stage, then the code, both owned by root.
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY . .

USER 1000:1000

EXPOSE 8080
CMD ["bundle", "exec", "puma"]
```

| Line or choice | Why |
| - | - |
| `ruby:4.0-slim-trixie` | The Ruby line the `Gemfile` pins, on Debian 13. A line tag, not a digest, so every build picks up the line's security fixes. |
| Three stages | `base` holds the runtime settings, `build` adds the compilers that Puma's native extension needs, and the final stage copies only the installed gems and the code. |
| `Gemfile` and `Gemfile.lock` before the code | A change to the code reuses the cached gem layer. `BUNDLE_DEPLOYMENT` installs exactly what `Gemfile.lock` lists. |
| User `1000:1000`, code owned by root | The application can't change its own files. Sinatra writes nothing to disk. |
| `CMD ["bundle", "exec", "puma"]` | `bundle exec` replaces itself with Puma, which runs as the container's main process, so it gets `SIGTERM` when Sevalla replaces an instance and finishes the requests in flight. |
| `PORT="8080"` and `EXPOSE 8080` | `config/puma.rb` reads `PORT`, which Sevalla sets to the web process's port at runtime. |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
.bundle
.env*
vendor/bundle
log
tmp
Dockerfile
.dockerignore
```

Commit and push the three files. Ingot doesn't use the `Dockerfile`, and it applies the `.dockerignore` patterns to its own build, so the repository still 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 build log shows `📦 Building and pushing docker image using Dockerfile...` instead of Ingot's build plan. The application's domain shows `Hello from Sinatra on Sevalla`, and the deployment's **Dockerfile** tab shows the file the image was built from. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md).

## 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, then replaces the web process's instances. When you add a gem with `bundle add`, commit `Gemfile.lock` with it. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Read settings with `ENV.fetch("NAME")`, for example an API key or a database URL. Add them on the application's **Environment variables** tab: they apply at the next deployment. To connect a Sevalla database, [add an internal connection](https://docs.sevalla.com/databases/connect.md#connect-an-application) with **Add environment variables to the application** turned on, and read `DB_URL` or `REDIS_URL`. 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).

## Deploy with the API

The same deployment as [Deploy to Sevalla](#deploy-to-sevalla), as calls to the [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) that a script or an AI agent runs in order. Each step names the matching [CLI](https://docs.sevalla.com/developer-tools/cli.md) command and [Sevalla MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool. The sequence needs:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) with the **Company developer** role, or the capabilities to create, read and update applications, in `SEVALLA_API_KEY`.
* The repository pushed to GitHub, and a GitHub account with access to it connected to Sevalla. Connecting one is a dashboard step, and the API has no call that reports it: if no connected account can read the repository, the step that creates the application fails with `400` and a message that starts with `Cannot access repository` and says where to connect one.
* `curl` and `jq`.

Set the base URL, the authorization header and the repository:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
API=https://api.sevalla.com/v3
H="Authorization: Bearer $SEVALLA_API_KEY"
REPO_URL=https://github.com/acme/storefront
BRANCH=main
```

**1. Choose the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/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](https://docs.sevalla.com/platform/locations.md) lists the others. Display names differ between environments, so don't select by them:

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

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

**2. Look up the size**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) gives the ID of **H1**:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
H1_ID=$(curl -s "$API/resources/process-resource-types" -H "$H" | jq -r '.[] | select(.name == "h1") | .id')
```

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

**3. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates it with a web process and doesn't deploy it. [List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns its `sevalla.app` domain as the primary one, for the checks:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST "$API/applications" -H "$H" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg repo "$REPO_URL" --arg branch "$BRANCH" \
    '{display_name: "storefront", cluster_id: $cluster, source: "privateGit", git_type: "github",
      repo_url: $repo, default_branch: $branch}')" \
  | jq -r .id)
for i in $(seq 1 30); do
  APP_HOST=$(curl -s "$API/applications/$APP_ID/domains" -H "$H" | jq -r '.data[]? | select(.is_primary) | .name')
  [ -n "$APP_HOST" ] && break
  sleep 2
done
echo "$APP_HOST"
```

Sevalla adds the domain about 2 seconds after it creates the application, so the loop waits for it.

CLI: `sevalla apps create --name storefront --cluster $CLUSTER_ID --source privateGit --git-type github --repository $REPO_URL --branch $BRANCH`, then `sevalla apps domains list --app-id $APP_ID`. MCP: `create_application` with `deploy: false`, then `get_application`.

**4. Size the web process**

The API creates the web process at **S1**, not the dashboard's **H1**. [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) finds it, and [Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets **H1**:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(curl -s "$API/applications/$APP_ID/processes" -H "$H" | jq -r '.data[] | select(.type == "web") | .id')
curl -s -X PATCH "$API/applications/$APP_ID/processes/$WEB_ID" -H "$H" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$H1_ID" '{resource_type_id: $size}')" | jq -r .resource_type_id
```

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

**5. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s -X POST "$API/applications/$APP_ID/deployments" -H "$H" -H "Content-Type: application/json" \
  -d "$(jq -n --arg branch "$BRANCH" '{branch: $branch}')" | jq -r .id)
```

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

**6. Wait for the deployment**

Poll [Get deployment](https://api-docs.sevalla.com/v3/applications/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:

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

On `failed`, read the build log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) and see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds). CLI: `sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID` and `sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID`. MCP: `get_deployment` and `get_deployment_logs`.

**7. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "$API/applications/$APP_ID" -H "$H" -H "Content-Type: application/json" \
  -d '{"auto_deploy": true}' | jq -r .auto_deploy
```

To build from the [Dockerfile](#deploy-with-a-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](#check-from-a-terminal), with `https://$APP_HOST` as the domain.


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