Skip to main content
UbuntuDebianDocker

What this covers

How to get the Levelrail control plane (levelrail) running on a Linux host, confirm it works, and upgrade or remove it later. The node agent is levelrail-agent, and levelrail-cli is the client. All three are static Go binaries. Want only the CLI, to control an instance from your laptop? Jump to Installing just the CLI. install.sh sets up the server, not a remote client.

install.sh

Recommended. One command provisions Docker, a systemd unit, and the control plane on a real Linux server.

Docker

Already running everything else as containers? Pull the published images instead.

Build from source

For contributors, unreleased commits, or trying Levelrail locally without a server.

Requirements

Confirm these before you provision a server.
No official minimum has been benchmarked or published (see the roadmap). The sizes above are a starting point, not a hard requirement. The control plane itself checks no minimum.
What install.sh preflight checks: root, architecture, OS, systemd, curl, Docker version, RAM, free disk and ports 80/443/8080. It stops on a failed check unless you pass --force. RAM below 1 GB only warns (LEVELRAIL_MIN_RAM_MB); free disk below 10 GB on the data directory’s filesystem fails (LEVELRAIL_MIN_DISK_GB). After install: levelrail-cli doctor (GET /api/v1/system/doctor) checks Docker reachability, free disk (warns below 1 GiB by default), data-directory writability, and that ports 80/443 are free to bind. It checks no memory or CPU floor. If a port is blocked, see Domains and ingress: firewall.
The script:
  • Runs the preflight table and stops on a failed check (--force continues anyway).
  • Installs Docker via get.docker.com if missing.
  • Downloads the newest release for linux/amd64 or linux/arm64 and verifies its SHA-256 checksum.
  • Installs levelrail-cli to /usr/local/bin from the same release (skip with LEVELRAIL_INSTALL_CLI=0).
  • Writes a levelrail.service systemd unit, starts it, and waits for GET /healthz.
  • Checks that ports 80 and 443 answer on the server’s public IP, and prints the ufw/firewalld commands to open them if not.
  • Prints the dashboard URL, the one-time setup token for the first admin, and a reminder to back up <data dir>/master.key.
A missing checksums.txt is a hard failure unless you set LEVELRAIL_SKIP_CHECKSUM=1. While no stable release exists, the installer picks the newest pre-release; choose explicitly with LEVELRAIL_CHANNEL=stable|beta. A release with no binary for your architecture is skipped with a warning. Some providers never route a server’s own public IP back to itself, so treat a failed reachability test as a hint, not proof.

First sign-in

Levelrail setup wizard on a fresh install: server checks with a Continue button
1

Open the setup link

Open one of the http://<ip>:8080/login?setup=<token> links from the install summary. The public address is printed first, then private addresses. Container bridge addresses are hidden.8080 is only the default. If that port was taken, install.sh picks the next free one (LEVELRAIL_DASHBOARD_PORT) and prints the real one. Use the port from your own output.
2

Create the admin account

After you create the admin account, the setup wizard opens and runs the server checks shown above.
3

Enable HTTPS

The dashboard shows a “connection is not encrypted” banner until you enable HTTPS. Once you have an https:// dashboard URL, sign-in over plain HTTP is refused.
Open the Domains page and click Enable HTTPS. It issues a real Let’s Encrypt certificate for <dashed-ip>.sslip.io, no DNS needed. See Domains and ingress.
Pick a dashboard-only subdomain, not an app’s own domain, such as console.example.com or panel.example.com (the same convention CapRover uses for captain.<domain>). Reusing a domain an app already serves breaks whichever one loses the conflict, silently. See Domains and ingress.
Print the token again on the server. The dashboard port is also in the unit file:
Set APP_ADMIN_USERNAME and APP_ADMIN_PASSWORD in the unit (via sudo systemctl edit levelrail) before the first start.
Allow plain HTTP sign-in temporarily. Run sudo systemctl edit levelrail, add the lines below, then restart:
AI chat, AI models, the load balancer, platform as code and Cloudflare Tunnel are hidden until you opt in with APP_EXPERIMENTAL, see experimental features.

Common scenarios

Check the server first

install.sh --check is a read-only readiness report. It changes nothing and runs without root (port owners are then only partly visible).
It reports who holds ports 80 and 443 (Traefik, nginx, Caddy, Apache, HAProxy, a container or a host process), the Docker version and whether the daemon is rootless, whether the Docker API is exposed on the network, free disk, memory, the host firewall, and whether the domain you give resolves to this server. It ends with the recommended mode, own ports or behind your existing proxy, and the exact command to run next. It exits non-zero when a check fails.

Server already runs a proxy

Coolify, Traefik, nginx or Caddy on the same box already hold ports 80 and 443, so a default install would start but never serve or issue certificates. The installer checks this before it changes anything (ss, and docker ps --filter publish=<port> to name a container and image):
If both ports are free nothing changes. If they are taken, the installer switches to coexist mode by itself and says so. Pass --no-coexist (or LEVELRAIL_COEXIST=0) to fail preflight instead. The installer never prompts. Pass --coexist (or LEVELRAIL_COEXIST=1) to choose the mode up front, for example to move a server whose ports are currently free:
--domain only fills in the next command, which the installer prints at the end:
To move an existing install behind a proxy later: sudo sh install.sh upgrade --coexist. See Behind an existing proxy for the proxy side. Let’s Encrypt only validates on 80/443, so certificates for these domains are issued by your existing proxy, not Levelrail.
Every address the installer chooses (APP_HTTP_ADDR, APP_INGRESS_HTTP_ADDR, APP_INGRESS_HTTPS_ADDR, and APP_PUBLIC_HOST when you set it) is written to a systemd drop-in, /etc/systemd/system/levelrail.service.d/10-install.conf, never into the main unit.
  • install.sh upgrade reads it back and does not rewrite it, so custom ports survive upgrades, including the socket-activation switch that regenerates the main unit.
  • Installs made before the drop-in existed are migrated on their next upgrade (ports are read from the old main unit).
  • If the main unit no longer matches what the installer would write (hand edits or an older installer), upgrade warns and, if it has to regenerate the unit, keeps a copy at levelrail.service.pre-upgrade.
  • Keep your own overrides in a later-sorting drop-in such as override.conf (systemctl edit levelrail).

Verifying release binaries

Each release publishes checksums.txt (SHA-256 of every CLI, agent and control plane binary), a keyless cosign signature bundle for it (checksums.txt.sigstore.json), and a GitHub build provenance attestation for the binaries. Releases cut before signing was added carry no bundle. install.sh always verifies the SHA-256 checksum and refuses to install on a mismatch. It also verifies the signature when cosign is installed and the release ships a bundle.
When cosign is not on PATH, install and upgrade download a pinned cosign release (v3.1.3, linux/amd64 or linux/arm64) from the official sigstore GitHub releases, check it against a SHA-256 pinned inside install.sh, and place it at /usr/local/bin/cosign. An existing cosign is never touched.If the download fails (offline host, blocked GitHub, wrong checksum) the installer warns and carries on with checksum-only verification. Opt out with --no-cosign or LEVELRAIL_NO_COSIGN=1.To install it on its own, for example to clear the Settings > Updates warning (this subcommand does fail loudly if it cannot install):

Option 2: Docker

Already running everything else as containers? See Docker for docker run and docker-compose.yml examples. Images (both linux/amd64 and linux/arm64, multi-arch):
  • ghcr.io/glincker/levelrail (control plane)
  • ghcr.io/glincker/levelrail-agent (node agent)
:latest and :vX.Y only move on a stable tag; :beta moves on every prerelease tag. :edge moves only when someone manually triggers a build against main, not on every push. Pin an exact :vX.Y.Z tag for anything you care about staying still.
No stable release yet. No non-prerelease tag has shipped as of this writing, so :latest currently points at an old beta build, not the newest one. Use :beta until the first stable release ships, then switch to :latest.
install.sh remains the recommended path for a real single-node deployment since it also provisions Docker and a systemd unit. The Docker image is for operators who want Levelrail to fit into an existing container-only setup.

Verifying image signatures

Every image is signed keylessly with cosign via GitHub Actions OIDC (no long-lived signing key). Verify a pulled image against this repository’s release workflow:
Swap in levelrail-agent and any tag from the table above.
SBOM (SPDX) and provenance (SLSA) are attached as build attestations, not the older cosign attach-sbom format, so cosign download sbom does not find them. Read them with docker buildx imagetools inspect:

Option 3: build from source

The path for contributors, anyone running an unreleased commit, or trying Levelrail on a laptop without a server. See CONTRIBUTING.md in the repository for branch and commit conventions and how to run the tests. Requirements: Go 1.26 or newer, a running Docker daemon, and Node.js with npm only if you build the frontend yourself.
The frontend is a separate Vite project in web/, embedded into the control plane binary at build time, so production runs no Node process. See web/README.md for the frontend commands.
The control plane listens on :8080 by default. For a throwaway local instance, start it with APP_DEV_MODE=1: it creates a fixed dev/dev admin and fixed API tokens from dev-fixtures.yml so you can skip sign-up.
Dev mode is for local use only. A release build (-tags embedweb) ignores APP_DEV_MODE, so it can never run this way in production.

Installing just the CLI

levelrail-cli is a separate, small client binary for your own laptop or a CI runner, the same shape as aws or gh: install it locally, point it at a running instance, authenticate over the network, no SSH key and no inbound port on the server. The dashboard prints these same steps under Settings, CLI access, with a copy button on each command. Levelrail CLI access page with install, point-at-server, and device login steps
1

Install the binary

Detects your OS and architecture, verifies the release checksum (and its cosign signature, if cosign is installed) the same way install.sh does, and installs to ~/.local/bin, no root needed. For a system-wide install, run with sudo and set LEVELRAIL_CLI_INSTALL_DIR=/usr/local/bin.
2

Point it at your instance

3

Log in

Prints a short code and opens a browser approval page on the control plane, the same model gh auth login and aws sso login use. Approving a code requires an already-authenticated dashboard session, so it inherits that account’s two-factor or passkey requirement. Nothing extra to configure.
See the CLI reference for every command.

Other ways to install the CLI

Every release asset is listed in checksums.txt, which is signed with cosign and has a build provenance attestation. Windows binaries are published from the release after v0.2.0-beta.15; before that, run the install script from WSL.

Verifying the install

install.sh already fails loudly if the control plane is not healthy within 60 seconds (LEVELRAIL_HEALTH_WAIT). Re-check at any time:

Upgrading

Pick the path that matches how you installed. The self-update (next section) is the safest, because it restores the database as well as the binary on failure.

Check first, then upgrade

levelrail-cli upgrade (or Settings > Updates in the dashboard) runs read-only preflight checks. Nothing is applied unless you start or schedule an upgrade, or you turned on automatic upgrades (off by default, see Upgrade safely). The CLI also takes a fresh control plane backup unless you pass --no-backup, then prints the exact command to run. The API is GET /api/v1/updates/preflight.

Upgrade with automatic rollback

Settings > Updates > Upgrade to vX.Y.Z now (or Schedule upgrade) and levelrail-cli upgrade --apply run the safe self-update. It:
  1. Downloads the release and checks its checksum and cosign signature.
  2. Snapshots the database.
  3. Tries the new release’s migrations on a copy of that snapshot.
  4. Swaps the binary, restarts, and waits for health.
If the new release is not healthy within --timeout (90 seconds by default), the previous binary and, when the schema changed, the pre-upgrade database snapshot come back automatically. Every attempt, with its step timeline, is kept on the Updates page.
The same engine runs on the host as sudo levelrail self-upgrade --to <version>. See Upgrade safely for the runbook and what to do when something fails.

Run the upgrade

Replaces the binary with the newest release and keeps your unit file, chosen ports (the 10-install.conf drop-in, see Server already runs a proxy), systemctl edit overrides and data. It restarts the service and waits for it to come back healthy. If the new binary is not healthy within LEVELRAIL_HEALTH_WAIT seconds the previous binary is put back and started again.
The installer cannot undo a schema migration. For an upgrade that also restores the data, use the self-update above.
To pin a release instead of the latest:
Re-running the installer without arguments also works: it repairs the installation and rewrites the unit file, still keeping the ports in the drop-in.

Older binary refuses to start

A binary older than the database exits with status 78 and prints which release last ran on the data and the exact commands to recover. The installer’s unit adds RestartPreventExitStatus=78, so systemd does not restart-loop it. Nothing is modified: migrations only go forward. Run the release it names (LEVELRAIL_VERSION=vX.Y.Z ... install.sh upgrade) or restore a backup taken by the older release.

Rolling back

Before pending migrations run, the control plane snapshots its database as levelrail-<timestamp>.db under the data directory’s backups folder. To go back after a bad upgrade, install the previous binary, stop the service, then:
The live database is kept beside the restored one as .before-restore-<timestamp>. The master key is not part of a snapshot.

Uninstalling

--purge deletes all app and deploy state, and the master key every stored secret is encrypted with.
Uninstalling does not touch Docker itself or any containers, images, or volumes Levelrail created for your deployed apps, and install.sh uninstall leaves levelrail-cli in place. Remove those separately if needed.

Getting help

Next steps

Getting started

Sign in and deploy your first app.

Docker

Run the control plane and agent as containers.

Upgrade safely

Preflight checks, scheduled upgrades and automatic rollback.

Behind an existing proxy

Run next to Coolify, Traefik, nginx or Caddy.
Last modified on October 12, 2026