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

# Run Playwright

> Run a Playwright service on Sevalla that takes screenshots and PDFs with headless Chromium: Ingot installs the browser and its libraries, or build your own image. Includes fonts and sizing.

A Playwright service runs on Sevalla as an application: Ingot, Sevalla's builder, sees `playwright` in your dependencies and installs Chromium, the Linux libraries it needs and a set of fonts into the image, and the web process runs a Node.js server that drives Chromium on the port that Sevalla sets in the `PORT` environment variable. This guide builds a service that returns a screenshot or a PDF of a page.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Node.js 24, npm, Playwright's Chromium), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `node server.js`, from `scripts.start` in `package.json` |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process |
| Services | None |
| Disk | None |
| Size | **S1** for the web process. See [Choose a size](#choose-a-size) |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `INGOT_RUN_PACKAGES` | No | Build and runtime | No | `fonts-noto-core`, only to [add fonts](#add-fonts). Ingot reads it during the build. |
| `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 or later on your computer.

## Create the project

Create a folder, install Playwright and download Chromium for local use:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
mkdir storefront && cd storefront
npm init -y
npm install playwright
npx playwright install chromium
npm pkg set type=module scripts.start="node server.js" engines.node="24.x"
```

The `playwright` package doesn't download browsers when you install it, so `npx playwright install chromium` downloads the Chromium release your Playwright version drives.

Create the server. It starts Chromium once, opens a new page for each request and returns a PNG or a PDF of `https://example.com`:

```js server.js theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import http from "node:http";
import { chromium } from "playwright";

const browser = await chromium.launch();

const server = http.createServer(async (req, res) => {
  if (req.url !== "/screenshot" && req.url !== "/pdf") {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Open /screenshot or /pdf to capture https://example.com");
    return;
  }

  // Each page gets its own browser context: no cookies or storage shared between requests
  const page = await browser.newPage();
  try {
    await page.goto("https://example.com");
    if (req.url === "/pdf") {
      const pdf = await page.pdf({ format: "A4" });
      res.writeHead(200, { "Content-Type": "application/pdf" });
      res.end(pdf);
    } else {
      const image = await page.screenshot({ type: "png" });
      res.writeHead(200, { "Content-Type": "image/png" });
      res.end(image);
    }
  } catch (error) {
    res.writeHead(500, { "Content-Type": "text/plain" });
    res.end(String(error));
  } finally {
    await page.close();
  }
});

server.listen(process.env.PORT || 3000);

process.on("SIGTERM", async () => {
  console.log("SIGTERM received, closing the browser");
  await browser.close();
  server.close(() => process.exit(0));
});
```

Keep installed packages out of the repository:

```text .gitignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
node_modules/
```

## Prepare the project for Sevalla

The `npm pkg set` command already set what [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), the default build strategy, reads from `package.json`:

| Field | Why Sevalla needs it |
| - | - |
| `"type": "module"` | Lets `server.js` use `import` and top-level `await`. |
| `scripts.start` | Ingot starts the web process with the `start` script. Without one, it runs the file in `main`, which `npm init` sets to `index.js`, a file this project doesn't have. |
| `engines.node` | Ingot 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). |

Because `playwright` is in `dependencies`, Ingot also installs the browser, with no Dockerfile:

* The final image runs `playwright install --with-deps chromium`, which installs the Chromium release that your locked Playwright version drives into `/usr/local/share/playwright`, with the Debian packages Chromium needs and a set of fonts.
* The image sets `PLAYWRIGHT_BROWSERS_PATH` to `/usr/local/share/playwright`, so `chromium.launch()` finds that Chromium.
* Playwright 1.55.0 and later install Debian 13's packages, which the Node.js 24 images are built on. With an older Playwright in `package-lock.json`, Ingot builds on the Node.js 24 images for Debian 12 (`node:24-bookworm-slim`) instead, and its build plan adds a `Debian` row that says why.

Ingot installs the browser for `playwright` or `@playwright/test` in `dependencies` only. In `devDependencies`, it treats Playwright as a test tool and installs no browser.

The server reads its port from [`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), and `server.listen()` listens on all network interfaces. Chromium needs no flags in a container: `chromium.launch()` already passes `--no-sandbox`, because Chromium's sandbox needs kernel privileges that containers don't get, and `--disable-dev-shm-usage`, which keeps Chromium's shared memory out of the small `/dev/shm`. Without the sandbox, open only pages you trust.

Run `npm start` and open `http://localhost:3000/screenshot` to check the service locally. Then commit everything and push it to your Git provider:

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

## Deploy to Sevalla

These steps build with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md). To build from your own Dockerfile instead, see [Deploy with a Dockerfile](#deploy-with-a-dockerfile).

**1. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose a **Data center**. Choose **S1** (1 GB of memory) or a larger **Size**: Chromium and every page it opens share the instance's memory, and **H1** has 0.3 GB. See [Choose a size](#choose-a-size). Click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**: creating an application doesn't deploy it.

**2. Deploy**

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

**3. Turn on auto-deploy**

To deploy every push to the branch automatically, click the **Settings** tab, then click **Update auto-deploy** in the **Auto-deploy** section. Turn on **Auto-deploy** and click **Update auto-deploy**. See [Deployments](https://docs.sevalla.com/applications/deployments.md#deploy-automatically-on-push).

## Verify the deployment

1. On the **Deployments** tab, click the deployment. Its **Build log** shows Ingot's [build plan](https://docs.sevalla.com/applications/builds/ingot.md#read-the-build-plan): Node.js 24 from `engines.node`, npm from `package-lock.json` and the start command from the `start` script.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Node.js 24 application ───────────────────────────────────╮
   │ Node.js          24              package.json engines "24.x" │
   │ Package manager  npm             package-lock.json           │
   │ Start            node server.js  package.json scripts.start  │
   ├─ Image ──────────────────────────────────────────────────────┤
   │ Build            node:24-trixie                              │
   │ Runtime          node:24-trixie-slim                         │
   │ User             1000:1000                                   │
   │ Init             tini                                        │
   │ Port             $PORT, default 8080                         │
   ╰─────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

   Further down, the step that installs the browser prints the release and its path, such as `Chrome Headless Shell 156.0.8078.4 (playwright chromium-headless-shell v1248) downloaded to /usr/local/share/playwright/chromium_headless_shell-1248`.
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`, and add `/screenshot` to the address. The page shows a screenshot of `https://example.com`. With `/pdf`, the browser opens a PDF of the same page.
4. Check the same addresses from a terminal:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://storefront-x7k2m.sevalla.app/; echo
   curl -s -o screenshot.png -w '%{http_code} %{content_type}\n' https://storefront-x7k2m.sevalla.app/screenshot
   curl -s -o page.pdf -w '%{http_code} %{content_type}\n' https://storefront-x7k2m.sevalla.app/pdf
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   Open /screenshot or /pdf to capture https://example.com
   200 image/png
   200 application/pdf
   ```

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

With the API, [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`, `success` once it's deployed, and [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns its build log, which ends with `Web has been deployed successfully.` See [Deploy with the API](#deploy-with-the-api).

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 instance restarts or `/screenshot` fails, read the application's [runtime logs](https://docs.sevalla.com/applications/logs.md) and see [Troubleshoot](#troubleshoot).

## Choose a size

Chromium and every page it has open share the instance's memory. When they need more than the size has, the instance restarts, and every request in progress fails. These are the peaks of one instance of this service, measured on Sevalla with pages of `https://example.com`:

| Pages open at once | Memory at the peak |
| - | - |
| None (Node.js and Chromium started) | 140 MB |
| 1 | 180 MB |
| 5 | 280 MB |
| 10 | 380 MB |

**H1** (0.3 GB) handled 5 requests at once and restarted at 10. **S1** (1 GB) handled 10 with room to spare. Real pages with images, fonts and scripts need more memory per page than `https://example.com`, so measure with your own: send the requests you expect at once, then run `cat /sys/fs/cgroup/memory.peak` in the application's [web terminal](https://docs.sevalla.com/applications/web-terminal.md), which prints the instance's peak memory in bytes since it started. [Get process exit codes](https://api-docs.sevalla.com/v3/applications/get-process-exit-codes) (`getProcessExitCodesV3`) returns `137` with `OOMKilled (SIGKILL). The process was killed because it exceeded its memory limit.` for each restart that ran out of memory.

When an instance runs out of memory, [scale](https://docs.sevalla.com/applications/scaling.md) the web process to a larger size, or run more instances so that each one renders fewer pages at once.

## Add fonts

The image has the fonts that Playwright installs with Chromium on Debian: Liberation, FreeFont and Unifont for Latin, Greek, Cyrillic, Arabic, Hebrew, Devanagari, Bengali and Tamil, IPA Gothic for Japanese, WenQuanYi Zen Hei for Chinese and Korean, Loma for Thai, and Noto Color Emoji. Text in these scripts renders without boxes. To render with another font family, such as Noto, install it with Ingot's `INGOT_RUN_PACKAGES` variable, which adds Debian packages to the final image:

**1. Add the variable**

Click the application's **Environment variables** tab, then click **Add environment variables**. Enter `INGOT_RUN_PACKAGES` as the **Key** and `fonts-noto-core` as the **Value**. Keep **Available during buildtime** selected, because Ingot reads the variable during the build, and click **Add environment variables**.

**2. Deploy**

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

After the deployment, pages can use `Noto Sans` and `Noto Serif` in their CSS. To keep the packages in the repository instead, set `packages.run` in an `ingot.json` file. See [Configure the build](https://docs.sevalla.com/applications/builds/ingot.md#configure-the-build). With your own Dockerfile, add `fonts-noto-core` to the `apt-get install` line in [Deploy with a Dockerfile](#deploy-with-a-dockerfile) instead. To add the variable with the API, see [Add fonts with the API](#add-fonts-with-the-api).

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. This one installs only Chromium's headless shell, the build that `chromium.launch()` runs, which keeps the image smaller than Ingot's. 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

WORKDIR /app
ENV NODE_ENV=production PORT=8080 PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# Debian's packages get the base image's security updates, so they aren't pinned
# hadolint ignore=DL3008
RUN apt-get update \
  && apt-get install -y --no-install-recommends tini \
  && npx playwright install --with-deps --only-shell chromium \
  && rm -rf /var/lib/apt/lists/*

COPY . .

USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]
```

| Line or choice | Why |
| - | - |
| `node:24-trixie-slim` | The official Node.js 24 image on Debian 13, the same line that `engines.node` pins for Ingot. The tag follows Node.js 24's patch releases, so each build gets its security fixes; a digest would stay on one release. |
| `package.json` and `package-lock.json` first | `npm ci --omit=dev` installs the exact versions in the lock file, without devDependencies. Copying the two files before the code lets Docker reuse the installed dependencies and the browser when only the code changes. |
| `playwright install --with-deps --only-shell chromium` | Playwright's installer downloads the Chromium headless shell that the locked Playwright version drives, and `--with-deps` installs the Debian packages and fonts it needs. Playwright supports only the browser builds it was released with, so update the browser by updating `playwright`. |
| `PLAYWRIGHT_BROWSERS_PATH=/ms-playwright` | The installer and `chromium.launch()` both use this folder. Without it, the browser lands in root's home folder, which the server's user can't read. |
| `tini` | Runs as the first process: it passes `SIGTERM` to Node.js and reaps the processes Chromium leaves behind. |
| `USER 1000:1000` | The service runs as the image's unprivileged `node` user. The code, the dependencies and the browser stay owned by root, so it can read them but not change them. Chromium keeps its profile in `/tmp`. |
| `PORT=8080` and `EXPOSE 8080` | `server.js` listens on `PORT`, which Sevalla sets at runtime. The image's default matches Sevalla's, for running it on your computer. |

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

Commit and push both files. Ingot doesn't use the `Dockerfile`, so you can keep it in a repository that builds with Ingot, but it adds the `.dockerignore` patterns to its own, and its build plan lists them in an `Ignored` row. Then switch the application to the Dockerfile build strategy:

**1. Choose the Dockerfile build strategy**

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

**2. Deploy**

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

![The Update build strategy sheet with Dockerfile selected, the Dockerfile path, the Docker context and the Docker registry credential](https://assets.sevalla.com/screenshots/light/app-settings-build-strategy-dockerfile.png)

The deployment's **Build log** shows `📦 Building and pushing docker image using Dockerfile...` instead of Ingot's build plan, then installs the headless shell. Its **Dockerfile** tab shows your `Dockerfile`. When the deployment is **Deployed**, `/screenshot` and `/pdf` work again, and the `curl` commands in [Verify the deployment](#verify-the-deployment) print the same lines. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) for build arguments and private base images, and [Deploy with the API](#build-from-the-dockerfile-with-the-api) to switch the build strategy with the API.

## Deploy changes

With **Auto-deploy** on, every push to the branch starts a deployment; otherwise, click **Deploy now** for each change. Each deployment builds the image, then replaces the web process's instances: each old instance gets `SIGTERM`, and its runtime log shows `SIGTERM received, closing the browser`. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Run browser jobs in the background

Screenshots and PDFs of long pages can take longer than a visitor waits. Run them in a [background worker](https://docs.sevalla.com/applications/processes.md) process that takes work from a queue in a Sevalla [Redis database](https://docs.sevalla.com/guides/javascript/bullmq.md), and store the results in [object storage](https://docs.sevalla.com/guides/javascript/object-storage.md) instead of the instance's disk. The worker runs from the same image as the web process, so it has Chromium too.

## 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. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| Requests return `503` with `upstream connect error or disconnect/reset before headers. reset reason: connection termination`, then work again | The instance ran out of memory and restarted. [Get process exit codes](https://api-docs.sevalla.com/v3/applications/get-process-exit-codes) returns `137` | Scale the web process to a larger size or more instances. See [Choose a size](#choose-a-size). |
| The web process stops at start with `browserType.launch: Executable doesn't exist at` and a path in `.cache/ms-playwright` | The image has no browser: `playwright` is in `devDependencies`, or a Dockerfile doesn't run `playwright install` or set `PLAYWRIGHT_BROWSERS_PATH` | Move `playwright` to `dependencies`, or install the browser as in [Deploy with a Dockerfile](#deploy-with-a-dockerfile). |
| The web process stops at start with `browserType.launch: Target page, context or browser has been closed`, and the log shows `error while loading shared libraries: libglib-2.0.so.0` | A Dockerfile installed the browser without the Debian packages it needs | Run `playwright install` with `--with-deps`. |

For more causes, see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds) and [Troubleshoot builds and deployments](https://docs.sevalla.com/applications/troubleshooting/builds-and-deployments.md).

## Deploy with the API

These calls make the same deployment as [Deploy to Sevalla](#deploy-to-sevalla), 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` and `jq`.

**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`, its `display_name` and its region ID in `location` (see [Locations](https://docs.sevalla.com/platform/locations.md)). Keep the ID of the one you want, here Frankfurt, Germany:

```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 at **S1**, the size this guide needs, and auto-deploy off. It builds with Ingot (`build_type` is `ingot`) and isn't deployed yet:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"storefront","cluster_id":"'"$CLUSTER_ID"'","source":"privateGit","git_type":"github","repo_url":"https://github.com/acme/storefront","default_branch":"main"}' \
  | jq -r .id)
```

CLI: `sevalla apps create --name storefront --cluster $CLUSTER_ID --source privateGit --git-type github --repository https://github.com/acme/storefront --branch main`. MCP: `create_application` with `deploy` set to `false`, because it deploys by default.

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

CLI: `sevalla apps update $APP_ID --data '{"auto_deploy":true}'`. MCP: `sevalla_api_update` with `updateApplicationV3`.

**4. Deploy**

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

**5. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`: `waiting` and `inProgress` while it runs, then `success` or `failed`. Poll it every 15 seconds, for up to 30 minutes, until the status is final:

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

CLI: `sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID`. MCP: `get_deployment`.

**6. Read the build log**

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, oldest line first: the build plan from [Verify the deployment](#verify-the-deployment), the build and the rollout, which ends with `Web has been deployed successfully.` When the status is `failed`, the last lines say why. Without `limit`, the call returns the latest 1,000 lines, and installing the browser alone prints more, so pass `5000`, the most it allows.

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

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

**7. Check the application**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domain. Request the home page, a screenshot and a PDF from it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
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')
curl -s https://$HOST/; echo
curl -s -o screenshot.png -w '%{http_code} %{content_type}\n' https://$HOST/screenshot
curl -s -o page.pdf -w '%{http_code} %{content_type}\n' https://$HOST/pdf
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Open /screenshot or /pdf to capture https://example.com
200 image/png
200 application/pdf
```

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

### Build from the Dockerfile with the API

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

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}' \
  | jq -c '{build_type, dockerfile_path, docker_context}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}
```

CLI: `sevalla apps update $APP_ID --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'`. MCP: `sevalla_api_update` with `updateApplicationV3`.

### Add fonts with the API

To [add fonts](#add-fonts) to the Ingot build, [Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds `INGOT_RUN_PACKAGES`. Without `is_runtime` and `is_buildtime`, the variable is available during the build and at runtime, and Ingot reads it during the build. Then repeat the **Deploy**, **Wait for the deployment** and **Check the application** steps:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/env-vars \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"INGOT_RUN_PACKAGES","value":"fonts-noto-core"}' \
  | jq -c '{key, is_runtime, is_buildtime}'
```

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

CLI: `sevalla apps env-vars create $APP_ID --key INGOT_RUN_PACKAGES --value fonts-noto-core`. MCP: `set_environment_variables`.

The build plan doesn't list the package. The new deployment's build log shows Debian installing it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -G "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID/logs?limit=5000" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  --data-urlencode "search=Setting up fonts-noto-core" \
  | jq -r '.[].message | sub("^#[0-9]+ [0-9.]+ "; "")'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Setting up fonts-noto-core (20201225-2) ...
```


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