> ## 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 Qwik City application

> Deploy a Qwik City application to Sevalla with the Node.js server adapter: add the adapter, handle SIGTERM, and build with Ingot or a Dockerfile.

A Qwik City application runs on Sevalla as an application with Qwik's Node.js server adapter. Ingot, Sevalla's builder, detects Qwik City in your Git repository, runs `qwik build`, and the web process runs the adapter's `serve` script, `node server/entry.node-server`, which listens on the port that Sevalla sets in the `PORT` environment variable.

## At a glance

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

### Environment variables

The application needs no environment variables of its own. Sevalla sets `PORT` for the web process.

## 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 Qwik's `empty` starter, which includes Qwik City's routing, and install the dependencies:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm create qwik@latest empty storefront
cd storefront
npm install
```

Add the Node.js server adapter:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npx qwik add node-server --skipConfirmation=true
```

The adapter adds `src/entry.node-server.tsx`, a server built on Node.js's `http` module, and two scripts: `build.server`, which `npm run build` runs, and `serve`, which starts the server. The project comes with a `.gitignore`.

## Prepare the project for Sevalla

### Stop the server gracefully

When Sevalla replaces an instance, during a deployment for example, it sends `SIGTERM`. The adapter's server doesn't handle it, so requests in progress would be cut off. At the end of `src/entry.node-server.tsx`, after `server.listen(...)`, close the server on `SIGTERM`:

```tsx src/entry.node-server.tsx theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
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` `scripts.serve` | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), the default build strategy, starts the web process with it when it detects Qwik City. The `start` script runs Vite's development server, so Ingot doesn't use it. |
| `package.json` `engines.node` | The starter allows Node.js 18 and later, so Ingot would pick its default line, Node.js 24 today, which a new Ingot release can move. `24.x` keeps the build on Node.js 24. See [Pin the Node.js version](https://docs.sevalla.com/guides/javascript/overview.md#pin-the-node-js-version). |
| `package-lock.json` | Ingot installs the exact versions in it with `npm ci`. |
| `src/entry.node-server.tsx` | 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. |

Qwik's starter lists every package in `devDependencies`, including the ones the server imports when it runs, so Ingot keeps the `devDependencies` in the image. The build plan's **DevDependencies** row says so.

Run `npm run build && npm run serve` and open `http://localhost:3004` to check the production build locally. Then commit everything and push it to your Git provider:

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

## Deploy to Sevalla

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

**1. Create the application**

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

**2. Deploy**

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

**3. Turn on auto-deploy**

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

## Verify the deployment

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

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Qwik City · Node.js 24 ───────────────────────────────────────────────────────────────────────────────────────────────╮
   │ Node.js          24                             package.json engines "24.x"                                              │
   │ Package manager  npm                            package-lock.json                                                        │
   │ Framework        Qwik City                      package.json                                                             │
   │ Start            node server/entry.node-server  package.json scripts.serve                                               │
   │ DevDependencies  kept                           Qwik City's server imports packages its starters list as devDependencies │
   ├─ Image ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
   │ Build            node:24-trixie                                                                                          │
   │ Runtime          node:24-trixie-slim                                                                                     │
   │ User             1000:1000                                                                                               │
   │ Init             tini                                                                                                    │
   │ Port             $PORT, default 8080                                                                                     │
   ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

2. Wait for the deployment's status to change to **Deployed**. The application's status next to its name changes to **Running**.

3. Click the domain under the application's name, for example `storefront-x7k2m.sevalla.app`. The starter's page shows **Hi 👋**, rendered by the server.

![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 'Hi 👋'
```

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

If the deployment fails, read its build log, then see [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds). A build log that shows `Qwik City has no server adapter, so its build makes nothing to run` means the project has no adapter: run `npx qwik add node-server --skipConfirmation=true`, commit and push.

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

The server build imports one package at runtime, `undici`, which the starter lists in `devDependencies`. Move it to `dependencies`, so the Dockerfile can leave every other development package out of the image:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm install --save-prod undici
```

Then add a `Dockerfile` and a `.dockerignore` to the root of the repository:

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

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

| 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, runs `qwik build`, then removes the `devDependencies` with `npm prune --omit=dev`, which leaves `undici`. The final image gets `server` (the server build), `dist` (the files it serves to browsers) and `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. |
| `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/entry.node-server.tsx` 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
node_modules
dist
server
.env*
*.log
Dockerfile
.dockerignore
```

Commit and push the changes. 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. When the status changes to **Deployed**, the domain shows **Hi 👋**. If the runtime logs show `Cannot find package 'undici'`, `undici` is still in `devDependencies`: run `npm install --save-prod undici`, commit and push. 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. Server code reads it with `process.env` at runtime; variables apply at the next deployment. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## Add a custom domain

To serve the application from your own domain, such as `shop.example.com`, add the domain on the application's **Domains** tab and create the DNS records that Sevalla shows. Custom domains need a web process larger than **H1**, so [scale](https://docs.sevalla.com/applications/scaling.md) the web process first. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Deploy with the API

The [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.

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

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

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

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

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

**10. 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 server/entry.node-server package.json scripts.serve │
```

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

**11. Check the application**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domains. Request the application on its domain:

```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 "https://$DOMAIN/" | grep -o 'Hi 👋'
```

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

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.