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

> Deploy a full-stack Leptos 0.8 application with server-side rendering to Sevalla: cargo-leptos builds the Axum server and the WebAssembly bundle in a Dockerfile, which Ingot can't do.

A Leptos application with server-side rendering runs on Sevalla as one Rust binary and a folder of site files: [cargo-leptos](https://github.com/leptos-rs/cargo-leptos) compiles the Axum server that renders each page to HTML, and the WebAssembly bundle that the browser loads to make the page interactive (hydration). Sevalla builds both from a Dockerfile in your repository, because [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's builder, doesn't run cargo-leptos. The web process runs the server, which listens on the port that Sevalla sets in the `PORT` environment variable.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | The [Dockerfile](#deploy-with-a-dockerfile) (Rust 1.99, cargo-leptos 0.3.11). Ingot can't build a cargo-leptos project: see [Deploy with a Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `storefront`, the Dockerfile's `CMD` |
| 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

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `LEPTOS_SITE_ROOT` | Yes | Runtime | No | `site`, set by the Dockerfile: the folder with the site files that the server serves |
| `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).
* Rust 1.99 on your computer, installed with [rustup](https://rustup.rs).
* cargo-leptos 0.3.11, from its [releases](https://github.com/leptos-rs/cargo-leptos/releases) or its [installation instructions](https://github.com/leptos-rs/cargo-leptos#getting-started).

## Create the project

Create a project from Leptos's Axum starter:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CARGO_GENERATE_VALUE_NIGHTLY=No cargo leptos new --git leptos-rs/start-axum --name storefront
cd storefront
```

The starter asks one question, `Use nightly features?`. `CARGO_GENERATE_VALUE_NIGHTLY=No` answers it without a prompt, so the project builds on stable Rust, as the Dockerfile does. The project depends on Leptos 0.8 and Axum 0.8, and its `Cargo.toml` has a `[package.metadata.leptos]` section that tells cargo-leptos what to build:

* The server, the `storefront` binary, with the `ssr` feature, into `target/release`.
* The browser's code, the library, with the `hydrate` feature, for the `wasm32-unknown-unknown` target, then `wasm-bindgen` and `wasm-opt` turn it into `pkg/storefront.wasm` and `pkg/storefront.js` in `target/site`.
* The stylesheet and the files in `public`, also into `target/site`.

In `src/app.rs`, change the page title to `<Title text="Storefront"/>`, then replace the `HomePage` component at the end of the file with a page that loads products from a server function:

```rust src/app.rs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
/// Runs on the server. The browser calls it with a POST request to /api/products.
#[server(endpoint = "products")]
pub async fn list_products() -> Result<Vec<String>, ServerFnError> {
    Ok(vec!["Espresso beans".to_string(), "Pour-over kettle".to_string()])
}

/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
    // Creates a reactive value to update the button
    let count = RwSignal::new(0);
    let on_click = move |_| *count.write() += 1;
    // Loads the products on the server while it renders the page
    let products = Resource::new(|| (), |_| list_products());

    view! {
        <h1>"Storefront"</h1>
        <button on:click=on_click>"Click Me: " {count}</button>
        <Suspense fallback=|| view! { <p>"Loading products..."</p> }>
            <ul>
                {move || {
                    products
                        .get()
                        .map(|names| {
                            names
                                .unwrap_or_default()
                                .into_iter()
                                .map(|name| view! { <li>{name}</li> })
                                .collect_view()
                        })
                }}
            </ul>
        </Suspense>
    }
}
```

The server renders the products into the HTML of the page, and the button counts clicks once the WebAssembly bundle has loaded in the browser.

## Prepare the project for Sevalla

The starter's server listens on `127.0.0.1:3000` and stops at once when Sevalla stops it. Replace `src/main.rs`, so the server listens on `PORT` on all network interfaces, answers a health check, and shuts down gracefully:

```rust src/main.rs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
    use axum::{routing::get, Router};
    use leptos::logging::log;
    use leptos::prelude::*;
    use leptos_axum::{generate_route_list, LeptosRoutes};
    use storefront::app::*;

    let conf = get_configuration(None).unwrap();
    let mut addr = conf.leptos_options.site_addr;
    // Sevalla sets PORT: listen on it, on all network interfaces
    if let Ok(port) = std::env::var("PORT") {
        addr = std::net::SocketAddr::from(([0, 0, 0, 0], port.parse().expect("PORT must be a number")));
    }
    let leptos_options = conf.leptos_options;
    // Generate the list of routes in your Leptos App
    let routes = generate_route_list(App);

    let app = Router::new()
        .route("/healthz", get(|| async { "ok" }))
        .leptos_routes(&leptos_options, routes, {
            let leptos_options = leptos_options.clone();
            move || shell(leptos_options.clone())
        })
        .fallback(leptos_axum::file_and_error_handler(shell))
        .with_state(leptos_options);

    log!("listening on http://{}", &addr);
    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    axum::serve(listener, app.into_make_service())
        .with_graceful_shutdown(shutdown_signal())
        .await
        .unwrap();
    log!("server stopped");
}

// Resolves on Ctrl+C or on the SIGTERM that Sevalla sends to stop an instance
#[cfg(feature = "ssr")]
async fn shutdown_signal() {
    use tokio::signal;
    let ctrl_c = async {
        signal::ctrl_c().await.expect("failed to listen for Ctrl+C");
    };
    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to listen for SIGTERM")
            .recv()
            .await;
    };
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }
    leptos::logging::log!("shutting down");
}

#[cfg(not(feature = "ssr"))]
pub fn main() {
    // no client-side main function
    // unless we want this to work with e.g., Trunk for pure client-side testing
    // see lib.rs for hydration function instead
}
```

Then change three files:

1. In `Cargo.toml`, add `"signal"` to Tokio's features, which the shutdown handler uses, and point `style-file` at a CSS file:

   ```toml Cargo.toml theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
   ```

   ```toml Cargo.toml theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   style-file = "style/main.css"
   ```

2. Rename the stylesheet to match, with `mv style/main.scss style/main.css`. The starter's SCSS is plain CSS, which cargo-leptos optimizes with Lightning CSS, built in. For an `.scss` file, cargo-leptos downloads Dart Sass during every build.

3. In `.gitignore`, delete the line `Cargo.lock`, so the lock file is committed and every build uses the versions you tested.

Add a `rust-toolchain.toml` at the root of the project, so rustup on your computer uses Rust 1.99 with the WebAssembly target:

```toml rust-toolchain.toml theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
[toolchain]
channel = "1.99"
targets = ["wasm32-unknown-unknown"]
```

| File | Why Sevalla needs it |
| - | - |
| `Cargo.toml` | The crate, its `ssr` and `hydrate` features, and `[package.metadata.leptos]`, which cargo-leptos reads in the build. |
| `Cargo.lock` | The exact version of every crate. The Dockerfile builds with `--cargo-locked`, and installs the `wasm-bindgen` command-line tool of the version that `Cargo.lock` locks. |
| `src/main.rs` | Listens on `PORT` on `0.0.0.0` when Sevalla sets it, and otherwise on `site-addr` from `Cargo.toml`, `127.0.0.1:3000`, which `cargo leptos watch` passes to the server. `with_graceful_shutdown` finishes the requests in progress when Sevalla sends `SIGTERM`. |
| `Dockerfile` and `.dockerignore` | Build the server and the site files with cargo-leptos. See [Deploy with a Dockerfile](#deploy-with-a-dockerfile). |

Run `cargo leptos watch` and open `http://127.0.0.1:3000` to check the application locally: the page lists the two products, and the button counts your clicks.

## Deploy with a Dockerfile

Ingot can't build a cargo-leptos project. It builds Rust with `cargo auditable build --release --locked`, which compiles the server without the `ssr` feature, so its `main` function is the empty one at the end of `src/main.rs`, and builds no WebAssembly bundle: Ingot doesn't run cargo-leptos. The image builds, but the server exits as soon as it starts. The Dockerfile runs cargo-leptos instead. 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 rust:1.99-slim-trixie AS build
# hadolint ignore=DL3008
RUN apt-get update \
    && apt-get install -y --no-install-recommends binaryen curl \
    && rm -rf /var/lib/apt/lists/* \
    && rustup target add wasm32-unknown-unknown
# cargo-leptos 0.3.11, checked against the SHA-256 checksum of its release
RUN curl -fsSLO https://github.com/leptos-rs/cargo-leptos/releases/download/v0.3.11/cargo-leptos-x86_64-unknown-linux-gnu.tar.gz \
    && echo "1c9cf0e1e3ffae6c8b58d29281d949dab560cea613b80d361f323f93071df7c8  cargo-leptos-x86_64-unknown-linux-gnu.tar.gz" > cargo-leptos.sha256 \
    && sha256sum -c cargo-leptos.sha256 \
    && tar -xzf cargo-leptos-x86_64-unknown-linux-gnu.tar.gz --strip-components=1 -C /usr/local/cargo/bin cargo-leptos-x86_64-unknown-linux-gnu/cargo-leptos \
    && rm cargo-leptos-x86_64-unknown-linux-gnu.tar.gz cargo-leptos.sha256
# The same version as the wasm-bindgen crate in Cargo.lock
RUN cargo install --locked wasm-bindgen-cli@0.2.130
WORKDIR /src
COPY Cargo.toml Cargo.lock ./
# Compile the dependencies alone, so this layer stays cached until Cargo.toml or Cargo.lock changes
RUN mkdir src style public \
    && echo "fn main() {}" > src/main.rs \
    && touch src/lib.rs style/main.css \
    && cargo leptos build --release --cargo-locked \
    && rm -rf src style public target/site
COPY src ./src
COPY style ./style
COPY public ./public
# touch: the copied files are older than the placeholders' build, so Cargo would skip them
RUN touch src/main.rs src/lib.rs \
    && cargo leptos build --release --cargo-locked

FROM debian:trixie-slim
# hadolint ignore=DL3008
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/* \
    && useradd --uid 1000 --create-home app
WORKDIR /app
COPY --from=build /src/target/release/storefront /usr/local/bin/storefront
COPY --from=build /src/target/site ./site
ENV LEPTOS_SITE_ROOT=site
USER 1000:1000
EXPOSE 8080
CMD ["storefront"]
```

| Line or choice | Why |
| - | - |
| `rust:1.99-slim-trixie` | The Rust 1.99 line that `rust-toolchain.toml` pins, on Debian 13. A line tag, not a digest, so each build gets Rust's patch releases and Debian's security fixes. `rustup target add` installs the WebAssembly target that the browser's code compiles for. |
| `binaryen` | `wasm-opt`, which cargo-leptos runs to shrink the WebAssembly bundle in release builds. cargo-leptos uses the `wasm-opt`, `wasm-bindgen` and `sass` it finds on the `PATH`, and downloads the ones it doesn't find, without checking them. |
| cargo-leptos from its release | The prebuilt binary for Linux x86-64, the platform Sevalla builds for, checked with `sha256sum` against the checksum that the release publishes in `cargo-leptos-x86_64-unknown-linux-gnu.tar.gz.sha256`. To update cargo-leptos, change the version and the checksum together. |
| `wasm-bindgen-cli@0.2.130` | The tool that writes the JavaScript for the WebAssembly bundle. Its version must be the version of the `wasm-bindgen` crate in `Cargo.lock`: `grep -A1 'name = "wasm-bindgen"$' Cargo.lock` prints it. After `cargo update`, change both. `cargo install --locked` builds it from crates.io with the versions its release locks. |
| The placeholder sources | cargo-leptos compiles every dependency, for the server and for the browser, from `Cargo.toml` and `Cargo.lock` alone, in a layer that Docker reuses until either file changes. A code change then compiles only your crate. `touch` makes Cargo rebuild the real files, which keep the time of their Git checkout. |
| `--cargo-locked` | Passes `--locked` to Cargo: the build uses the versions in `Cargo.lock`, and fails if `Cargo.toml` no longer matches it. |
| Only `Cargo.toml`, `Cargo.lock`, `src`, `style` and `public` copied | What cargo-leptos reads. `rust-toolchain.toml` stays out: in the folder, it would make rustup download the Rust 1.99 toolchain again instead of using the image's. |
| `debian:trixie-slim` | The binary links to the GNU C library, so it runs on the Debian release it was built on. The image has `/bin/sh` for the [web terminal](https://docs.sevalla.com/applications/web-terminal.md) and custom start commands, and `ca-certificates` adds the CA certificates for outgoing HTTPS. `# hadolint ignore=DL3008` tells hadolint to skip its version-pinning check, so each build gets Debian's current packages. |
| `target/site` copied to `site`, and `LEPTOS_SITE_ROOT=site` | `get_configuration(None)` reads the Leptos settings from `LEPTOS_*` environment variables when the server starts, and looks for the site files in `target/site` by default. The server serves `/pkg/storefront.wasm` and the other site files from `/app/site`. |
| `useradd --uid 1000` and `USER 1000:1000` | The server runs as a user with no privileges. The binary and the site files stay owned by root, so the server can't change them. |
| No init process | `shutdown_signal` handles `SIGTERM`, so the server 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
/target
/end2end
Dockerfile
.dockerignore
```

The `.dockerignore` keeps the Git history, local environment files, logs, your local `target` folder of build output and the Playwright tests in `end2end` out of the build.

Commit everything and push it to your Git provider. `cargo leptos new` already created a Git repository:

```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 from the [Dockerfile](#deploy-with-a-dockerfile). New applications build with Ingot, so the second step switches the build strategy before the first deployment.

**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. Choose the Dockerfile build strategy**

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

**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 `📦 Building and pushing docker image using Dockerfile...`, then each step of the Dockerfile, and its **Dockerfile** tab shows your file. The first build compiles every dependency twice, for the server and for the browser, and the deployment takes about 5 minutes. Later builds reuse the dependency layers and compile only your crate.
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 page shows **Storefront**, the two products and the **Click Me** button, which counts your clicks once the WebAssembly bundle has loaded.

![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 `listening on http://0.0.0.0:8080`. 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 page, the server function, the WebAssembly bundle and the health check. The server renders the title and the products into the HTML:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://storefront-x7k2m.sevalla.app/ | grep -o '<title>[^<]*</title>\|<li>[^<]*</li>'
curl -s -X POST -w '\n' https://storefront-x7k2m.sevalla.app/api/products
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' https://storefront-x7k2m.sevalla.app/pkg/storefront.wasm
curl -s -w '\n' https://storefront-x7k2m.sevalla.app/healthz
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<title>Storefront</title>
<li>Espresso beans</li>
<li>Pour-over kettle</li>
["Espresso beans","Pour-over kettle"]
200 application/wasm
ok
```

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

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

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
listening on http://0.0.0.0:8080
```

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

A Rust program that doesn't handle `SIGTERM` ignores it when it runs as process 1, as the server does in the Dockerfile, and Sevalla then stops it only after the 30 seconds. `shutdown_signal` listens for `SIGTERM` with Tokio, and `with_graceful_shutdown` then stops accepting connections and waits for the requests in progress before `axum::serve` returns. On every deployment, the old instance's [runtime logs](https://docs.sevalla.com/applications/logs.md) end with:

```text Runtime logs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
shutting down
server stopped
```

## 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 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. With **Cache enabled** in the build strategy, a deployment of a code change shows the dependency step as `CACHED` in the build log and compiles only your crate. When you add or update a crate, commit `Cargo.toml` and `Cargo.lock` with the code; if `wasm-bindgen` changes version in `Cargo.lock`, change `wasm-bindgen-cli@` in the Dockerfile to match. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Add configuration on the application's **Environment variables** tab and read it in server code with `std::env::var`, as the server reads `PORT`. Code that runs in the browser can't read them. Leptos reads its own settings from `LEPTOS_*` variables when the server starts, such as `LEPTOS_SITE_ROOT`, which the Dockerfile sets. Variables apply at the next deployment, and with the Dockerfile, variables marked **Available during buildtime** arrive as build arguments, which a stage reads only after an `ARG` line for each one. 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 Ingot's build plan, `╭─ ■ Rust 1.99 application`, and the image builds, then the deployment fails with `Failed to deploy Web.` about 20 minutes later | The application builds with Ingot, which compiles the server without the `ssr` feature, so the binary exits at once and no instance becomes ready. The previous version keeps running. | Switch the build strategy to **Dockerfile**, as in [Deploy to Sevalla](#deploy-to-sevalla), and deploy. |
| A Dockerfile build fails with `"/Cargo.lock": not found` | The starter's `.gitignore` lists `Cargo.lock`, so it isn't in the repository | Delete the `Cargo.lock` line from `.gitignore`, then commit `Cargo.lock`. |
| A Dockerfile build fails with `cannot update the lock file /src/Cargo.lock because --locked was passed to prevent this` | `Cargo.toml` changed, for example with a new Tokio feature, and `Cargo.lock` wasn't updated and committed | Run `cargo leptos build` locally, then commit `Cargo.lock`. |
| The page shows the products without styles, the button doesn't count, and `/pkg/storefront.wasm` returns `404` | The server doesn't find the site files: `LEPTOS_SITE_ROOT` isn't the folder they're in | Keep `ENV LEPTOS_SITE_ROOT=site` and the `COPY` of `target/site` to `./site` in the Dockerfile. |

For more causes, see [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) 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 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. Choose the Dockerfile build strategy**

The application builds with Ingot (`build_type` is `ingot`), which can't build this project. [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches it to the Dockerfile, and 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 '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":".","auto_deploy":true}' \
  | jq -c '{build_type, dockerfile_path, docker_context, auto_deploy}'
```

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

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

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

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

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

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

**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: each step of the Dockerfile, then the rollout. When the status is `failed`, the last lines say why. With `search`, it finds the line that 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...
```

Without `search`, add `?limit=5000` to read the whole build log. CLI: `sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID`. MCP: `get_deployment_logs`.

**11. Check the application**

Request the page from the application's domain:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://$HOST/ | grep -o '<title>[^<]*</title>'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
<title>Storefront</title>
```

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.