> ## 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 Fiber application

> Deploy a Fiber v3 JSON API to Sevalla: trust Sevalla's proxy for c.IP() and c.Scheme(), shut down gracefully on SIGTERM, and build with Ingot or a Dockerfile.

A Fiber application runs on Sevalla as a Go application: [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's builder, downloads the modules in `go.sum`, compiles the `main` package into a static binary, and the web process runs that binary, `/usr/local/bin/storefront`. Fiber listens on the port that Sevalla sets in the `PORT` environment variable, reads the visitor's address and scheme from Sevalla's proxy, and shuts down gracefully when Sevalla sends `SIGTERM`.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Go 1.27), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `/usr/local/bin/storefront`, the binary Ingot builds from the `main` package |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process |
| Services | None |
| Disk | None |
| Health check | `GET /healthz` returns `200` (readiness probe) |
| Size | **H1** for the web process |

### Environment variables

The application needs no environment variables of its own. It reads `PORT`, which Sevalla sets 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).
* Go 1.27 on your computer.

## Create the project

Create a folder and a Go module, and add Fiber. The last part of the module path, `storefront`, becomes the name of the binary:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
mkdir storefront && cd storefront
go mod init github.com/acme/storefront
go get -u github.com/gofiber/fiber/v3
```

`go get` installs Fiber 3.6, the current major version, and `-u` updates its dependencies, such as `fasthttp`, the HTTP server Fiber runs on, to their newest releases. Fiber v3 handlers take a `fiber.Ctx`.

Create the server, a JSON API with a health endpoint:

```go main.go theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"strconv"
	"syscall"

	"github.com/gofiber/fiber/v3"
)

type Product struct {
	ID    int     `json:"id"`
	Name  string  `json:"name"`
	Price float64 `json:"price"`
}

var products = []Product{
	{ID: 1, Name: "Espresso beans", Price: 14.5},
	{ID: 2, Name: "Pour-over kettle", Price: 39},
}

func main() {
	app := fiber.New(fiber.Config{
		// Read proxy headers only from Sevalla's proxy, which connects from a private address
		TrustProxy:       true,
		TrustProxyConfig: fiber.TrustProxyConfig{Private: true},
		ProxyHeader:      "CF-Connecting-IP",
	})

	app.Get("/healthz", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"status": "ok"})
	})
	app.Get("/api/products", func(c fiber.Ctx) error {
		return c.JSON(products)
	})
	app.Get("/api/products/:id", func(c fiber.Ctx) error {
		id, _ := strconv.Atoi(c.Params("id"))
		for _, p := range products {
			if p.ID == id {
				return c.JSON(p)
			}
		}
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "product not found"})
	})
	app.Get("/api/request", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"ip": c.IP(), "scheme": c.Scheme()})
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	// Shut down gracefully on SIGINT and SIGTERM
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	if err := app.Listen(":"+port, fiber.ListenConfig{GracefulContext: ctx}); err != nil {
		log.Fatal(err)
	}
	log.Print("Server stopped")
}
```

Record Fiber as a direct dependency of the module:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
go mod tidy
```

* The server listens on [`PORT`](https://docs.sevalla.com/guides/go/overview#how-go-applications-run-on-sevalla), which Sevalla sets to the web process's port (`8080` by default), on all network interfaces. On your computer, it uses port 8080.
* The three proxy settings in `fiber.Config` make `c.IP()` and `c.Scheme()` return what the visitor used. See [Trust the proxy headers](#trust-the-proxy-headers).
* `GracefulContext` shuts the server down when the context ends. See [Shut down gracefully](#shut-down-gracefully).

Keep the binary that `go build` writes out of the repository:

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

## Prepare the project for Sevalla

The project needs no build or start configuration:

| File | Why Sevalla needs it |
| - | - |
| `go.mod` | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) recognizes a Go module by it. Its `go` line, which `go mod init` sets to the Go release you run, such as `go 1.27.1`, is the oldest Go the module builds with. Ingot builds on the newest Go line that satisfies it, Go 1.27. See [Pin the Go version](https://docs.sevalla.com/guides/go/overview.md#pin-the-go-version). |
| `go.sum` | The checksums of every module the build downloads. Ingot runs `go mod download` from `go.mod` and `go.sum` before it copies the rest of the code, so a code change reuses the downloaded modules. Commit it. |
| `main.go` | The `main` package at the root of the module. Ingot compiles it into `/usr/local/bin/storefront` and starts the web process with that binary. |

Nothing in Fiber's dependencies imports `"C"`, so Ingot builds with `CGO_ENABLED=0`, and the binary needs no C library at runtime.

Run `go run .` and open `http://localhost:8080/api/products` to check the server 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): Go 1.27 from the `go` line of `go.mod`, and the start command from the `main` package at the root.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Go 1.27 application ──────────────────────────────────────╮
   │ Go       1.27                       go.mod "go 1.27.1"       │
   │ Start    /usr/local/bin/storefront  main package at the root │
   ├─ Image ──────────────────────────────────────────────────────┤
   │ Build    golang:1.27-trixie                                  │
   │ Runtime  debian: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`, and add `/api/products` to the address. The response is:

   ```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   [{"id":1,"name":"Espresso beans","price":14.5},{"id":2,"name":"Pour-over kettle","price":39}]
   ```

   `/api/products/2` returns one product, `/api/products/9` returns `{"error":"product not found"}` with status `404`, `/healthz` returns `{"status":"ok"}`, and `/api/request` returns your own IP address and the scheme `https`.

![The application header with its name, Running status, repository, branch and the domain that opens the application](https://assets.sevalla.com/screenshots/light/app-overview-address.png)

The application's [runtime logs](https://docs.sevalla.com/applications/logs.md) show Fiber's start banner with `Server started on`. If the deployment fails, read its build log, then see [Troubleshoot](#troubleshoot).

To check the deployment from a terminal or an AI agent, read its `status` with [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until it's `success`, and its build log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`), as in [Deploy with the API](#deploy-with-the-api). Then request the endpoints:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -w '\n' https://storefront-x7k2m.sevalla.app/api/products/2
curl -s -o /dev/null -w '%{http_code}\n' https://storefront-x7k2m.sevalla.app/api/products/9
curl -s -w '\n' https://storefront-x7k2m.sevalla.app/healthz
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"id":2,"name":"Pour-over kettle","price":39}
404
{"status":"ok"}
```

[Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) with `search` finds the line of Fiber's start banner:

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

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
INFO Server started on: 	http://127.0.0.1:8080 (bound on host 0.0.0.0 and port 8080)
```

## Trust the proxy headers

Every request reaches the application through Cloudflare and Sevalla's proxy, which connects to the web process from a private network address and sends the visitor's details in headers. Fiber ignores those headers unless you trust the proxy: by default, `c.IP()` returns the proxy's address and `c.Scheme()` returns `http`, even for an HTTPS request.

| Setting | What it does |
| - | - |
| `TrustProxy: true` | Reads proxy headers, but only from the addresses in `TrustProxyConfig` |
| `TrustProxyConfig: fiber.TrustProxyConfig{Private: true}` | Trusts connections from private addresses, such as `10.0.0.0/8`, which is where Sevalla's proxy connects from. Then `c.Scheme()` reads `X-Forwarded-Proto` and returns `https`. |
| `ProxyHeader: "CF-Connecting-IP"` | `c.IP()` returns the header Cloudflare sets to the visitor's address. Cloudflare refuses requests that already carry it, so a client can't fake it. |

Don't use `X-Forwarded-For` as the `ProxyHeader`: it lists every address the request passed through, starting with whatever the client sent, so it isn't the visitor's address. With `CF-Connecting-IP`, a request sent with `X-Forwarded-For: 203.0.113.99` still gets the visitor's real address from `/api/request`. On your computer, the connection comes from `127.0.0.1`, which isn't a private address, so `c.IP()` returns `127.0.0.1` and the headers are ignored. See [Request headers](https://docs.sevalla.com/applications/http-headers.md) for every header Sevalla and Cloudflare add.

## Shut down gracefully

When Sevalla stops an instance, during a deployment, a restart or a scale-down, it waits 5 seconds, sends `SIGTERM`, and stops the instance if it's still running 30 seconds after the shutdown began. See [How processes run](https://docs.sevalla.com/applications/processes.md#how-processes-run).

`signal.NotifyContext` turns `SIGTERM` into a cancelled context. With the context as `GracefulContext`, Fiber stops accepting connections and waits up to 10 seconds, its default `ShutdownTimeout`, for the requests in progress, then `app.Listen` returns and the server logs `Server stopped`. That line ends the old instance's [runtime logs](https://docs.sevalla.com/applications/logs.md) on every deployment.

## Add a health check

A readiness probe makes each new instance wait for `/healthz` to answer before it gets traffic, so a deployment switches over only to instances that serve requests.

**1. Open the web process**

On the application's **Processes** tab, click **⋯** on the **Web** process, then click **Update process**.

**2. Turn on the readiness probe**

Under **Health checks**, turn on **Readiness probe** and set the **Path** to `/healthz`. Keep the **Port** at the web process's port, `8080` unless you changed it.

**3. Save and deploy**

Click **Update process**, then click **Deploy** in the undeployed changes bar at the bottom of the page.

The deployment's status changes to **Deployed** once the new instance answers the probe. See [Health checks](https://docs.sevalla.com/applications/processes.md#health-checks) for every probe setting.

[Deploy with the API](#deploy-with-the-api) sets the same probe. [Get process](https://api-docs.sevalla.com/v3/applications/get-process) (`getProcessV3`) returns it in `readiness_probe`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -c .readiness_probe
```

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

## 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. 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 golang:1.27-trixie AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /storefront .

FROM alpine:3.24
RUN adduser -D -u 1000 app
COPY --from=build /storefront /usr/local/bin/storefront
USER 1000:1000
EXPOSE 8080
CMD ["storefront"]
```

| Line or choice | Why |
| - | - |
| `golang:1.27-trixie` | The Go 1.27 line, on Debian 13, the image Ingot builds with too. A line tag, not a digest, so each build gets Go's and Debian's security fixes. The image sets `GOTOOLCHAIN=local`, so it never downloads another Go. |
| Two stages | The `build` stage has the Go toolchain, the modules and the source. The final stage gets only the binary. |
| `go.mod` and `go.sum` copied before the source | `go mod download` runs in its own layer, which Docker reuses until `go.mod` or `go.sum` changes. A code change downloads nothing again. |
| `CGO_ENABLED=0` | A static binary that needs no C library, so it runs on Alpine. `-trimpath` and `-ldflags="-s -w"` leave out your local paths and the debug symbols, as Ingot does. |
| `alpine:3.24` | A static binary needs no more than Alpine: `/bin/sh` for the [web terminal](https://docs.sevalla.com/applications/web-terminal.md) and custom start commands, and the CA certificates for outgoing HTTPS. The 3.24 line gets Alpine's security fixes. |
| `adduser -D -u 1000 app` and `USER 1000:1000` | The server runs as a user with no privileges. The binary stays owned by root, so the server can't change it. |
| No init process | The server handles `SIGTERM` itself, so it runs as process 1 without `tini`. |
| `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
.env*
*.log
/storefront
Dockerfile
.dockerignore
```

The `.dockerignore` keeps the Git history, local environment files, logs and a binary you built locally out of the build.

Commit and push both files. Ingot doesn't build from your `Dockerfile`, and it adds the patterns in `.dockerignore` 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 deployment's status is **Deployed**, `/api/products` returns the same JSON. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md).

With the API, [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches the build strategy. Then deploy and wait as in [Deploy with the API](#deploy-with-the-api):

```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":"."}
```

The CLI does the same with `sevalla apps update $APP_ID --build-type dockerfile --dockerfile-path Dockerfile`. The new deployment's build log names the Dockerfile build:

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

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

## 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. When you add a module, run `go mod tidy` and commit `go.mod` and `go.sum` with the code. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Add configuration on the application's **Environment variables** tab and read it with `os.Getenv`, as the server reads `PORT`. Variables marked **Available during buildtime** also reach `go mod download` and `go build`; with a Dockerfile, they arrive as build arguments, which a stage reads only after an `ARG` line for each one. 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).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| The build log shows `✗ go.mod requires modules, but go.sum is missing` | `go.sum` isn't in the repository | Run `go mod tidy` and commit `go.sum` |
| The build log shows `✗ go.mod: no go version in this Ingot release satisfies ">=1.28.0" (newest known: 1.27.3)` | The `go` line in `go.mod` asks for a newer Go than Ingot knows, usually because a newer Go on your computer ran `go mod init` or `go get` | Set the line to a release Ingot knows, for example with `go mod edit -go=1.27.0`, check that `go build` passes, and commit `go.mod` |
| A Dockerfile build fails with `go: go.mod requires go >= 1.28.0 (running go 1.27.3; GOTOOLCHAIN=local)` | The same `go` line, and the `golang:1.27` image doesn't download a newer Go | Lower the `go` line as in the row before, or build on a newer `golang` line when one exists |

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) and [Add a health check](#add-a-health-check), 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`, the `display_name` that the dashboard shows and its region ID in `location`. Keep the ID of the one you want by its region ID, such as `eu-frankfurt-1` for Frankfurt, Germany. [Locations](https://docs.sevalla.com/platform/locations.md) lists them all:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CLUSTER_ID=$(curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with a web process. It 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`.

**3. Find the domain**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the domain that Sevalla gives the application. It appears a few seconds after the application is created, so ask until it's there:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 24); do
  HOST=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/domains \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[]? | select(.is_primary) | .name')
  [ -n "$HOST" ] && break
  sleep 5
done
echo "$HOST"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
storefront-x7k2m.sevalla.app
```

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

**4. Find the web process**

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

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.data[] | select(.type == "web") | .id')
```

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

**5. Find the H1 size**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) returns the sizes by `name`, in lowercase:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
H1_ID=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "h1") | .id')
```

CLI: `sevalla resources process-types`. MCP: `get_platform_options`.

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

The API creates the web process at **S1**, where the dashboard's default is **H1**. [Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) sets the size and adds the readiness probe from [Add a health check](#add-a-health-check), with the dashboard's default timings. Send `timeoutSeconds`: a probe without it gets 120 seconds, where the dashboard sets 1.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resource_type_id":"'"$H1_ID"'","readiness_probe":{"httpGet":{"path":"/healthz","port":8080,"scheme":"HTTP"},"initialDelaySeconds":0,"periodSeconds":10,"timeoutSeconds":1,"successThreshold":1,"failureThreshold":3}}' \
  | jq -c '{resource_type_name, readiness_probe}'
```

CLI: `sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID` sets the size; the CLI can't set probes. MCP: `scale_process` with `size_id` for the size, and `sevalla_api_update` with `updateProcessV3` and the same body for the probe.

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

**8. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) deploys the latest commit on the branch:

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

CLI: `sevalla apps deployments trigger $APP_ID --branch main`. MCP: `trigger_deployment`.

**9. Wait for the deployment**

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

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

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

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

**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, oldest line first: the build plan from [Verify the deployment](#verify-the-deployment), the build and the rollout. When the status is `failed`, the last lines say why.

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

**11. Check the application**

Request the products from the application's domain:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -w '\n' https://$HOST/api/products
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
[{"id":1,"name":"Espresso beans","price":14.5},{"id":2,"name":"Pour-over kettle","price":39}]
```

Then run the other checks in [Verify the deployment](#verify-the-deployment).


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