- TypeScript 96.2%
- Rust 1.2%
- Python 1.1%
- Shell 1%
- JavaScript 0.3%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
A Shell button on every running container of an app opens an xterm.js terminal over the WebSocket at /api/apps/:id/containers/:name/shell, authenticated by the token subprotocol like /ws. The server runs `exec --interactive --tty` on a pseudo-terminal util-linux `script` allocates (ContainerExec.terminal, ContainerBackend.shell, Supervisor.openShell), sizes it through stty on the pty script opened, ends it by typing ^C and ^D before signalling, and writes who opened and closed each session into the app's log. xterm.js is loaded lazily the first time a shell is opened. |
||
| .omp | ||
| assets | ||
| dashboard | ||
| docs | ||
| packages | ||
| packaging | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| config.example.yaml | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
NodeRig
A small self-hosted PaaS. NodeRig deploys applications from git, runs them as containers, serves static sites itself, routes every name through one ingress with automatic TLS, captures logs and metrics, and lets an application push its own widgets, metrics and structured logs back over a local socket.
An application is one workload: a container built from its own Dockerfile, pulled from a registry or declared as a compose project, or a repository whose files NodeRig serves. One record carries its declaration, its deployment history and its lifecycle, and everything it owns lives under one directory NodeRig owns.
NodeRig targets internal and self-hosted systems: one supervisor process on a
Linux host, deploys triggered by webhooks, one listener that answers for every
application by name, and real-time observability with no additional
infrastructure. It is aiming at the gap between "run it with pm2 and hope"
and "adopt Kubernetes", on one machine: a project deploys the way it would on a
PaaS, the host underneath it is managed rather than hidden, and one edge answers
for every name it serves. Where that is going next is
docs/roadmap.md.
Status. The supervisor, the REST API, webhook deploys, the dashboard, container and static applications, the hostname-routing ingress with its automatic certificates, volume snapshots, the credential vault and the System section, which is this host's network, its container runtime and its firewall, all work today. Open work is tracked in the
tickets/tree in the repo root, which is local and untracked, so a fresh clone starts without it. Read docs/security.md before exposing a host.
Documentation
| Document | What is in it |
|---|---|
| docs/roadmap.md | What NodeRig is for, what answers for it today, and what is deliberately next |
| docs/concepts.md | Design goals, the app record, the two kinds, the two sources, deployments, volumes and snapshots |
| docs/deploys.md | Remotes, the deploy pipeline, zero-downtime deploys, the readiness gate, revert, and the webhook a forge calls |
| docs/ingress.md | Hostname routing, the dashboard's own hostname, redirects, TLS |
| docs/containers.md | Container apps under Docker or Podman |
| docs/static-sites.md | Serving a built directory, and the build container that produces it |
| docs/error-pages.md | Per-application error pages: the statuses each kind can declare, the placeholders, and what is served while a workload is down |
| docs/icons.md | What the dashboard draws for an application: the icon a deploy detects, and the one an operator picks |
| docs/configuration.md | Every config.yaml key, the container runtime setting, signing in |
| docs/rest-api.md | Every REST route, the query parameters, the WebSocket events |
| docs/cli.md | nrig, the command line client: signing in from a terminal, contexts, and every command |
| docs/app-ipc.md | Widgets, app metrics and structured logs an app pushes over IPC |
| docs/ipc-protocol.md | The normative IPC specification |
| docs/dashboard.md | Every page, and the route behind each element |
| docs/security.md | What the runtime enforces, what NodeRig adds, and what is still open |
| docs/credentials.md | Forge credentials, and how one reaches a git invocation |
| docs/firewall.md | Listing and editing the host's own firewall |
| docs/system.md | The System section: this host's network report, and which container runtime NodeRig drives |
| docs/development.md | Running the suite, the dashboard dev server, the conventions |
Installation
Prerequisites
- Node.js 24 or newer, for
node:sqlite gitonPATH- Docker or Podman. A workload is a container, and a static site's build runs in
one too, so a host with neither can serve a repository that carries its output
already built and nothing else.
GET /api/hostand the dashboard report which runtime is in charge, its version and whether it is rootless - Root, in practice: talking to the container runtime's daemon, publishing a port below 1024 and owning the state directory all need it
Install with pacman
The repository carries its own PKGBUILD, in packaging/, which builds
noderig-git from the tip of the forge. This is how a host that runs NodeRig
should get it: two machines stay in step by building the same commit, with
nothing rsynced between them.
mkdir -p ~/Aur && cd ~/Aur
git clone https://git.raccoon.sk/martin/noderig.git
cd noderig/packaging
makepkg -si
Later versions are the same two commands from that directory:
git pull && makepkg -si. Build from packaging/ rather than from the
checkout root: makepkg puts its scratch directories, the forge clone and the
staged install tree, next to the PKGBUILD, and at the root makepkg -C would
delete the repository's own src/. The package is per-architecture, x86_64
and aarch64, because node_modules carries esbuild's binary, and every
dependency is installed on the building host.
| Path | What |
|---|---|
/usr/lib/noderig |
src/, package.json, dashboard/dist and node_modules, laid out exactly as a checkout is, plus build-info.json, the commit the package was built from |
/usr/bin/noderig |
the launcher: tsx src/main.ts, no build step, no working directory of its own |
/usr/lib/systemd/system/noderig.service |
the unit for the supervisor itself. It runs as root, names /etc/noderig/config.yaml in NODERIG_CONFIG, and creates /var/lib/noderig as its StateDirectory |
/etc/noderig/config.yaml |
the configuration, 0600 and in the package's backup=, so an upgrade keeps your edits and leaves a .pacnew |
/usr/share/noderig/config.example.yaml |
every key, documented |
/var/lib/noderig |
the state root, installed at 0700 by the package, so pacman -Qo /var/lib/noderig names it. What is inside is NodeRig's: no upgrade touches it, and pacman -R leaves the directory in place with a notice, because uninstalling the supervisor is not a decision to destroy the applications it supervised |
sudoedit /etc/noderig/config.yaml # at least webui.password
systemctl enable --now noderig
Upgrading restarts nothing on its own. systemctl restart noderig is safe while
sites are live: the supervisor detaches from its workloads on SIGTERM and
adopts them again on the way back up.
What is running is v<version>+r<count>.g<commit> in the dashboard footer and
the first line of journalctl -u noderig, carrying the same commit count and
hash pacman -Q noderig-git prints. A ticket bumps no manifest version, so
that suffix is what tells a host holding every recent change from one that
never got the upgrade. A checkout has no such record and reports the manifest
version alone.
Install from a checkout
For development, or on a host with no pacman:
git clone <repo>
cd noderig
npm install
npm run build:dashboard
cp config.example.yaml config.yaml
npm start
Then edit config.yaml. NodeRig refuses to start while the account is still
admin/admin, and warns at startup when the password is empty, which locks
the web UI rather than opening it. Every key is validated at startup: an unknown
key, a wrong type, or an out-of-range value names itself and aborts rather than
failing obscurely later.
The REST API and dashboard listen on webui.port, 8080 by default. State goes
wherever paths.dataDir points, ./data by default, which is why a checkout
keeps its state beside its code and the package does not.
Every configuration key is documented in
docs/configuration.md and in config.example.yaml.
License
MIT