Install
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):
curl -fsSL https://noite.now/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), /opt/noite/compose.multi.yaml (the multi-tenant cgroup grant, applied only when .env says NOITE_TENANCY=multi) and /opt/noite/.env with generated secrets, CONTROL_SUBDOMAIN=app and the edge on 80/443. On a host under 4 GB with no swap it also creates a /noite.swap swapfile (1–2 GB, vm.swappiness=10) as a safety net so one transient peak cannot OOM-kill RustFS or Caddy and as the build leaf’s bounded overflow (RUNNER_BUILD_SWAP_MB): a build larger than the box’s spare RAM swaps the difference and finishes instead of dying at its cap, while fleets keep zero swap. Set NOITE_SWAP=0 to leave the host’s swap alone; the uninstaller removes the file. 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.
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. A setting the runner needs that .env lacks, or still holds at a dev default (BASE_DOMAIN, CONTROL_SUBDOMAIN, BETTER_AUTH_URL, GIT_PUBLIC_BASE, the ports, RUNNER_TOKEN, BETTER_AUTH_SECRET, the RustFS keys), gets the first-install default instead of a refused boot, and the script lists what it added. A value that is set is never touched; a noite.localhost .env is left alone. When the pulled image differs from the running container it says so and asks once before recreating — replacing it restarts the runner and every tenant fleet, which cold-boot in a few seconds (see the downtime model) — and a fresh install or a re-run of the same image asks nothing. --yes (or NOITE_YES=1 / NOITE_CONFIRM=1) skips the prompt; without a terminal it warns and continues. 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 |
NOITE_ADMIN_EMAIL |
asked (Enter = setup code) | only this address may claim the first admin account, and it must still present the setup code; kept on re-runs (--admin-email sets it for scripted installs) |
NOITE_EMAIL_WEBHOOK_URL |
none | receives lost-passkey sign-in codes; without it, recover from the server |
NOITE_TELEMETRY |
unset = on | 0, false or off turns off the anonymous daily heartbeat; written to .env (Telemetry) |
NOITE_UPDATE_CHECK |
unset = on | 0, false or off turns off the daily release check; written to .env (Updating) |
NOITE_DIR |
/opt/noite |
install directory |
NOITE_REF |
main |
git ref the installer and compose.yaml are fetched from |
NOITE_YES |
none | 1 = skip the upgrade confirmation (same as --yes; NOITE_CONFIRM=1 works too) |
NOITE_SWAP |
unset = on under 4 GB | 0 = do not create the small-host /noite.swap swapfile |
The script always runs the installer from the ref’s latest commit, so a CDN-cached copy is never stale. bash -s install --pre installs the newest release, pre-releases included, before it reaches the alpha channel (see Releases and channels); an explicit NOITE_VERSION or NOITE_REF wins over it.
Before you invite anyone, read Known limits: Noite is alpha, and tenant isolation is not a hard boundary yet.
For your own bucket (option B), lost-passkey email (NOITE_EMAIL_WEBHOOK_URL) or any other setting, edit /opt/noite/.env and re-run the installer (it picks up the multi override itself when .env says NOITE_TENANCY=multi), or run cd /opt/noite && docker compose up -d by hand — with -f compose.yaml -f compose.multi.yaml on a multi install. The sections below are the same install by hand, for hosts where you’d rather not run the script.
Uninstall or start over
curl -fsSL https://noite.now/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.
| 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 and their preview environments (<label>.<slug>.<domain>— the same wildcard, which matches every depth). 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).
- Outbound HTTPS from the runner host to
github.comif you want GitHub imports and templates (Create an app). Onlyhttps://github.com/<owner>/<repo>is fetched, redirects are not followed, and each import is capped at 500 MiB and 10 minutes. Without it, blank apps andgit pushstill work.
A: bundled RustFS
mkdir -p /opt/noite && cd /opt/noite
curl -fsSLO https://raw.githubusercontent.com/ryuzcorp/noite/main/docker/compose.yaml
curl -fsSLO https://raw.githubusercontent.com/ryuzcorp/noite/main/docker/compose.multi.yaml
# write .env (below), then single installs:
# docker compose up -d && docker compose logs -f noite
# multi installs (`NOITE_TENANCY=multi` in .env) layer the cgroup grant:
# docker compose -f compose.yaml -f compose.multi.yaml up -d
.env next to compose.yaml:
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 noite.localhost default); app is the production setting. Without an .env, compose.yaml boots a local trial on http://noite.localhost:9080.
Open https://app.<domain> and create the owner account (first signup): with NOITE_ADMIN_EMAIL set, sign up as that address and enter the one-time setup code; without it, enter the code alone from the runner logs (docker compose logs noite | grep "FIRST ADMIN"). Then deploy an app as in the Quickstart. noite.localhost trials skip the proof entirely: the first account just becomes admin.
Other variables worth setting: NOITE_S3_BUCKET, NOITE_ADMIN_EMAIL, NOITE_EMAIL_WEBHOOK_URL (without it a lost passkey is recovered from the server), NOITE_SMTP_FROM, CONTROL_EXTRA_HOSTS (extra hostnames serving the control UI), CADDY_AUTO_HTTPS, NOITE_TENANCY (see Tenancy). Full detail for every variable lives in Environment variables.
B: your own bucket
Add the bucket to .env (dropping the RUSTFS_* keys) and start without the bundled store:
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
docker compose up -d --scale rustfs=0
# multi: docker compose -f compose.yaml -f compose.multi.yaml 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
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.