---
title: Install
description: Install Noite on a fresh server with one command, or by hand with Docker Compose — bundled RustFS or your own S3 bucket.
---

## One-command install

On a fresh Ubuntu or Debian server (amd64 or arm64, 2 GB of memory or more, ports 80 and 443 free):

```bash
curl -fsSL https://noite.run/run.sh | bash -s install
```

It elevates itself with `sudo` (no `sudo` in front: piped into `sudo bash`, the script runs in the background and cannot ask anything). It installs Docker Engine and the Compose plugin when missing, asks for the base domain, and writes `/opt/noite/compose.yaml` (option A, from `main`) and `/opt/noite/.env` with generated secrets, `CONTROL_SUBDOMAIN=app` and the edge on 80/443. It then opens 80/443 in `ufw` when it is active, starts the stack and waits for `/ready`. Press Enter for the default `<public-ip>.sslip.io`, which needs no DNS and is enough to try Noite out; with no terminal to ask on, it uses that default. For a real domain, point `app.`, `api.`, `git.` and `*.<domain>` at the server first.

:::warning
Register right away: the first account becomes the admin without an invite code.
:::

Re-running the script is the upgrade: it refreshes `compose.yaml`, keeps `.env` (the domain and secrets never change after the first install, since passkeys bind to `BETTER_AUTH_URL`), pulls and recreates. Inputs are environment variables, set on `bash`, not on `curl`: `curl … | NOITE_DOMAIN=example.com bash -s install` (a variable in front of `curl` only reaches `curl`).

| Variable | Default | Effect |
| --- | --- | --- |
| `NOITE_DOMAIN` | asked, else `<ip>.sslip.io` | `BASE_DOMAIN` on first install |
| `NOITE_VERSION` | `alpha` | release channel or version; see [Releases and channels](/self-hosting/operations#releases-and-channels) |
| `NOITE_ADMIN_EMAIL` | none | account promoted to admin at boot |
| `NOITE_EMAIL_WEBHOOK_URL` | none | receives lost-passkey sign-in codes; without it, [recover from the server](/self-hosting/accounts#lost-passkey) |
| `NOITE_DIR` | `/opt/noite` | install directory |
| `NOITE_REF` | `main` | git ref the installer and `compose.yaml` are fetched from |

The script always runs the installer from the ref's latest commit, so a CDN-cached copy is never stale.

Before you invite anyone, read [Known limits](/self-hosting/known-limits): Noite is alpha, and tenant isolation is not a hard boundary yet.

For your own bucket ([option B](#b-your-own-bucket)), lost-passkey email (`NOITE_EMAIL_WEBHOOK_URL`) or any other setting, edit `/opt/noite/.env` and run `cd /opt/noite && docker compose up -d`. The sections below are the same install by hand, for hosts where you'd rather not run the script.

## Uninstall or start over

```bash
curl -fsSL https://noite.run/run.sh | bash -s uninstall
```

The script removes the stack's containers, network and volumes (the runner database, git mirrors, Caddy certificates and the bundled bucket), the Noite and RustFS images, and `/opt/noite` with its `.env`. Docker and the `ufw` rules for 80/443 stay. It asks you to type the base domain first; without a terminal, set `NOITE_CONFIRM=1`. Then run `install` again for a fresh install, which is also how you change the domain.

:::danger
Uninstalling deletes every app, account, repository and secret of the install. [Back up](/self-hosting/operations#backups) first if you need any of it.
:::

| Variable | Default | Effect |
| --- | --- | --- |
| `NOITE_KEEP_DATA` | `0` | `1` removes the containers only; volumes, images and `.env` stay, and `install` brings the same install back |
| `NOITE_CONFIRM` | `0` | `1` skips the confirmation |
| `NOITE_DIR` | `/opt/noite` | install directory |

A bucket of your own is never touched: a reinstall pointed at it restores the old state from its snapshot, so empty it or set a new `NOITE_S3_BUCKET` to start fresh. Each full reinstall also issues new TLS certificates, and Let's Encrypt allows 5 certificates per hostname per week, so reinstall a real domain sparingly (`NOITE_KEEP_DATA=1` keeps the certificates).

## Manual install

The same result without the script, on any host with Docker (or Podman) and Compose. Only two files are needed; nothing is cloned or built.

### Prerequisites

- A Docker or Podman host with Compose.
- DNS: `app.<domain>` (control UI), `api.<domain>`, `git.<domain>`, plus `*.<domain>` for tenant apps. The apex stays free for your marketing site.
- Ports 80/443 reachable — Caddy terminates per-host TLS itself via on-demand certificates (ask-gated, so only live hosts get certs).

### A: bundled RustFS

```bash
mkdir -p /opt/noite && cd /opt/noite
curl -fsSLO
# write .env (below), then:
docker compose up -d && docker compose logs -f noite
```

`.env` next to `compose.yaml`:

```bash
BASE_DOMAIN=<domain>
CONTROL_SUBDOMAIN=app
BETTER_AUTH_URL=https://app.<domain>
GIT_PUBLIC_BASE=https://git.<domain>
HTTP_PORT=80
HTTPS_PORT=443
NOITE_IMAGE=ghcr.io/ryuzcorp/noite:alpha
RUNNER_TOKEN=<openssl rand -hex 32>        # shared runner↔ui bearer token
BETTER_AUTH_SECRET=<openssl rand -hex 32>  # session signing secret
RUSTFS_ACCESS_KEY=<openssl rand -hex 8>    # dev defaults are refused
RUSTFS_SECRET_KEY=<openssl rand -hex 24>
```

`CONTROL_SUBDOMAIN` empty means the control UI is served on the bare domain (the `localhost` default); `app` is the production setting. Without an `.env`, `compose.yaml` boots a local trial on `http://localhost:9080`.

:::warning
Set `BETTER_AUTH_URL` to the final URL **before** anyone registers — passkeys are bound to it.
:::

Open `https://app.<domain>` and create the owner account (first signup), then deploy an app as in the [Quickstart](/quickstart).

Other variables worth setting: `NOITE_S3_BUCKET`, `NOITE_ADMIN_EMAIL`, `NOITE_EMAIL_WEBHOOK_URL` (without it a lost passkey is recovered [from the server](/self-hosting/accounts#lost-passkey)), `NOITE_SMTP_FROM`, `CONTROL_EXTRA_HOSTS` (extra hostnames serving the control UI), `CADDY_AUTO_HTTPS`, `NOITE_TENANCY` (see [Tenancy](/self-hosting/tenancy)). Full detail for every variable lives in [Environment variables](/reference/environment-variables).

### B: your own bucket

Add the bucket to `.env` (dropping the `RUSTFS_*` keys) and start without the bundled store:

```bash
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_PUBLIC_ENDPOINT=https://s3.us-east-1.amazonaws.com
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
NOITE_S3_BUCKET=noite
```

```bash
docker compose up -d --scale rustfs=0
```

Key requirements: Get/Put/Delete/List on the bucket. The bucket is created if the key allows, otherwise create it first. This works the same for an installer-made `/opt/noite`.

## Verify

```bash
cd /opt/noite
docker compose exec noite curl -s http://127.0.0.1:8080/ready   # {"ok":true,...}
curl https://api.<domain>/health                                 # edge → runner
```

`/ready` is the summary: 200 only when the first reconcile ran, the bucket answers, isolation holds (in `multi`), the control fleet is healthy and Caddy accepted its config, and its body names whatever is failing; it is also the container's healthcheck. celld's own view of the control node: `docker compose exec noite curl -s http://127.0.0.1:8090/.well-known/celld/health`.

:::note
Building Noite from source, the dev stack and the e2e lanes are for contributors: see the repository README.
:::
