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/install.sh | sudo bash
The script 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. With no terminal to ask on, the domain defaults to <public-ip>.sslip.io, which needs no DNS and is enough to try Noite out. 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. Inputs are environment variables; with sudo, put them after it (curl … | sudo NOITE_DOMAIN=example.com bash):
| Variable | Default | Effect |
|---|---|---|
NOITE_DOMAIN |
asked, else <ip>.sslip.io |
BASE_DOMAIN on first install |
NOITE_VERSION |
latest |
image tag; a short SHA holds back or rolls back |
NOITE_ADMIN_EMAIL |
none | account promoted to admin at boot |
NOITE_TENANCY |
multi |
single when only you push code |
NOITE_DIR |
/opt/noite |
install directory |
NOITE_REF |
main |
git ref compose.yaml is fetched from |
NOITE_SKIP_DOCKER |
0 |
1 fails instead of installing Docker |
For your own bucket (option B), 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
curl -fsSL https://noite.now/uninstall.sh | sudo bash
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, pass NOITE_CONFIRM=1. Then run install.sh 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.sh 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
mkdir -p /opt/noite && cd /opt/noite
curl -fsSLO https://raw.githubusercontent.com/ryuzcorp/noite/main/docker/compose.yaml
# write .env (below), then:
docker compose up -d && docker compose logs -f noite
.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:latest
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.
Open https://app.<domain> and create the owner account (first signup), then deploy an app as in the Quickstart.
Other variables worth setting: NOITE_S3_BUCKET, NOITE_ADMIN_EMAIL, NOITE_EMAIL_WEBHOOK_URL, 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
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.