> ## 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 Vite single-page app

> Deploy a Vite single-page app built with React or Vue to Sevalla as a free static site, or as an application built with Ingot or a Dockerfile, with client-side routes and VITE_ variables.

A Vite single-page app builds to static files: `vite build` writes HTML, CSS and JavaScript to the `dist` folder, and the router in the browser renders each page. Sevalla publishes `dist` as a static site, served from Cloudflare's edge and free up to 100 GB of bandwidth a month. When the app has to run as an [application](https://docs.sevalla.com/applications/overview.md) instead, for example next to an API in a [pipeline](https://docs.sevalla.com/pipelines/overview.md), Ingot builds it and serves `dist` with Caddy.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | Static site: `npm run build` on Node.js **Latest LTS**, publishing `dist`. Application: [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Node.js 24, npm), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | Static site: none. Application: Caddy serves `dist`, set up by Ingot for Vite or by the Dockerfile |
| Port | Application: the `PORT` environment variable, `8080` by default |
| Processes | Static site: none. Application: web process |
| Services | None |
| Disk | None |
| Size | Application: **H1** for the web process |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `VITE_API_URL` | No | Build | No | `https://api.example.com`, the address the browser code calls. Vite writes it into the JavaScript, so never put a secret in it |

## 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).
* Node.js 24 or later on your computer.

## Create the project

Create a project from Vite's TypeScript template for your framework, and add its router:

**React**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm create vite@latest storefront -- --template react-ts --no-interactive
cd storefront
npm install
npm install react-router
```

**Vue**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm create vite@latest storefront -- --template vue-ts --no-interactive
cd storefront
npm install
npm install vue-router
```

`--no-interactive` skips the questions, so the command creates the project and stops. The project comes with a `.gitignore` and a `build` script that checks the TypeScript and runs `vite build`.

Add an About page at `/about`, which shows a [build-time variable](#use-build-time-variables), and the routes:

**React**

```tsx src/About.tsx theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
export default function About() {
  return (
    <section id="center">
      <h1>About Storefront</h1>
      <p>Orders API: {import.meta.env.VITE_API_URL ?? 'not set'}</p>
    </section>
  )
}
```

Replace `src/main.tsx` with this:

```tsx src/main.tsx theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Route, Routes } from 'react-router'
import './index.css'
import App from './App.tsx'
import About from './About.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
)
```

**Vue**

```vue src/About.vue theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<script setup lang="ts">
const apiUrl = import.meta.env.VITE_API_URL ?? 'not set'
</script>

<template>
  <section id="center">
    <h1>About Storefront</h1>
    <p>Orders API: {{ apiUrl }}</p>
  </section>
</template>
```

```ts src/router.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { createRouter, createWebHistory } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import About from './About.vue'

export default createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: HelloWorld },
    { path: '/about', component: About },
  ],
})
```

Replace `src/main.ts` and `src/App.vue` with these:

```ts src/main.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')
```

```vue src/App.vue theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<template>
  <RouterView />
</template>
```

The router reads the path from the address bar, so `/about` exists only in the browser: `dist` has no `about` file. When someone opens `/about` directly or reloads it, the server must answer with `index.html`, and the router renders the page. Each way of deploying in this guide sets that up.

Run `npm run dev` and open `http://localhost:5173/about` to check the routes 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 "Create the storefront"
git branch -M main
git remote add origin https://github.com/acme/storefront.git
git push -u origin main
```

## Deploy as a static site

**1. Choose the repository**

In the sidebar, click **Static sites**, then click **Create static site**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**.

**2. Check the build settings**

Under **Build settings**, Sevalla reads `package.json` and fills in the build command and the publish directory. For the React template, it shows **Framework detected: React + Vite**. Check the values and set the **Error file**:

| Field | Value |
| - | - |
| **Build site before publishing** | On |
| **Node version** | **Latest LTS** |
| **Build command** | `npm run build` |
| **Root directory** | Empty, for the repository root |
| **Publish directory** | `dist` |
| **Error file** | `index.html` |

**3. Add the build-time variable**

Under **Environment variables**, enter `VITE_API_URL` as the **Key** and `https://api.example.com` as the **Value**.

**4. Create the static site**

Enter a **Name** under **Details**, then click **Create static site**. Sevalla starts the first deployment right away, and deploys every push to the branch while **Auto-deploy** is on.

The **Error file** makes the site answer every path that has no file with `index.html`, so the router gets `/about` and any other route. Static sites don't read `engines` in `package.json`: the build uses the **Node version** from the build settings. See [Build settings](https://docs.sevalla.com/static-sites/build-settings.md) for every field.

### Verify the static site

1. On the **Deployments** tab, wait for the deployment's status to change to **Deployed**. The site's **Status** on the **Overview** tab changes to **Live**.
2. On the **Overview** tab, click **Visit site**. The Vite starter page opens on the site's `*.sevalla.page` domain.
3. Add `/about` to the address and press Enter. The page shows **About Storefront** and `Orders API: https://api.example.com`.

![The static site's Details card with its domain, Live status, source repository and the Visit site button](https://assets.sevalla.com/screenshots/light/static-site-overview-visit.png)

To check from a terminal, request `/about` and the JavaScript it loads, which holds the value of `VITE_API_URL`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
JS=$(curl -s https://storefront-x7k2m.sevalla.page/about | grep -o '/assets/index-[^"]*\.js')
curl -s "https://storefront-x7k2m.sevalla.page$JS" | grep -o 'https://api.example.com'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
https://api.example.com
```

If `/about` shows Sevalla's 404 page, the **Error file** is empty: set it to `index.html` on the **Settings** tab with **Update build strategy**, then deploy again. If the deployment fails, open it on the **Deployments** tab and read its build log, then see [Troubleshoot static sites](https://docs.sevalla.com/static-sites/troubleshooting.md).

## Use build-time variables

Vite replaces each `import.meta.env.VITE_*` expression with the variable's value while it builds, so the value ends up in the JavaScript files that every visitor downloads. Variables without the `VITE_` prefix never reach the browser code.

* **Static site.** Add the variable on the site's **Environment variables** tab, then deploy the site again: a changed value applies only to the next build. See [Environment variables](https://docs.sevalla.com/static-sites/environment-variables.md).
* **Application.** Add the variable on the application's **Environment variables** tab with **Available during buildtime** selected, then deploy again. See [Environment variables during the build](https://docs.sevalla.com/applications/builds/ingot.md#environment-variables-during-the-build).

Never put a secret, such as an API key that grants access to your data, in a `VITE_` variable: anyone can read it in the browser.

## Deploy as an application

An application built from the same repository serves the same files. A static site costs nothing up to the monthly allowances, so use an application only when the app needs something only applications have, such as promoting one build through the stages of a [pipeline](https://docs.sevalla.com/pipelines/overview.md).

Pin the Node.js version that builds the app:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm pkg set engines.node="24.x"
```

Without it, [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) builds on its default line, Node.js 24 today, which a new Ingot release can move. See [Pin the Node.js version](https://docs.sevalla.com/guides/javascript/overview.md#pin-the-node-js-version). Commit and push the change, then create the application. These steps build with Ingot. 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. Add the build-time variable**

Click the **Environment variables** tab, then click **Add environment variables**. Enter `VITE_API_URL` as the key and `https://api.example.com` as the value, keep **Available during buildtime** selected, and click **Add environment variables**.

**3. Deploy**

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

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

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): Vite from `package.json`, built on `node:24-trixie` and served from an `alpine:3` image with Caddy.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Vite · Node.js 24 ──────────────────────────────╮
   │ Node.js          24    package.json engines "24.x" │
   │ Package manager  npm   package-lock.json           │
   │ Framework        Vite  package.json                │
   ├─ Image ────────────────────────────────────────────┤
   │ Build            node:24-trixie                    │
   │ Build            caddy:2-alpine                    │
   │ Runtime          alpine:3                          │
   │ 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`, then add `/about` to the address. The page shows **About Storefront** and `Orders API: https://api.example.com`.

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

To check from a terminal, read the deployment's status with [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) and its build log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs), as [Deploy with the API](#deploy-with-the-api) does, then request `/about` and the JavaScript it loads:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -o /dev/null -w '%{http_code}\n' https://storefront-x7k2m.sevalla.app/about
JS=$(curl -s https://storefront-x7k2m.sevalla.app/about | grep -o '/assets/index-[^"]*\.js')
curl -s "https://storefront-x7k2m.sevalla.app$JS" | grep -o 'https://api.example.com'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
200
https://api.example.com
```

The image has no Node.js: Caddy serves `dist` on the port in `PORT`, answers paths that have no file with `index.html`, and marks the content-hashed files under `/assets/` as cacheable for a year. To serve them from Cloudflare's servers, turn on the CDN on the application's **Networking** tab. See [CDN and edge caching](https://docs.sevalla.com/applications/caching.md).

If the deployment fails, read its build log, then see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds).

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Write one when you want full control of the image: the base image, the web server and every layer. Add a `Dockerfile`, a `Caddyfile` and a `.dockerignore` to the root of the repository:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM node:24-trixie-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG VITE_API_URL
RUN npm run build

FROM caddy:2-alpine AS caddy
RUN setcap -r /usr/bin/caddy

FROM alpine:3
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
COPY --from=caddy /etc/mime.types /etc/mime.types
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
ENV XDG_CONFIG_HOME=/tmp XDG_DATA_HOME=/tmp
USER 1000:1000
EXPOSE 8080
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
```

```text Caddyfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
	admin off
	auto_https off
	persist_config off
}

:{$PORT:8080} {
	root * /srv
	encode zstd gzip
	header /assets/* Cache-Control "public, max-age=31536000, immutable"
	try_files {path} /index.html
	file_server
}
```

| Line or choice | Why |
| - | - |
| `node:24-trixie-slim` | Node.js 24, the line `engines.node` pins, on Debian 13, for the build only. A line tag, not a digest, so each build picks up Node.js and Debian security fixes. |
| Three stages | The `build` stage runs `vite build`. The `caddy` stage takes Caddy from its official image and removes the file capability that lets it bind ports below 1024, which a server running as a regular user on port 8080 doesn't need. The final image is Alpine Linux with Caddy and `dist`: no Node.js, no `node_modules` and no source. |
| `package.json` and `package-lock.json` before the source | `npm ci` installs exactly what the lock file lists, and the [build cache](https://docs.sevalla.com/applications/builds/overview.md#build-cache) reuses that layer until the lock file changes, so a code change doesn't reinstall the dependencies. |
| `ARG VITE_API_URL` | Sevalla passes every variable marked **Available during buildtime** as a build argument, and `ARG` hands it to `vite build`. It's declared in the build stage only, so it doesn't show in the final image's history. Declare each `VITE_` variable your code reads. |
| `try_files {path} /index.html` | Answers paths that have no file with `index.html`, so the router renders `/about` on a reload. |
| `USER 1000:1000` | Caddy runs as a regular user, not root. The copied files stay owned by root, so the server can read them but not change them. Caddy keeps its state in `/tmp`, which `XDG_CONFIG_HOME` and `XDG_DATA_HOME` point to. |
| `:{$PORT:8080}` | Caddy listens on `PORT`, which Sevalla sets to the web process's port, or `8080` when it isn't set. Caddy stops gracefully on `SIGTERM` by itself, so the image needs no init process. |

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

Commit and push the three files. Ingot doesn't use the `Dockerfile` and adds your `.dockerignore` patterns to its own, so you can keep the files in a repository that builds with Ingot. Then switch the application to the Dockerfile build strategy:

**1. Choose the Dockerfile build strategy**

On the application's **Settings** tab, in the **Build strategy** section, click **Update build strategy**. Click **Dockerfile**, keep the **Dockerfile path** `Dockerfile` and the **Docker context** `.`, then click **Update build strategy**.

**2. Deploy**

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

The deployment's build log shows `📦 Building and pushing docker image using Dockerfile...` instead of Ingot's build plan, and its **Dockerfile** tab shows your file. When the status changes to **Deployed**, `/about` on the application's domain shows `Orders API: https://api.example.com`. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) for build arguments and private base images.

## Deploy changes

With **Auto-deploy** on, every push to the branch starts a deployment of the static site or the application; otherwise, deploy each change from the **Deployments** tab. A static site's deployment builds the app and switches the site to the new files; an application's deployment builds a new image and replaces the web process's instances. See [Static site deployments](https://docs.sevalla.com/static-sites/deployments.md) and [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Add a custom domain

To serve the app from your own domain, such as `shop.example.com`, add the domain on the static site's **Domains** tab and create the DNS records that Sevalla shows. Applications have a **Domains** tab too, but 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 [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) makes the same deployment as this guide without the dashboard. Run the steps in order in one shell: each step is one API call, and saves the IDs that later steps need in shell variables. Each step also names 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. A variable that holds `null` means the call failed: run the `curl` command without its `jq` part to read the error.

This section has two sequences, one for each way to deploy: [as a static site](#deploy-as-a-static-site) and [as an application](#deploy-as-an-application). 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 for the application and the `STATIC_SITE:CREATE`, `STATIC_SITE:READ` and `STATIC_SITE:UPDATE` permissions for the static site, in the `SEVALLA_API_KEY` environment variable. The CLI reads the key from `SEVALLA_API_TOKEN`.
* The repository on GitHub, GitLab or Bitbucket, and a Git account that can read it connected to your Sevalla user, which only the dashboard can do. See [Git providers](https://docs.sevalla.com/git/overview.md). Creating the application or the static site checks the access: if 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 the access.
* `curl` and `jq`.

### Deploy the static site

**1. Create the static site**

[Create static site](https://api-docs.sevalla.com/v3/static-sites/create-static-site) (`createStaticSiteV3`) creates the static site with the build settings of the dashboard steps. `node_version` `lts` is the dashboard's **Latest LTS**. Static sites don't read `engines` in `package.json`.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
SITE_ID=$(curl -s -X POST https://api.sevalla.com/v3/static-sites \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r .id
{
  "display_name": "storefront",
  "source": "privateGit",
  "git_type": "github",
  "repo_url": "https://github.com/acme/storefront",
  "default_branch": "main",
  "build_command": "npm run build",
  "node_version": "lts",
  "published_directory": "dist",
  "error_file": "index.html",
  "auto_deploy": true
}
EOF
)
```

CLI: `sevalla static-sites create --name storefront --repository https://github.com/acme/storefront --branch main --git-type github --build-command "npm run build" --node-version lts --publish-directory dist`. The CLI has no options for the error file and auto-deploy, so set `error_file` and `auto_deploy` with the API. MCP: `create_static_site` with the same fields.

**2. Add the build-time variable**

[Create static site environment variable](https://api-docs.sevalla.com/v3/static-sites/create-static-site-environment-variable) (`createStaticSiteEnvVarV3`) adds `VITE_API_URL`, which `vite build` writes into the JavaScript. It applies to production and preview deployments by default.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "https://api.sevalla.com/v3/static-sites/$SITE_ID/env-vars" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"VITE_API_URL","value":"https://api.example.com"}' \
  | jq -r .key
```

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

CLI: `sevalla static-sites env-vars create "$SITE_ID" --key VITE_API_URL --value https://api.example.com`. MCP: `set_environment_variables` with `scope` set to `static_site`.

**3. Deploy the static site**

[Deploy static site](https://api-docs.sevalla.com/v3/static-sites/deploy-static-site) (`deployStaticSiteV3`) builds and publishes the latest commit of the branch. Unlike the dashboard, the API doesn't start the first deployment when it creates the site.

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

CLI: `sevalla static-sites deployments trigger "$SITE_ID"`. MCP: `trigger_deployment` with `service_type` set to `static_site`.

**4. Wait for the static site deployment**

[Get static site deployment](https://api-docs.sevalla.com/v3/static-sites/get-static-site-deployment) (`getStaticSiteDeploymentV3`) returns the deployment's `status`. Poll it until the deployment ends:

```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/static-sites/$SITE_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
```

`success` means the site is live. If it's `failed`, [Get static site deployment logs](https://api-docs.sevalla.com/v3/static-sites/get-static-site-deployment-logs) (`getStaticSiteDeploymentLogsV3`) returns the build log. CLI: `sevalla static-sites deployments get "$DEPLOYMENT_ID" --site-id "$SITE_ID"`. MCP: `get_deployment`, and `get_deployment_logs` for the log.

**5. Check the static site**

[Get static site](https://api-docs.sevalla.com/v3/static-sites/get-static-site) (`getStaticSiteV3`) returns the site's domain in `hostname`, such as `storefront-x7k2m.sevalla.page`. `/about` has no file, so the site answers it with `index.html`, the error file. The JavaScript that `index.html` loads holds the value of `VITE_API_URL`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
SITE=$(curl -s "https://api.sevalla.com/v3/static-sites/$SITE_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r .hostname)
JS=$(curl -s "https://$SITE/about" | grep -o '/assets/index-[^"]*\.js')
curl -s "https://$SITE$JS" | grep -o 'https://api.example.com'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
https://api.example.com
```

CLI: `sevalla static-sites get "$SITE_ID"`. MCP: `get_static_site`.

### Deploy the application

**1. Find the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns every location with its `id`, its name in `display_name` and its region ID in `location`. Pick one by its region ID, here `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 the Ingot build strategy and a web process, and doesn't deploy it.

```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 @- <<EOF | jq -r .id
{
  "display_name": "storefront",
  "cluster_id": "$CLUSTER_ID",
  "source": "privateGit",
  "git_type": "github",
  "repo_url": "https://github.com/acme/storefront",
  "default_branch": "main"
}
EOF
)
```

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. Find the web process**

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

```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`, in `processes`.

**4. Find the H1 size**

The API creates the web process with the **S1** size, where the dashboard picks **H1**. [List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) returns the ID of each size:

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

**5. Set the web process size**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets the size, which applies at the first deployment:

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

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

CLI: `sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$H1_ID"`. MCP: `scale_process` with `size_id`, and `apply_now` set to `false`.

**6. Add the VITE\_API\_URL variable**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds `VITE_API_URL`. Vite reads it during the build, so it needs to be available during the build only.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "https://api.sevalla.com/v3/applications/$APP_ID/env-vars" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF | jq -r .key
{
  "key": "VITE_API_URL",
  "value": "https://api.example.com",
  "is_runtime": false,
  "is_buildtime": true
}
EOF
```

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

CLI: `sevalla apps env-vars create "$APP_ID" --key VITE_API_URL --value "https://api.example.com"`, which makes the variable available during both the build and runtime. MCP: `set_environment_variables` with `runtime` set to `false` and `buildtime` set to `true`.

**7. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) with `auto_deploy` deploys 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 -r .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`, the application ID as `path_params.id` and the same body.

**8. Choose the Dockerfile build strategy**

Skip this step to build with Ingot. To build from the [Dockerfile](#deploy-with-a-dockerfile) instead, [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) sets the build strategy:

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

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

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.

**9. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) deploys the latest commit of 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`.

**10. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`: `waiting`, `inProgress`, then `success`, `failed`, `cancelled` or `skipped`. Poll it until the deployment ends:

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

The loop asks every 15 seconds, for up to 30 minutes, and keeps asking through an empty or failed reply. `success` means the application is deployed. CLI: `sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID"`. MCP: `get_deployment`.

**11. Read the build log**

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, one line per entry. With Ingot, the build plan shows Vite, and the `alpine:3` runtime image that serves `dist` with Caddy:

```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' | grep -E '│ (Framework|Runtime) ' | tr -s ' '
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
│ Framework Vite package.json │
│ Runtime alpine:3 │
```

With a Dockerfile, the log shows `📦 Building and pushing docker image using Dockerfile...` in place of the build plan. When a deployment fails, the end of this log says why. CLI: `sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID"`. MCP: `get_deployment_logs`.

**12. Check the application**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domains. Request `/about` on the application's domain, then the JavaScript that it loads:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DOMAIN=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/domains" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.data[] | select(.is_primary) | .name')
curl -s -o /dev/null -w '%{http_code}\n' "https://$DOMAIN/about"
JS=$(curl -s "https://$DOMAIN/about" | grep -o '/assets/index-[^"]*\.js')
curl -s "https://$DOMAIN$JS" | grep -o 'https://api.example.com'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
200
https://api.example.com
```

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


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