hardening and bugfixing prior to stable release

This commit is contained in:
Benjamin Diedrichsen
2026-07-31 18:21:43 +02:00
parent ac7ea07e3c
commit 0aa0be5542
44 changed files with 3016 additions and 436 deletions
@@ -35,7 +35,9 @@ Key benefits:
## Configuration
This cube currently has no configurable parameters.
| Variable | Default | What it does |
| --- | --- | --- |
| `DISTRO` | `ubuntu` | which of Docker's package repositories to add — `ubuntu` or `debian`. It selects the download path and nothing else; the release codename comes from the host's own `/etc/os-release`. |
## Dependencies
@@ -1,69 +1,79 @@
# nodevm
**Install Node.js with essential global packages**
**Install Node.js through nvm, for one user, with global packages**
## Purpose
This cube installs the latest LTS (Long Term Support) version of Node.js along with essential global npm packages commonly needed for development and deployment.
Installs [nvm](https://github.com/nvm-sh/nvm) into a single user's home
directory, uses it to install one pinned Node.js version under an alias, and
installs a list of global npm packages for that user.
## What is Node.js?
Node.js is a JavaScript runtime built on Chrome's V8 engine that allows you to run JavaScript on the server. It's widely used for:
- Building web servers and APIs
- Command-line tools
- Build tools and task runners
- Real-time applications (chat, notifications)
- Microservices
Per-user, not system-wide. Nothing is placed on the system `PATH`, and another
user on the same host is unaffected — which is the point: version pinning belongs
to whoever runs the app.
## What This Cube Does
1. **Installs Node.js LTS**
- Downloads and runs the official NodeSource setup script
- Installs the latest LTS version of Node.js
- Includes npm (Node Package Manager)
2. **Installs build dependencies**
- `libssl-dev` - SSL/TLS libraries
- `libtool` - Library building tools
- `cmake` - Cross-platform build system
- `libpng-dev`, `libjpeg-dev`, `libvips-dev` - Image processing libraries
3. **Installs global npm packages**
- **npm@11.1.0** - Latest npm version
- **pm2** - Production process manager for Node.js apps
- **yarn** - Alternative package manager
- **local-web-server** - Local development web server
- **node-gyp** - Node.js native addon build tool
- **inquirer** - Interactive command-line prompts
- **execa** - Better child process execution
- **@dotenvx/dotenvx** - Environment variable management
1. **Installs build dependencies** with apt, as root — `build-essential`,
`libssl-dev`, `libtool`, `cmake`, and the cairo/pango/png/jpeg/vips/rsvg/pixman
headers that native addons need. The package index is refreshed first: a box
nobody has updated lists .deb versions the mirror has already dropped.
2. **Installs nvm** for `USER` via the official install script, then
`nvm install <VERSION>` and `nvm alias <ALIAS> <VERSION>`.
3. **Installs `GLOBAL_PACKAGES`** with `npm install -g`, as `USER`.
## Configuration
This cube currently has no configurable parameters.
| Variable | Default | What it does |
| --- | --- | --- |
| `VERSION` | `v22.20.0` | the Node.js version nvm installs. A pin, not "latest LTS" — nvm's own version strings work, so `--lts` or `22` are accepted too. |
| `USER` | `vagrant` | the user nvm is installed **for**. Everything lands in that user's `~/.nvm`. |
| `ALIAS` | `nodelts` | the nvm alias pointing at `VERSION`, so later cubes and scripts can say `nvm use nodelts` without knowing the number. |
| `GLOBAL_PACKAGES` | `pm2 yarn local-web-server node-gyp inquirer execa @dotenvx/dotenvx` | space-separated, passed to one `npm install -g`. Setting it **replaces** the list rather than adding to it. |
| `SHELL` | `fish` | the login shell to install through — `fish` or `bash`. See below. |
### `SHELL`
nvm wires itself into whichever shell installed it, so this is not cosmetic.
- **`fish`** (default) additionally requires **Oh My Fish**, because loading nvm
goes through the `omf install nvm` plugin. `user:add` installs both, which is
the usual way a host arrives here. The cube fails with one line, before
changing anything, if `SHELL=fish` on a host with no fish.
- **`bash`** needs nothing beyond bash. Use it on a host where `user:add` has not
run.
The default stays `fish` so that an existing user — whose login shell `user:add`
set to fish — keeps getting a Node that their shell can actually see. Switching
would install it invisibly.
## Dependencies
None - this cube can run standalone.
None declared: the cube runs standalone. With `SHELL=fish` it does have a real
prerequisite (fish + Oh My Fish, which `user:add` provides), but `user:add` is
deliberately not a declared dependency — it would *create* a user who is normally
meant to already exist. `SHELL=bash` is the standalone path.
## Post-Installation
Verify installation:
`node` is on `USER`'s `PATH` in a login shell, not in root's and not in a
non-interactive one. To check:
```bash
node --version
npm --version
su - <USER> -c 'node --version && npm --version'
```
Common commands:
- Run a Node.js app: `node app.js`
- Start with PM2: `pm2 start app.js`
- Install packages: `npm install <package>`
- Use yarn: `yarn add <package>`
From a bash script that is not a login shell, load nvm first:
## PM2 - Process Manager
```bash
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"
nvm use nodelts
```
PM2 is included for production deployments. Common PM2 commands:
## PM2 — process manager
`pm2` is in the default `GLOBAL_PACKAGES`, so it is installed unless you replaced
the list.
```bash
pm2 start app.js # Start application
@@ -75,9 +85,11 @@ pm2 startup # Enable PM2 on boot
pm2 save # Save current process list
```
`pm2 startup` prints a `sudo` command to run; it does not enable itself.
## Notes
- Node.js is installed system-wide
- Global packages are accessible to all users
- npm cache is stored in `~/.npm`
- Use `nvm` if you need multiple Node.js versions
- Node.js is installed **per user**, under `~/.nvm` for `USER`.
- Global packages belong to that user too, not to everyone on the host.
- Run the cube again with a different `VERSION` and `ALIAS` to have several
versions side by side; nvm is built for exactly that.
@@ -1,15 +1,53 @@
from pyinfra.operations import server, apt, npm, python
from pyinfra.operations import server, apt
from pyinfra import host
from pyinfra.facts.files import Directory
from pyinfra.facts.server import Which
from pyinfra.api.exceptions import DeployError
hasNode = host.get_fact(Which, 'node')
VERSION = host.data.VERSION
ALIAS = host.data.ALIAS
GLOBAL_PACKAGES = host.data.GLOBAL_PACKAGES
USER = host.data.USER
SHELL = host.data.SHELL
apt.packages(
INSTALL_NVM = "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash"
# Under bash, every entry in `commands` is its own shell, so loading nvm and
# using it have to be one entry. Loading cannot be skipped either: nvm's
# installer appends its hook to ~/.bashrc, and Ubuntu's ~/.bashrc returns at
# line 1 for a non-interactive shell, so under `su -c` the hook never runs.
LOAD_NVM = 'export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"'
# The binary only. Oh My Fish is a set of fish functions with no binary and no
# fixed path, so probing for it would be guesswork — and if fish is there while
# omf is not, `omf install nvm` says so itself. Half a guard that is certain
# beats a whole one that is not.
if SHELL == 'fish' and not host.get_fact(Which, 'fish'):
raise DeployError(
f'runtime:nodevm: SHELL is "fish" but fish is not installed for {USER}. '
'Run user:add first, or set SHELL=bash.'
)
if SHELL == 'fish':
shell_executable = '/usr/bin/fish'
# The omf plugin defines `nvm` as a fish function that every login shell
# loads, and activates the `default` alias on load — so `nvm` and `npm` are
# both reachable in any later shell without setup, and NVM_DIR is set for us.
nvm_commands = [
INSTALL_NVM,
'omf install nvm',
f'nvm install {VERSION}',
f'nvm alias {ALIAS} {VERSION}',
]
npm_commands = [f'npm install -g {GLOBAL_PACKAGES}']
else:
shell_executable = '/bin/bash'
nvm_commands = [
INSTALL_NVM,
f'{LOAD_NVM}; nvm install {VERSION}; nvm alias {ALIAS} {VERSION}',
]
npm_commands = [f'{LOAD_NVM}; nvm use {ALIAS}; npm install -g {GLOBAL_PACKAGES}']
apt.packages(
name=f'Install nodejs tools',
no_recommends=True,
packages=[
@@ -26,33 +64,30 @@ apt.packages(
'librsvg2-dev',
'libpixman-1-dev',
],
# Every other cube that installs packages refreshes the index first, and
# this one only got away without it while `user:add` ran ahead of it and
# dragged in `apt:essentials`. On a box nobody has updated, the index
# names .deb versions the mirror has already superseded and the fetch
# 404s — the same "assumes a predecessor cube ran" defect as the shell.
update=True,
_sudo = True,
)
server.shell(
commands=[
"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash",
"omf install nvm",
f"nvm install {VERSION}",
f"nvm alias {ALIAS} {VERSION}",
"set -gx NVM_DIR $HOME/.nvm",
],
name=f'Install nvm and node {VERSION} for {USER}',
commands=nvm_commands,
_sudo=True,
_su_user=USER,
_use_su_login=True,
_shell_executable='/usr/bin/fish'
_shell_executable=shell_executable,
)
server.shell(
commands=[
"npm install -g pm2 yarn local-web-server node-gyp inquirer execa @dotenvx/dotenvx"
],
name=f'Install global packages for {USER}',
commands=npm_commands,
_sudo=True,
_su_user=USER,
_use_su_login=True,
_shell_executable='/usr/bin/fish'
_shell_executable=shell_executable,
)
@@ -7,14 +7,24 @@ export default Manifest({
dependencies: () => [],
schema: z.object({
VERSION: z
.nullable(z.string())
.string()
.describe('Node.js version to install. It is recommended to use semver notation')
.default('v22.20.0'),
USER: z.string().describe('Username for which to install nodejs').default('vagrant'),
ALIAS: z.string().describe('The alias for this node version').default('nodelts'),
// The list the deploy script used to hardcode. It is the default rather than
// a constant so that setting the variable adds to nothing and replaces
// everything — which is what "space-separated list" reads as.
GLOBAL_PACKAGES: z
.string()
.describe('Space-separated list of global npm packages to install')
.default('npm-check-updates'),
.default('pm2 yarn local-web-server node-gyp inquirer execa @dotenvx/dotenvx'),
// fish is the default because nvm wires itself into whichever shell installed
// it: switching would leave an existing user — whose login shell `user:add`
// set to fish — with node installed and invisible.
SHELL: z
.enum(['fish', 'bash'])
.describe('Login shell to install through. fish needs Oh My Fish; bash needs nothing')
.default('fish'),
}),
});
@@ -92,52 +92,7 @@ After deployment:
- Oh My Fish provides package management: `omf install <package>`
- To switch shells: `chsh -s /bin/bash` (or back to fish: `chsh -s /usr/bin/fish`)
---
# 📌 Most Useful Fish Key Bindings (with Fisher Extensions)
## 🐟 Default Fish Key Bindings
- `Ctrl + C` → Cancel the current command
- `Ctrl + D` → Exit the shell (or logout if in SSH)
- `Ctrl + L` → Clear the terminal
- `Ctrl + R` → Search command history (enhanced by `fzf.fish`)
- `Ctrl + U` → Delete the entire command line
- `Ctrl + W` → Delete the last word
- `Alt + ← / →` → Move backward/forward by a word
## 🔍 Enhanced with `fzf.fish`
- `Ctrl + R` → **Fuzzy search command history**
- `Ctrl + T` → **Fuzzy search and insert file path**
- `Alt + C` → **Fuzzy search directories (`cd` with `z`)**
## 📂 Directory Navigation (with `z`)
- `z <dir>` → Jump to a frequently used directory
- `z -l` → List most-used directories
- `z -c` → Remove a directory from `z`'s database
## 🔄 Process & Job Management
- `Ctrl + Z` → Suspend the current process
- `fg` → Bring a suspended process back to foreground
- `jobs` → List background jobs
## 🎨 Other Handy Shortcuts
- `fish_vi_key_bindings` → Enable Vi mode (press `Esc` for normal mode)
- `Ctrl + G` → Show Git status (if using `fzf.fish`)
- `Ctrl + E` → Edit command line in `$EDITOR`
## ⚙️ Useful Commands for Key Binding
```fish
# Set Fish default key bindings
fish_default_key_bindings
# Enable Vi mode
fish_vi_key_bindings
# Rebind a custom key (Example: Ctrl + G for git status)
bind \cg 'git status'
Fish's own key bindings and the plugins this cube installs are documented
upstream — `fish_key_reader` lists what is bound, and `omf help` what is
installed. They used to be reproduced here at length, which is not something
this cube knows anything about.