> ## 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 an Angular application

> Deploy an Angular 22 application with server-side rendering to Sevalla, built with Ingot or a Dockerfile, or publish an Angular app without SSR as a free static site.

An Angular application with server-side rendering runs on Sevalla as an application: Ingot, Sevalla's builder, detects Angular and `@angular/ssr` in your Git repository, runs `ng build`, and the web process runs the Express server that Angular builds, `node dist/storefront/server/server.mjs`. An Angular app without server-side rendering builds to static files only, and you can [publish it as a static site](#deploy-an-angular-app-without-server-side-rendering) instead.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Node.js 24, npm at the version in `packageManager`), or the [Dockerfile](#deploy-with-a-dockerfile). Without server-side rendering, a [static site](#deploy-an-angular-app-without-server-side-rendering) |
| Start command | `node dist/storefront/server/server.mjs`, which Ingot sets for Angular with server-side rendering |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process |
| Services | None |
| Disk | None |
| Size | **H1** for the web process |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `NG_ALLOWED_HOSTS` | Yes | Runtime | No | The application's domain without `https://`, such as `storefront-x7k2m.sevalla.app`. Separate several domains with commas |
| `NG_TRUST_PROXY_HEADERS` | Yes | Runtime | No | `x-forwarded-for,x-forwarded-proto` |
| `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).
* Node.js 24.15 or later on your computer. Angular 22 needs Node.js 22.22.3 or later in the 22 line, 24.15 or later in the 24 line, or 26.

## Create the project

Create a workspace with server-side rendering:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
NG_CLI_ANALYTICS=false npx @angular/cli@latest new storefront --defaults --ssr --style=css --ai-config=none --package-manager=npm
cd storefront
```

`--ssr` adds `@angular/ssr` and an Express server in `src/server.ts`. The other flags, and `NG_CLI_ANALYTICS=false` for the question about sharing usage data, answer the CLI's questions, so the command runs without prompts and installs the dependencies with npm. The workspace comes with a `.gitignore` and a Git repository with a first commit.

## Prepare the project for Sevalla

### Render each request on the server

`ng new` sets every route to prerender: `ng build` renders the pages once, and the server sends the same HTML to everyone. To render each request on the server instead, set the render mode to `RenderMode.Server` in `src/app/app.routes.server.ts`:

```ts src/app/app.routes.server.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: '**',
    renderMode: RenderMode.Server
  }
];
```

Keep `RenderMode.Prerender` for pages that are the same for every visitor; the server sends them as built.

### Stop the server gracefully

When Sevalla replaces an instance, during a deployment for example, it sends `SIGTERM`. Angular's server doesn't handle it, so requests in progress would be cut off. In `src/server.ts`, keep the server that `app.listen()` returns and close it on `SIGTERM`. Replace the block that starts the server with this:

```ts src/server.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
if (isMainModule(import.meta.url) || process.env['pm_id']) {
  const port = process.env['PORT'] || 4000;
  const server = app.listen(port, (error) => {
    if (error) {
      throw error;
    }

    console.log(`Node Express server listening on http://localhost:${port}`);
  });

  process.on('SIGTERM', () => {
    console.log('SIGTERM received, closing the server');
    server.close();
  });
}
```

`server.close()` stops accepting connections and lets the requests in progress finish, then Node.js exits.

### Pin the Node.js version

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

| File | Why Sevalla needs it |
| - | - |
| `package.json` `engines.node` | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) builds on the Node.js 24 line. Without it, Ingot uses its default line, 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). |
| `package.json` `packageManager` | `ng new` sets it to the npm version that created the project, and Ingot installs that npm version for the build. |
| `package-lock.json` | Ingot installs the exact versions in it with `npm ci`. |
| `angular.json` | Ingot reads the build's `outputPath` and `outputMode` from it to find the server: `dist/storefront/server/server.mjs`. |
| `src/server.ts` | The server listens on [`PORT`](https://docs.sevalla.com/guides/javascript/overview#listen-on-the-port-environment-variable), which Sevalla sets to the web process's port (`8080` by default), on all network interfaces. |

Ingot needs no start command: the `start` script runs `ng serve`, Angular's development server, so Ingot ignores it and starts the server that `ng build` writes. It removes the `devDependencies` after the build; the server doesn't need them, because `ng build` bundles Express and Angular into `server.mjs`.

### Allow the domains and the proxy headers

Angular's server answers only requests for host names it knows, to protect server-side requests from forged `Host` headers. Any other host gets status `400` and `Header "host" with value "<host>" is not allowed.` You give it the list in the `NG_ALLOWED_HOSTS` environment variable.

It also ignores the `X-Forwarded-For` and `X-Forwarded-Proto` headers that Sevalla's proxy adds to every request, and logs `Received "x-forwarded-proto" header but "trustProxyHeaders" was not set up to allow it.` for each one, unless you list them in `NG_TRUST_PROXY_HEADERS`. Trusting `X-Forwarded-Proto` also tells the server that the request came over HTTPS.

The [deployment steps](#deploy-to-sevalla) set both variables on Sevalla. To check the production build locally, set `NG_ALLOWED_HOSTS` to `localhost`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm run build
NG_ALLOWED_HOSTS=localhost npm run serve:ssr:storefront
```

Open `http://localhost:4000`. The Angular starter page shows **Hello, storefront**. Then push the repository that `ng new` created to your Git provider:

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

## Deploy to Sevalla

These steps build 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. Add the environment variables**

Click the **Environment variables** tab, then click **Add environment variables**. Add these variables and click **Add environment variables**:

| Variable | Value |
| - | - |
| `NG_ALLOWED_HOSTS` | The application's domain without `https://`, for example `storefront-x7k2m.sevalla.app`. It's under the application's name in the page header. |
| `NG_TRUST_PROXY_HEADERS` | `x-forwarded-for,x-forwarded-proto` |

**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 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): Node.js 24 from `engines.node`, npm at the version in `packageManager`, Angular with server-side rendering, and the server it starts.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Angular (SSR) · Node.js 24 ───────────────────────────────────────────────────────╮
   │ Node.js          24                                      package.json engines "24.x" │
   │ Package manager  npm 11.19.0                             package.json packageManager │
   │ Framework        Angular (SSR)                           package.json                │
   │ Start            node dist/storefront/server/server.mjs  Angular (SSR)               │
   │ DevDependencies  removed after the build                                             │
   ├─ Image ──────────────────────────────────────────────────────────────────────────────┤
   │ Build            node:24-trixie                                                      │
   │ Runtime          node:24-trixie-slim                                                 │
   │ User             1000:1000                                                           │
   │ Init             tini                                                                │
   │ Port             $PORT, default 8080                                                 │
   ╰─────────────────────────────────────────────────────────────────────── Ingot v0.4.1 ─╯

   ! Angular SSR rejects requests for hosts it doesn't know: list your domains in NG_ALLOWED_HOSTS (comma-separated) or in security.allowedHosts in angular.json
   ```

   The plan's warning about `NG_ALLOWED_HOSTS` shows on every build: Ingot reads `angular.json`, not the application's environment variables.
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 Angular starter page opens with **Hello, storefront**. In the page source, the `<h1>` already holds `Hello, storefront` and `<app-root>` has `ng-server-context="ssr"`: the server rendered the page for this request.

![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 the last steps of [Deploy with the API](#deploy-with-the-api) do, then request the domain:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://storefront-x7k2m.sevalla.app/ | grep -o -e 'ng-server-context="ssr"' -e 'Hello, storefront'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
ng-server-context="ssr"
Hello, storefront
```

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 the domain answers `Header "host" with value "storefront-x7k2m.sevalla.app" is not allowed.`, the domain isn't in `NG_ALLOWED_HOSTS`: fix the value, then deploy again, because variables apply at the next deployment.

## Deploy with a Dockerfile

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

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

FROM node:24-trixie-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
USER 1000:1000
EXPOSE 8080
CMD ["node", "dist/storefront/server/server.mjs"]
```

| Line or choice | Why |
| - | - |
| `node:24-trixie-slim` | Node.js 24, the line `engines.node` pins, on Debian 13. A line tag, not a digest, so each build picks up Node.js and Debian security fixes. |
| Two stages | The build stage installs every dependency and runs `ng build`. The final image gets only `dist`: `server.mjs` bundles Express and Angular, so it needs no `node_modules`. |
| `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. |
| `USER 1000:1000` | The server runs as the image's `node` user, not root. The copied files stay owned by root, so the server can read its code but not change it. |
| `CMD` in exec form | Node.js runs as the container's main process and gets `SIGTERM` directly, so the handler in `src/server.ts` closes the server. Without that handler, Node.js would ignore the signal as the main process; add an init process such as `tini` if you remove it. |
| `EXPOSE 8080` | Documents Sevalla's default `PORT`. The server reads `PORT` when it starts. |

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

Commit and push both files. Ingot doesn't use the `Dockerfile` and adds your `.dockerignore` patterns to its own, so you can keep both 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. `NG_ALLOWED_HOSTS` and `NG_TRUST_PROXY_HEADERS` still apply: the server reads them when it starts. When the status changes to **Deployed**, the domain opens the starter page with **Hello, storefront**, rendered by the server. 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; otherwise, click **Deploy now** for each change. Each deployment builds the image, then replaces the web process's instances, and the old instances finish their requests before they stop. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Add configuration on the application's **Environment variables** tab. Code that runs on the server, such as `src/server.ts`, reads it with `process.env` when the instance starts; variables apply at the next deployment. Code that runs in the browser can't read the server's environment. 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. Then add the domain to `NG_ALLOWED_HOSTS`, separated by a comma, for example `storefront-x7k2m.sevalla.app,shop.example.com`, and deploy. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Deploy an Angular app without server-side rendering

An Angular app created without `--ssr` builds to HTML, CSS and JavaScript files only, and Sevalla can publish it as a [static site](https://docs.sevalla.com/static-sites/overview.md), free up to 100 GB of bandwidth a month:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
NG_CLI_ANALYTICS=false npx @angular/cli@latest new storefront --defaults --no-ssr --style=css --ai-config=none --package-manager=npm
```

`ng build` writes the app to `dist/storefront/browser`. Push the repository to your Git provider, then create the 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. Set the build settings**

Under **Build settings**, set these values:

| 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/storefront/browser` |
| **Error file** | `index.html` |

**3. 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** answers every path that has no file with `index.html`, so the Angular router renders its routes when someone opens or reloads them. **Latest LTS** meets Angular 22's Node.js requirement; static sites don't read `engines` in `package.json`. When the deployment's status changes to **Deployed**, click **Visit site** on the static site's **Overview** tab: the Angular starter page opens on the site's `*.sevalla.page` domain. From a terminal, any path returns the app's `index.html`, which the browser renders:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://storefront-x7k2m.sevalla.page/any/route | grep -o '<app-root>'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<app-root>
```

See [Build settings](https://docs.sevalla.com/static-sites/build-settings.md) for every field, and [Troubleshoot static sites](https://docs.sevalla.com/static-sites/troubleshooting.md) if the deployment fails.

## Deploy with the API

The [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) makes the same deployment as [Deploy to Sevalla](#deploy-to-sevalla) 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: the application with server-side rendering, and the static site of [Deploy an Angular app without server-side rendering](#deploy-an-angular-app-without-server-side-rendering). 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 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 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. Find the domain**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domains. `NG_ALLOWED_HOSTS` needs the primary one. Sevalla adds the domain a few seconds after it creates the application, so the loop asks until the domain is there:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 30); do 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'); [ -n "$DOMAIN" ] && break;
  sleep 2
done
echo "$DOMAIN"
```

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

**7. Add the environment variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds one variable per call. The server reads both variables when it starts, so they need to be available at runtime only. The `add_var` function sends each one, with `jq` building the JSON body:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
add_var() {
  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 -n --arg key "$1" --arg value "$2" \
      '{key: $key, value: $value, is_runtime: true, is_buildtime: false}')" \
    | jq -r .key
}
add_var NG_ALLOWED_HOSTS "$DOMAIN"
add_var NG_TRUST_PROXY_HEADERS x-forwarded-for,x-forwarded-proto
```

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

CLI: `sevalla apps env-vars create "$APP_ID" --key <key> --value <value>` for each variable, which makes it available during both the build and runtime. MCP: `set_environment_variables` with all the variables in one call, `runtime` set to `true` and `buildtime` set to `false`.

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

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

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

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

**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, one line per entry. With Ingot, the `Start` row of the build plan shows the start command:

```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 '│ Start' | tr -s ' '
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
│ Start node dist/storefront/server/server.mjs Angular (SSR) │
```

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

**13. Check the application**

Request the application on its domain. The HTML holds the heading and the `ng-server-context="ssr"` attribute that the server adds when it renders the page:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://$DOMAIN/" | grep -o -e 'ng-server-context="ssr"' -e 'Hello, storefront'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
ng-server-context="ssr"
Hello, storefront
```

### Deploy the static site

The static site needs an API key with the `STATIC_SITE:CREATE`, `STATIC_SITE:READ` and `STATIC_SITE:UPDATE` permissions, or the **Company developer** role.

**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**. A static site has no location.

```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/storefront/browser",
  "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/storefront/browser`. 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. 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`.

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

**4. 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`. The home page is the empty `<app-root>` element that the browser renders, and a path with no file gets the same `index.html`, the error file, so the router can render it:

```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)
curl -s "https://$SITE/" | grep -o '<app-root>'
curl -s "https://$SITE/any/route" | grep -o '<app-root>'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<app-root>
<app-root>
```

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


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