> ## 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 Play Framework application in Scala

> Deploy a Play Framework 3.0 application in Scala to Sevalla from a Dockerfile: build it with sbt stage on Java 25, set the secret, the port and the allowed hosts, and add a readiness probe.

A Play Framework application runs on Sevalla as a Scala application on the JVM, built from a Dockerfile in your Git repository: sbt compiles it and stages it with `sbt stage`, and the web process runs the start script that Play writes, on the Java 25 runtime. [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's default builder, builds Maven and Gradle projects but not sbt projects, 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`. Play listens on the port that Sevalla sets in the `PORT` environment variable once `conf/application.conf` reads it.

## At a glance

| | |
| - | - |
| Source | A Git repository with a `Dockerfile` |
| Build | The [Dockerfile](#deploy-with-a-dockerfile): sbt on the Java 25 JDK builds the application, `eclipse-temurin:25-jre-resolute` runs it. Ingot doesn't build sbt projects |
| Start command | `/app/bin/storefront`, the start script that `sbt stage` writes |
| Port | The `PORT` environment variable, `8080` by default, read by `play.server.http.port` in `conf/application.conf` |
| Processes | Web process |
| Services | None |
| Disk | None |
| Health check | `GET /health` returns `200` (readiness probe) |
| Size | **H1** to try it, **S1** or larger for traffic. See [Size the web process](#size-the-web-process) |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `APPLICATION_SECRET` | Yes | Runtime | Yes | At least 32 random bytes. `openssl rand -base64 48` prints 64 characters |
| `ALLOWED_HOST` | Yes | Runtime | No | The application's domain without the scheme, such as `storefront-x7k2m.sevalla.app` |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port |

## 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).
* A Java 25 JDK and [sbt](https://www.scala-sbt.org/download/) 1.13 on your computer.

## Create the project

Create a project named `storefront` from Play's official Scala template:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
sbt new playframework/play-scala-seed.g8 --name=storefront --organization=com.example
cd storefront
```

The project is a Play 3.0 application in Scala 2.13.18, built with sbt 1.13.0, with a home page at `/` that shows `Welcome to Play!`. The template pins the Play sbt plugin at 3.0.11. Raise it to the latest release, 3.0.12, in `project/plugins.sbt`:

```scala project/plugins.sbt theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.12")
```

## Prepare the project for Sevalla

Add these lines to the end of `conf/application.conf`:

```text conf/application.conf theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
play.http.secret.key = ${?APPLICATION_SECRET}
play.server.http.port = ${?PORT}
play.server.pidfile.path = /dev/null
play.filters.hosts.allowed += ${?ALLOWED_HOST}
```

| Setting | Why Sevalla needs it |
| - | - |
| `play.http.secret.key` | Play signs session cookies with the secret, and refuses to start in production with its default value. The setting reads it from `APPLICATION_SECRET`. |
| `play.server.http.port` | Play listens on port 9000 and doesn't read `PORT`. This makes it listen on the port Sevalla sets, `8080` by default, on all network interfaces. On your computer, without `PORT`, it stays on 9000. |
| `play.server.pidfile.path` | In production, Play writes its process ID to a `RUNNING_PID` file in the application's folder, which the server's user can't write to in the image. `/dev/null` turns the file off. |
| `play.filters.hosts.allowed` | Play answers only requests for the hosts in this list, which starts with `localhost`, `.local` and `127.0.0.1`. `+=` adds the domain in `ALLOWED_HOST`. See [Allow your domains](#allow-your-domains). |

Each `${?NAME}` reads an environment variable when Play starts, and leaves the setting as it is when the variable isn't set.

Add a route for the readiness probe at the end of `conf/routes`:

```text conf/routes theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
# Readiness probe: answers whatever the Host header is
+ anyhost
GET     /health                     controllers.HomeController.health()
```

Add its action to `app/controllers/HomeController.scala`, after `index()`:

```scala app/controllers/HomeController.scala theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
  def health() = Action {
    Ok("OK")
  }
```

The readiness probe connects to the instance's IP address, which isn't in the allowed hosts. `+ anyhost` exempts this one route from the check, so the probe gets `200` while every other route still checks the host.

Replace `conf/logback.xml`, which writes the log to a file and adds color codes, with a configuration that writes plain lines to the standard output, where Sevalla collects the [runtime logs](https://docs.sevalla.com/applications/logs.md):

```xml conf/logback.xml theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<?xml version="1.0" encoding="UTF-8" ?>

<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->

<!DOCTYPE configuration>

<configuration>
  <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
  <import class="ch.qos.logback.classic.AsyncAppender"/>
  <import class="ch.qos.logback.core.ConsoleAppender"/>

  <appender name="STDOUT" class="ConsoleAppender">
    <encoder class="PatternLayoutEncoder">
      <charset>UTF-8</charset>
      <pattern>%-5level %logger{36} %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="ASYNCSTDOUT" class="AsyncAppender">
    <appender-ref ref="STDOUT"/>
  </appender>

  <logger name="play" level="INFO"/>
  <logger name="application" level="DEBUG"/>

  <root level="WARN">
    <appender-ref ref="ASYNCSTDOUT"/>
  </root>
</configuration>
```

Without this change, every start logs `Failed to create parent directories for [/app/./logs/application.log]`: the image has no writable `logs` folder, and a log file in an instance is lost at the next deployment anyway. The pattern leaves out the date, because the runtime logs show the time of each line.

| File | Why Sevalla needs it |
| - | - |
| `build.sbt` and `project/` | The build, the sbt version (`project/build.properties`) and the Play plugin. The Dockerfile downloads the dependencies from these files alone. |
| `conf/application.conf` | Reads `APPLICATION_SECRET`, `PORT` and `ALLOWED_HOST` each time Play starts |
| `conf/routes` and `app/` | The routes, including `/health` for the readiness probe, and the code |
| `Dockerfile` and `.dockerignore` | Build the image. See [Deploy with a Dockerfile](#deploy-with-a-dockerfile). |

Run `sbt run` and open `http://localhost:9000` to check the application locally.

## Deploy with a Dockerfile

Ingot doesn't build sbt projects, so Sevalla builds this application from a Dockerfile that you add to the repository. Add a `Dockerfile` and a `.dockerignore` to the root of the project:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM sbtscala/scala-sbt:eclipse-temurin-25_1.x AS build
WORKDIR /build
COPY project/build.properties project/plugins.sbt project/
COPY build.sbt ./
RUN sbt update
COPY app ./app
COPY conf ./conf
COPY public ./public
RUN sbt stage && chmod 755 target/universal/stage/bin/storefront

FROM eclipse-temurin:25-jre-resolute
WORKDIR /app
COPY --from=build /build/target/universal/stage ./
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
USER 1000:1000
EXPOSE 8080
CMD ["/app/bin/storefront"]
```

| Line or choice | Why |
| - | - |
| `sbtscala/scala-sbt:eclipse-temurin-25_1.x` | The sbt project's image: the sbt 1.x launcher on the Java 25 JDK. sbt has no Docker Official Image. The launcher runs the sbt version that `project/build.properties` pins. A line tag, not a digest, so each build gets the newest Java 25 update and Ubuntu's security fixes. |
| Build files copied before the source | `sbt update` downloads sbt, Scala, Play and every library in a layer that Docker reuses until `build.sbt` or a file in `project/` changes. A code change starts from there. |
| `sbt stage` | Compiles the application and writes it to `target/universal/stage`: the start script `bin/storefront`, every jar in `lib/` and the configuration in `conf/`. Tests don't run; run them in CI. |
| `chmod 755` | `sbt stage` makes the start script executable for its owner only. In the final image, root owns it, so without `chmod` the server's user can't run it. |
| `eclipse-temurin:25-jre-resolute` | The Java 25 runtime without the compiler, on Ubuntu 26.04: the image that Ingot runs Java applications on. It has Bash for the start script, and `/bin/sh` for the [web terminal](https://docs.sevalla.com/applications/web-terminal.md) and custom start commands. |
| `COPY` without `--chown`, then `USER 1000:1000` | The server runs as user `1000` (`ubuntu` in this image). The application's files stay owned by root, so the server can read them but not change them. |
| `JAVA_TOOL_OPTIONS` | Gives the heap 75% of the memory limit instead of 25%, and exits on `OutOfMemoryError` so Sevalla restarts the instance, as Ingot's image does for Java. See [Size the JVM memory](https://docs.sevalla.com/guides/java/overview.md#size-the-jvm-memory). |
| `CMD ["/app/bin/storefront"]` | The start script ends with `exec java`, so the JVM replaces it as process 1, receives `SIGTERM` directly and shuts Play down. The image needs no init process. |
| `EXPOSE 8080` | Documents Sevalla's default `PORT`, which `conf/application.conf` reads |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
.bsp
.idea
.g8
target
project/target
project/project
logs
RUNNING_PID
.env*
```

The `.dockerignore` keeps the Git history, sbt's and your editor's local folders, the template's `.g8` folder, local build output, the log and process ID files of `sbt run` and local environment files out of the build. Build the image locally to check it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
docker build -t storefront .
docker run --rm -p 8080:8080 -e APPLICATION_SECRET="$(openssl rand -base64 48)" storefront
```

The final image is about 560 MB, with 61 jars in `lib/`. Initialize a Git repository, 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 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 an sbt project, so choose the Dockerfile build strategy before you deploy.

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

**3. Add the environment variables**

Click the **Environment variables** tab, then click **Add environment variables**. Add these variables, clear **Available during buildtime**, and click **Add environment variables**:

| Variable | Value |
| - | - |
| `APPLICATION_SECRET` | The secret that signs session cookies. Generate one with `openssl rand -base64 48`, which prints 64 random characters. |
| `ALLOWED_HOST` | The application's domain without the scheme, for example `storefront-x7k2m.sevalla.app`. It's under the application's name in the page header. |

**4. Add the readiness probe**

Click the **Processes** tab, click **⋯** on the **Web** process, then click **Update process**. Under **Health checks**, turn on **Readiness probe** and set the **Path** to `/health`. Keep the **Port** at the web process's port, `8080` unless you changed it, then click **Update 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).

## 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 the `sbt update` and `sbt stage` steps. 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 deployment waits until the new instance answers the readiness probe. 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 `Welcome to Play!`

![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 `Application started (Prod)`, then `Listening for HTTP on /[0:0:0:0:0:0:0:0]:8080`. If the deployment fails, read its build log and the 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). The build log names the Dockerfile build:

```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?search=Building%20and%20pushing" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
📦 Building and pushing docker image using Dockerfile...
```

Then request the endpoints:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://storefront-x7k2m.sevalla.app/ | grep -o '<h1>.*</h1>'
curl -s -w '\n' https://storefront-x7k2m.sevalla.app/health
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<h1>Welcome to Play!</h1>
OK
```

[Get process](https://api-docs.sevalla.com/v3/applications/get-process) (`getProcessV3`) returns the web process's probe 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":"/health","port":8080,"scheme":"HTTP"},"initialDelaySeconds":0,"periodSeconds":10,"timeoutSeconds":1,"successThreshold":1,"failureThreshold":3}
```

[Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) with `search` finds the address Play listens on:

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

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
INFO  play.core.server.PekkoHttpServer Listening for HTTP on /[0:0:0:0:0:0:0:0]:8080
```

## Size the web process

On **H1**, the smallest size, with a memory limit of 300 MiB, Play started in about 14 seconds, from the JVM's first log line to `Listening for HTTP`, and the **Memory usage** chart on the application's **Analytics** tab reached the limit. The application served every request in the test. For traffic, [scale](https://docs.sevalla.com/applications/scaling.md) the web process to **S1** or larger. The readiness probe keeps a slow start from taking requests before Play listens. See [Size the JVM memory](https://docs.sevalla.com/guides/java/overview.md#size-the-jvm-memory).

## Allow your domains

Play's allowed hosts filter answers a request only when its `Host` header is in `play.filters.hosts.allowed`. Any other request gets `400` with a page titled `Bad Request`, and the runtime logs show `Host not allowed:` followed by the host. The list starts with `localhost`, `.local` and `127.0.0.1`, and `conf/application.conf` adds the domain in `ALLOWED_HOST`. If you set `ALLOWED_HOST` to another domain, the application's own domain gets `400` too.

To serve the application on a [custom domain](#add-a-custom-domain) as well as on its `sevalla.app` domain, add the custom domain in `conf/application.conf`, next to the `ALLOWED_HOST` line:

```text conf/application.conf theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
play.filters.hosts.allowed += "shop.example.com"
```

Commit the change and deploy. A value that starts with a dot, such as `".example.com"`, allows the domain and all its subdomains.

## 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).

The JVM receives `SIGTERM` and runs Play's shutdown hook. Play stops taking new connections, logs `Unbinding /[0:0:0:0:0:0:0:0]:8080`, and waits up to 5 seconds for the requests in progress, the default timeout of the `service-requests-done` phase of Pekko's coordinated shutdown, before it closes them. The old instance's [runtime logs](https://docs.sevalla.com/applications/logs.md) end with `Running provided shutdown stop hooks` on every deployment after the first. To wait longer for slow requests, set `play.server.terminationTimeout` and `pekko.coordinated-shutdown.phases.service-requests-done.timeout` in `conf/application.conf`, both under 25 seconds.

## 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 from the Dockerfile, waits for the new instance to answer the readiness probe, then replaces the old one. When only your code changes, the build log shows `CACHED` for the `sbt update` step, so the build downloads nothing 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 in `conf/application.conf` with `${?NAME}`, as the file reads `PORT`, or in code with `sys.env`. Variables apply at the next deployment. 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. Then add it to the allowed hosts, as in [Allow your domains](#allow-your-domains), and deploy. 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 doesn't build sbt projects | Choose the Dockerfile build strategy, as in [Deploy to Sevalla](#deploy-to-sevalla), then deploy |
| The runtime logs show `Oops, cannot start the server.` and `The application secret has not been set, and we are in prod mode. Your application is not secure.` | `APPLICATION_SECRET` isn't set, or `conf/application.conf` doesn't read it | Add `play.http.secret.key = ${?APPLICATION_SECRET}` and the variable, then deploy |
| The runtime logs show `The application secret is too short and does not have the recommended amount of entropy for algorithm HS256` | The secret is shorter than 32 bytes | Generate a new one with `openssl rand -base64 48` |
| The runtime logs show `Oops, cannot start the server.` and `java.nio.file.AccessDeniedException: /app/RUNNING_PID` | Play writes its process ID file to the application's folder, which root owns | Add `play.server.pidfile.path = /dev/null` to `conf/application.conf` |
| The runtime logs show `/__cacert_entrypoint.sh: line 123: /app/bin/storefront: Permission denied` | The start script is executable for root only | Add `chmod 755 target/universal/stage/bin/storefront` after `sbt stage` in the Dockerfile |
| The runtime logs show `Failed to create parent directories for [/app/./logs/application.log]` | The template's `conf/logback.xml` writes the log to a file in a folder the server's user can't create | Replace `conf/logback.xml` as in [Prepare the project for Sevalla](#prepare-the-project-for-sevalla) |
| The domain returns `400` with a `Bad Request` page, and the runtime logs show `Host not allowed:` | The domain isn't in the allowed hosts | Set `ALLOWED_HOST` to the domain, or add it as in [Allow your domains](#allow-your-domains), then deploy |
| The runtime logs show `WARNING: A terminally deprecated method in sun.misc.Unsafe has been called` at each start | Guava 32.1.3, which Play 3.0 uses, calls `sun.misc.Unsafe`, which Java 25 warns about once | Nothing: the application works. To hide the warning, add `--sun-misc-unsafe-memory-access=allow` to `JAVA_TOOL_OPTIONS` |
| 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 |

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` and `APP: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:

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

**3. Choose the Dockerfile build strategy**

The application builds with Ingot (`build_type` is `ingot`), which can't build an sbt 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.

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

**5. Add the environment variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds a new secret in `APPLICATION_SECRET` and the domain in `ALLOWED_HOST`, available at runtime only:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for VAR in "APPLICATION_SECRET=$(openssl rand -base64 48)" "ALLOWED_HOST=$HOST"; do
  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 kv "$VAR" '{key: ($kv | sub("=.*"; "")), value: ($kv | sub("^[^=]*="; "")), is_runtime: true, is_buildtime: false}')" \
    | jq -c '{key, is_runtime, is_buildtime}'
done
```

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

CLI: `sevalla apps env-vars create $APP_ID --key ALLOWED_HOST --value $HOST`, once per variable; it always makes a variable available during the build and runtime. MCP: `set_environment_variables` with `buildtime` set to `false`.

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

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

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

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 and adds the readiness probe on `/health`, 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"}}
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":"/health","port":8080,"scheme":"HTTP"},"initialDelaySeconds":0,"periodSeconds":10,"timeoutSeconds":1,"successThreshold":1,"failureThreshold":3}}' \
  | jq -c '{resource_type_name, readiness_probe}'
```

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

CLI: `sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID` sets the size; it has no option for the probe. MCP: `sevalla_api_update` with `updateProcessV3` and the same body.

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

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

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

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

**12. 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 and the rollout. When the status is `failed`, the last lines say why. Without `limit`, it returns the latest 1,000 lines:

```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 --limit 5000`. MCP: `get_deployment_logs`.

**13. 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 https://$HOST/ | grep -o '<h1>.*</h1>'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<h1>Welcome to Play!</h1>
```

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.