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

# Ingot

> Build an application with Ingot, Sevalla's own builder: how it generates a Dockerfile, supported languages, language versions, ingot.json, INGOT_ variables, monorepos and the image it builds.

Ingot is Sevalla's own builder: it reads your repository, detects the language, framework, language version and package manager, writes a production Dockerfile for the application, and Sevalla builds that Dockerfile with BuildKit. It's the default build strategy for new applications from Git. It needs no configuration for most applications, and you can override what it detects with an `ingot.json` file or `INGOT_*` environment variables.

## How Ingot builds your application

Every deployment that builds runs two steps in the build log:

1. **Generate.** Ingot reads the files in the [build path](https://docs.sevalla.com/applications/builds/overview.md#build-settings) and writes a Dockerfile and a `.dockerignore` for them. The build log prints `📦 Generating Dockerfile using ingot...`, then the build plan: each decision and the file it came from. See [Read the build plan](#read-the-build-plan).
2. **Build.** Sevalla builds the generated Dockerfile with the build path as the context and pushes the image. The build log prints `📦 Building and pushing docker image using the generated Dockerfile...`, then the Docker build.

Ingot generates the Dockerfile again on every deployment that builds, so a change to your repository or to an `INGOT_*` variable takes effect with the next deployment. Restarts and rollbacks reuse an existing image and don't run Ingot.

What makes Ingot different from the other builders:

* **The same input gives the same Dockerfile.** Ingot never runs your code and never uses the network while it plans. Language versions come from a catalog built into each Ingot release, so the same commit always produces the same Dockerfile.
* **Official images, maintained lines.** Builds use the official image of each language, such as `node:24-trixie-slim` or `python:3.14-slim-trixie` (FrankenPHP's for PHP). Images name a version line, not a patch release, so each new build picks up the line's latest security fixes.
* **Dependencies from your lock file.** Installs hold to the lock file, as `npm ci` and frozen pnpm, Yarn and Bun installs do.
* **Small, unprivileged images.** The final image has no compilers or build tools, runs as user `1000:1000` instead of root, and starts under `tini`. Build-time variables reach the build as a BuildKit secret, so they never land in an image layer.

Your repository's own `Dockerfile` isn't used or changed: Ingot writes its files outside the repository. To build your own Dockerfile, use the [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) build strategy.

## Supported languages

| Language | Package managers | Frameworks and tools |
| - | - | - |
| JavaScript and TypeScript (Node.js) | npm, pnpm, Yarn 1, Yarn 2 and later (Plug'n'Play included), Bun | Next.js, Nuxt, Astro, SvelteKit, React Router, Remix, TanStack Start, SolidStart, Qwik City, NestJS, AdonisJS, Strapi, Angular, Vite, Create React App, Expo web exports, Express, Fastify, Koa, Hono, Elysia, Puppeteer, Playwright, workspaces |
| Python | pip, uv, Poetry, PDM, Pipenv, setuptools | Django, FastAPI, Flask, Starlette, Quart, Litestar, Sanic, FastHTML, Streamlit, Gradio, Celery, console scripts, packages run with `python -m`, uv workspaces |
| PHP | Composer | Laravel, Symfony, Statamic, Slim, CodeIgniter, CakePHP, Mezzio, plain PHP and WordPress, served by FrankenPHP |
| Ruby | Bundler | Rails 7 and 8, Hanami, Roda, Sinatra, Rack |
| Go | Go modules | Any `main` package, `cmd/<name>` layouts, workspaces, vendoring, cgo |
| Rust | Cargo | Any binary crate, workspaces |
| Java | Maven, Gradle | Spring Boot, Quarkus, Micronaut, executable JARs, multi-module builds |
| .NET | NuGet | ASP.NET Core, worker and console applications, in C#, F# or Visual Basic |
| Elixir | Mix | Mix releases, Phoenix, Plug, umbrella projects |
| Deno | `deno.json` | Tasks, `deno serve` applications, Fresh, Hono |
| Gleam | Gleam | Erlang-target applications |
| Zig | `build.zig` | Executables |
| C and C++ | CMake, Meson, Make | Executables |
| Static sites | None | An `index.html` in the build path or in `public/`, `dist/`, `build/` or `site/` |
| Shell | None | A `start.sh` script |

Ingot tries the languages in a fixed order and builds with the first one that recognizes the application. If it detects the wrong one, set `INGOT_PROVIDER` (see [Configure the build](#configure-the-build)). If it recognizes none, the build fails; use a [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) instead.

## Use Ingot

**1. Open the build strategy**

In the sidebar, click **Applications**, then click the application. On the **Settings** tab, in the **Build strategy** section, click **Update build strategy**.

**2. Choose Ingot**

Click **Ingot** (the first option). Set the **Build path** if the application isn't at the repository root. For a workspace in a monorepo, leave it at `.` and read [Build an application in a monorepo](#build-an-application-in-a-monorepo).

**3. Save and deploy**

Click **Update build strategy**, then deploy the application.

![The Update build strategy sheet with Ingot selected first, Cache enabled and the build path set to the repository root](https://assets.sevalla.com/screenshots/light/app-settings-build-strategy-ingot.png)

The **Builder** row in the **Build strategy** section shows **Ingot**, and the next deployment's build log shows Ingot's build plan.

## Read the build plan

Before it builds, Ingot prints its plan in the build log: what it detected, the file each decision came from, and the image it builds. For a Node.js application with a `package-lock.json` and no Node.js version set:

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

* The rows at the top are the decisions, each with its source: a file (`package-lock.json`), a field in it (`package.json scripts.start`), a variable you set, or `default` when nothing in the repository decided it.
* The **Image** rows are the image Ingot builds in and the image the application runs in, the user it runs as, its init process and its port.
* The last line names the Ingot version Sevalla ran.

Lines that start with `!` after the plan are warnings: the build continues, but something deserves a look. For example, `composer.lock is missing, so Composer chooses dependency versions during the build; commit composer.lock to deploy what you tested`.

When Ingot can't plan the build, the build log shows a line starting with `✗` and what to do about it on the next line, and the deployment fails. For example:

```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
✗ No start command found
  Add a main.py or app.py, a Procfile with a web process, or set "start" in ingot.json.
```

## See the Dockerfile Ingot generated

Sevalla keeps every Dockerfile Ingot generates, with its `.dockerignore`. On a deployment's page, the **Dockerfile** tab (next to **Build log**) shows the Dockerfile the deployment's image was built from and its version. When it differs from the previous deployment's, it's marked **Changed** and **Changes** shows what changed. Click **Full file** to read it whole, or **History** to open the application's [Dockerfile history](https://docs.sevalla.com/applications/builds/overview.md#dockerfile-history) and compare any two versions.

The tab fills in when the build is done. A new Ingot release can change the Dockerfile without a change to your code: each version shows the Ingot version that generated it.

![A deployment's Dockerfile tab with version 4 marked Changed and three lines added since version 3](https://assets.sevalla.com/screenshots/light/app-deployments-detail-dockerfile.png)

## Set the language version

Ingot takes the version from the first source in this order:

1. `INGOT_<LANGUAGE>_VERSION`, such as `INGOT_NODE_VERSION`, set as an [environment variable](https://docs.sevalla.com/applications/environment-variables.md) marked **Available during buildtime**.
2. `versions` in [ingot.json](#configure-the-build), such as `"versions": { "node": "22" }`.
3. The files in this table, in the order listed.
4. The default line of the Ingot release Sevalla runs. The plan shows `default` as the source.

| Language | Variable | Files, in order | Default in Ingot 0.4.0 | Oldest line |
| - | - | - | - | - |
| Node.js | `INGOT_NODE_VERSION` | `.node-version`, `.nvmrc`, `.tool-versions`, mise's configuration; then `devEngines.runtime` and `engines.node` in `package.json` | 24 | 14 |
| Bun | `INGOT_BUN_VERSION` | `packageManager` (`bun@1.3.0`), `devEngines.packageManager` or `engines.bun` in `package.json` | 1 | 1 |
| Python | `INGOT_PYTHON_VERSION` | `.python-version`, `.tool-versions`, mise's configuration; `runtime.txt`; `requires-python` in `pyproject.toml` (or Poetry's `python` dependency); `python_version` in the `Pipfile` | 3.14 | 3.7 |
| PHP | `INGOT_PHP_VERSION` | `config.platform.php` in `composer.json`, then `require.php` | 8.5 | 8.2 |
| Ruby | `INGOT_RUBY_VERSION` | The `ruby` line in the `Gemfile`, `RUBY VERSION` in `Gemfile.lock`, `.ruby-version`, `.tool-versions`, mise's configuration | 4.0 | 2.7 |
| Go | `INGOT_GO_VERSION` | The `toolchain` line, then the `go` line, of `go.work` or `go.mod` | 1.27 | 1.20 |
| Rust | `INGOT_RUST_VERSION` | `rust-toolchain.toml` or `rust-toolchain`, then `rust-version` in `Cargo.toml` | 1.99 | 1.75 |
| Java | `INGOT_JAVA_VERSION` | A Gradle toolchain, `.java-version`, `.sdkmanrc`, `.tool-versions`, mise's configuration, then the release your Maven or Gradle build compiles for | 25 | 8 |
| .NET | `INGOT_DOTNET_VERSION` | `sdk.version` in `global.json`, then `TargetFramework` in the project files | 10 | 8 |
| Elixir | `INGOT_ELIXIR_VERSION` | `.tool-versions`, mise's configuration, then `elixir` in `mix.exs` | 1.20 | 1.14 |
| Deno | `INGOT_DENO_VERSION` | `.dvmrc` or `.deno-version`, `.tool-versions`, mise's configuration | 2.9 | 2.1 |
| Gleam | `INGOT_GLEAM_VERSION` | `.tool-versions`, mise's configuration, then `gleam` in `gleam.toml` | 1 | 1 |
| Zig | `INGOT_ZIG_VERSION` | `.tool-versions`, mise's configuration, then `minimum_zig_version` in `build.zig.zon` | 0.17 | 0.13 |

Version files are read from the build path, then from each folder above it up to the repository root, the nearest first, as version managers read them. Write versions the way each ecosystem does: `22` or `^22` for Node.js, `>=3.12` for Python, `~8.3.0` for PHP.

How Ingot treats the version you set:

* **A line follows its patch releases.** `22` or `3.12` builds on the line's image tag, such as `node:22-trixie-slim`, which moves to each new patch release, so rebuilding picks up security fixes.
* **An exact release stays on that release.** `22.11.0` or `python-3.12.8` builds on that release only, with a warning that its image won't get security rebuilds. If the catalog has no image for that patch release, such as `python-3.8.10`, Ingot builds on the newest patch release of the same minor release and warns about it.
* **End-of-life lines still build, with a warning.** Ranges such as `>=18` prefer maintained lines.
* **A version nothing satisfies fails the build.** Ingot doesn't fall back to another line. The error names the newest version the release knows, for example `no python version in this Ingot release satisfies ">=3.16" (newest known: 3.15.2)`.

For PHP, Ingot also picks a version every package in `composer.lock` supports, and says which package limited it. For Python with no version set, if `requirements.txt` pins a release that has no Linux wheels for the default Python, such as `psycopg2-binary==2.9.10` on 3.14, Ingot builds on the newest Python line those pins have wheels for and says which pin chose it.

## Configure the build

Most applications build with no configuration. To override what Ingot detects, add an `ingot.json` file to the build path. Comments are allowed:

```json ingot.json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  // Build on Node.js 22 instead of the default line
  "versions": { "node": "22" },
  "build": "npm run build:production",
  "start": "node dist/server.js",
  "packages": { "run": ["ffmpeg"] }
}
```

Each field has an environment variable. The variable wins over the file. Ingot reads only variables whose names start with `INGOT_` and that are marked **Available during buildtime**.

| `ingot.json` field | Environment variable | What it does |
| - | - | - |
| `app` | `INGOT_APP` | The application's folder in a monorepo, relative to the build path, such as `apps/web`. In `ingot.json`, read only from the file at the build path. See [Build an application in a monorepo](#build-an-application-in-a-monorepo). |
| `provider` | `INGOT_PROVIDER` | The language to build with instead of detecting one: `golang`, `rust`, `elixir`, `java`, `dotnet`, `deno`, `gleam`, `zig`, `ruby`, `php`, `python`, `node`, `cpp`, `static` or `shell` |
| `versions` | `INGOT_<LANGUAGE>_VERSION` | The version per language, such as `"versions": { "python": "3.13" }` or `INGOT_PYTHON_VERSION=3.13`. See [Set the language version](#set-the-language-version). |
| `install` | `INGOT_INSTALL_CMD` | Replaces the command that installs dependencies |
| `build` | `INGOT_BUILD_CMD` | Replaces the build command |
| `start` | `INGOT_START_CMD` | Replaces the start command |
| `packages.build` | `INGOT_BUILD_PACKAGES` | Debian packages to install while building, for example `libvips-dev`. In the variable, separate them with spaces. |
| `packages.run` | `INGOT_RUN_PACKAGES` | Debian packages to install in the final image, for example `ffmpeg`. In the variable, separate them with spaces. |

Commands in `install` and `build` run with `/bin/sh` in the build image, with your build-time variables set.

### Set the start command

Ingot takes the start command from the first of these:

1. The web process's start command on the [Processes](https://docs.sevalla.com/applications/processes.md) tab. Sevalla passes it to Ingot as `INGOT_START_CMD`, so it also wins over an `INGOT_START_CMD` variable you set.
2. An `INGOT_START_CMD` environment variable.
3. `start` in `ingot.json`.
4. The `web` entry of a `Procfile` in the build path.
5. What Ingot detects: the framework's production server (for example Gunicorn for Django and Flask, Uvicorn for FastAPI, FrankenPHP for PHP), or the `start` script in `package.json`.

The start command runs with `/bin/sh`, from the application's folder in `/app`, as user `1000:1000`. When the application has nothing that serves HTTP and the `Procfile` has a single other process, such as `worker: python bot.py`, Ingot runs that process and warns that you should deploy the application as a [background worker](https://docs.sevalla.com/applications/processes.md#background-workers).

## Build an application in a monorepo

There are two ways to build one application of a repository that holds several. Use the one that matches how the application installs its dependencies.

| The application | Build path | `INGOT_APP` | Why |
| - | - | - | - |
| Stands alone in its folder, with its own lock file | The application's folder, such as `apps/billing` | Not set | Ingot sees only that folder, like a repository of its own |
| Is a package in a workspace (npm, pnpm, Yarn or Bun workspaces, a uv workspace, a `go.work`) that installs from the lock file at the root | `.` | The application's folder, such as `apps/web` | The build needs the root's lock file and the shared packages the application depends on |

With `INGOT_APP`, Ingot installs the workspace's dependencies at the root with its lock file, builds the application and the workspace packages it depends on (with Turborepo or Nx when the workspace uses them), and starts the application from its own folder. An `ingot.json` in the application's folder configures that application on top of the root's.

Create one Sevalla application per application in the repository, each with its own `INGOT_APP`.

## Environment variables during the build

The build gets every environment variable marked **Available during buildtime**, and the [variables Sevalla sets](https://docs.sevalla.com/applications/environment-variables.md#variables-sevalla-sets), such as `SVL_DEPLOYMENT_COMMIT_SHA`. Sevalla passes them to the Docker build as a BuildKit secret named `ingot_env`, and the generated Dockerfile reads it in the steps that install dependencies and build the application.

* Variables never land in an image layer or the image's history. The running application gets its variables from Sevalla at runtime, not from the image.
* Only variables whose names are valid shell names reach the build: letters, digits and underscores, not starting with a digit. A variable such as `spring.profiles.active` is left out of the build, but still set at runtime.
* `PORT` isn't set during the build, only at runtime.

Variables that are only **Available during runtime** don't reach the build. If your framework reads a variable while it builds, for example `NEXT_PUBLIC_*` in Next.js or `VITE_*` in Vite, mark it **Available during buildtime** and deploy again after you change it.

## The image Ingot builds

| What | Value | What it means for you |
| - | - | - |
| Port | `PORT`, default `8080` | Sevalla sets `PORT` to the web process's port. Ingot's images make the server listen on it, on all interfaces. |
| User | `1000:1000` | The application doesn't run as root. It can write to `/app` and to `/tmp`; it can't install packages at runtime. Add system packages with `packages.run` instead. |
| Application folder | `/app` | Your code and its dependencies. Static sites are served from `/srv`. Use these paths for [disks](https://docs.sevalla.com/applications/disks.md). |
| Init process | `tini` | Passes stop signals to the application, so it shuts down cleanly on deployments, and reaps orphaned processes |
| Base | Debian 13 (trixie); Alpine for static sites | Older language lines and some dependencies, such as Playwright before 1.55, build on Debian 12 (bookworm) or 11 (bullseye). The plan shows the image. |
| Shell | `/bin/sh` | The [web terminal](https://docs.sevalla.com/applications/web-terminal.md) and start commands set on the **Processes** tab work |

Ingot's `.dockerignore` keeps files the image doesn't need out of the build, such as `.git`, `node_modules`, logs, editor folders and `.env.local`, plus every pattern in your own `.dockerignore`.

## Run workers and jobs

Ingot builds one image for the application. Sevalla runs its start command as the web process; every other process runs from the same image with its own start command. Create them on the [Processes](https://docs.sevalla.com/applications/processes.md) tab, for example:

| Framework | Process | Start command |
| - | - | - |
| Laravel | Background worker | `php artisan queue:work` |
| Laravel | Job, **Before deployment** | `php artisan migrate --force` |
| Rails | Job, **Before deployment** | `bin/rails db:migrate` |
| Rails with Sidekiq | Background worker | `bundle exec sidekiq` |
| Django | Job, **Before deployment** | `python manage.py migrate --noinput` |
| Celery | Background worker | `celery -A <module> worker --loglevel=INFO` |

Ingot doesn't create processes, and doesn't run a `Procfile`'s `release` entry. [Get Dockerfile](https://api-docs.sevalla.com/v3/applications/get-dockerfile) returns the processes Ingot recommends for the application in `processes`, such as a queue worker or a migration job; create the ones you need.

## Build cache

With **Cache enabled**, Sevalla stores the build's layers in the application's registry and reuses them on the next build. A step whose inputs didn't change, such as installing dependencies from an unchanged lock file, comes from the cache.

The steps that build your application run on every deployment, because they read the build-time variables, which include the deployment's own `SVL_*` variables. To force a clean build, [redeploy without the build cache](https://docs.sevalla.com/applications/builds/overview.md#build-cache).

## Move to Ingot

Ingot builds most applications from the same repository with no changes. It doesn't read `nixpacks.toml`, `railpack.json`, `project.toml` or `NIXPACKS_*` and `RAILPACK_*` variables. Before you switch a production application:

1. Pin your language version in the repository, for example with `engines.node`, `.python-version` or `require.php`, so it doesn't depend on any builder's default.

2. Translate the settings you rely on:

   | You set | With Ingot |
   | - | - |
   | `NIXPACKS_NODE_VERSION`, `RAILPACK_NODE_VERSION` | `INGOT_NODE_VERSION`, or the version files in [Set the language version](#set-the-language-version) |
   | `NIXPACKS_INSTALL_CMD`, `RAILPACK_INSTALL_CMD` | `INGOT_INSTALL_CMD` or `install` in `ingot.json` |
   | `NIXPACKS_BUILD_CMD`, `RAILPACK_BUILD_CMD` | `INGOT_BUILD_CMD` or `build` in `ingot.json` |
   | `NIXPACKS_START_CMD`, `RAILPACK_START_CMD` | The web process's start command, `INGOT_START_CMD` or `start` in `ingot.json` |
   | `NIXPACKS_APT_PKGS`, `RAILPACK_DEPLOY_APT_PACKAGES` | `INGOT_RUN_PACKAGES` or `packages.run` in `ingot.json` |
   | `RAILPACK_BUILD_APT_PACKAGES` | `INGOT_BUILD_PACKAGES` or `packages.build` in `ingot.json` |

3. Check what your application writes to disk while it runs. It runs as user `1000:1000`, not root: it can write to `/app` and `/tmp`.

4. Switch a staging copy first, or watch the first deployment's build log. If it fails, switch back and deploy again.

## Troubleshoot Ingot builds

Ingot's messages in the build log say what failed and how to fix it. The common ones:

| Build log | Cause | Fix |
| - | - | - |
| `No start command found` | Nothing in the build path tells Ingot how to start the application | Set the web process's start command, `INGOT_START_CMD`, `start` in `ingot.json`, or a `web` entry in a `Procfile` |
| `no <language> version in this Ingot release satisfies "<range>"` | The version you set is newer than every line Ingot knows, or older than the oldest one | Set a version in the range of [Set the language version](#set-the-language-version) |
| `<language> <version> has no image in this Ingot release` | An exact release newer than the newest one Ingot knows | Set the line, such as `22` instead of `22.99.0` |
| `Ingot couldn't find the <framework> application` | The framework is installed, but no file Ingot searches creates its application and there's no `main.py`, `app.py` or `server.py` to run | Create the application at the top level of `main.py` or `app.py`, or set the start command |
| `<framework> needs a production server, and <server> isn't installed` | A Python framework without the server Ingot runs it with | Add the server, such as `gunicorn` or `uvicorn`, to your dependencies, or set the start command |
| A permission error at runtime, such as `EACCES` or `Permission denied` | The application writes outside `/app` and `/tmp`, or needs root | Write to `/app` or `/tmp`, and install system packages with `packages.run` instead of at runtime |

For anything else, [Troubleshoot builds and deployments](https://docs.sevalla.com/applications/troubleshooting/builds-and-deployments.md) covers failures every builder shares.

## API and CLI

| Task | API | CLI |
| - | - | - |
| Build with Ingot | [Update application](https://api-docs.sevalla.com/v3/applications/update-application) with `build_type` `ingot` (and `build_path`, `build_cache_enabled`) | `sevalla apps update <app-id> --build-type ingot` |
| Set an `INGOT_*` variable | [Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (build-time by default) | `sevalla apps env-vars create <app-id> --key INGOT_NODE_VERSION --value 22` |
| Read the generated Dockerfiles | [List Dockerfiles](https://api-docs.sevalla.com/v3/applications/list-dockerfiles), [Get Dockerfile](https://api-docs.sevalla.com/v3/applications/get-dockerfile) | Not available |

Each Dockerfile version in the API has `source` `ingot`, the `ingot_version` that generated it, and what Ingot detected: `language` (such as `node` or `php`), `facts` (the decisions of the build plan, each with its source) and `processes` (the processes it recommends). A deployment's `dockerfile_id` names the version its image was built from.


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