PHP-FPM, which we recently connected to Nginx and Apache in Chapter 14, handles each request through a separate worker process pool, a model proven mature for classic PHP applications such as WordPress as well as frameworks like Laravel. Developers in many companies now also frequently ask Sysadmins to install other types of web applications on the same server, ranging from Express-based REST APIs to real-time applications such as chats or dashboards that update data without reloading the page. Such applications are almost always built on top of Node.js, a server-side JavaScript runtime whose execution model is vastly different from PHP-FPM, and it is precisely this difference that makes it suitable for workloads that must handle many concurrent connections.
This chapter begins with the foundational concepts of Node.js, specifically the execution model that allows a single process to serve thousands of connections without spawning a new execution path for each request, in direct contrast to the PHP-FPM worker pool in Chapter 14. Next, we compare four common Node.js installation paths found in the field: the official Ubuntu repository, NodeSource, NVM, and fnm, complete with their respective advantages and pitfalls, before practicing hands-on installation of the Long-Term Support (LTS) version via NodeSource, multi-version installation via NVM and fnm, and enabling pnpm as an alternative package manager to npm across each of these paths. The chapter concludes by running a Node.js application as a systemd service that survives reboots, following the unit file patterns we learned in Chapter 5, while preparing a real backend to be used when discussing reverse proxies and load balancing in Chapter 16.
15.1 Understanding Node.js: Event-Driven and Non-Blocking I/O
Node.js is a server-side JavaScript runtime built on the V8 engine, the same engine that runs JavaScript inside Google Chrome. Its presence allows JavaScript code, which originally lived only in the browser, to run directly on the server to handle requests, access the filesystem, or communicate with databases, exactly like the role of PHP in Chapter 14 or other backend languages.
The fundamental difference between Node.js and PHP-FPM lies in how both handle multiple requests simultaneously. PHP-FPM, as discussed in Section 14.2.2, runs a set of worker processes in a single pool, and each worker handles one request at a time until completion. Node.js, on the other hand, essentially runs as a single process per instance, and that process never stops waiting for one operation to complete before handling another request. This model is called event-driven: every event, from an incoming HTTP request to the completion of a file read, is treated as an event queued for processing, rather than a linear flow that blocks the process.
15.1.1 Single Thread, Many Connections: How the Event Loop Works
At the core of Node.js's event-driven model is the event loop, an endless loop that continuously checks whether there are events ready to be processed. When Node.js code performs an I/O operation such as reading a file, sending a database query, or calling an external API, that operation is handed off to the operating system layer via an internal library called libuv, and the event loop is free to continue with other work instead of idling. Once the I/O operation finishes, its result is returned to the event loop in the form of a callback executed on the next turn. This pattern is called non-blocking I/O, and it is the reason a single Node.js process can serve thousands of concurrent connections without spawning a separate thread for each connection as in some traditional server models.
In practice, this characteristic makes Node.js exceptionally well-suited for applications dominated by I/O operations such as REST APIs, proxies, WebSocket-based real-time applications, or services that spend more time waiting for responses from other services than performing heavy computation. Conversely, CPU-bound processes that heavily load the processor for extended periods, such as high-resolution image processing or large-scale data encryption, can block the event loop and delay all other connections, because there is fundamentally only one main thread running the JavaScript code. Cases like this are usually addressed using Node.js's built-in worker_threads module or by offloading heavy processing to another service, an advanced topic beyond the scope of this chapter.
15.2 Installation Methods: Ubuntu Repository, NodeSource, NVM, and fnm
There are four common paths to install Node.js on Ubuntu Server, each with different trade-offs regarding versions, installation scope, and suitability for production environments. Choosing the right path from the beginning saves Sysadmins from tedious migrations later, especially when developer applications require a specific Node.js version or package manager unavailable through an already used path.
15.2.1 Official Ubuntu Repository
The simplest way is to install the nodejs package directly from the official Ubuntu repository.
sudo apt update
sudo apt install nodejs npmThe nodejs package in Ubuntu Server 26.04 resides in the universe component, not main, and provides Node.js version 22.x, which at the time of writing this module holds Maintenance LTS status rather than being the newest version. Contrary to typical package behavior, npm here is not a mandatory dependency but merely recommended, so it must be explicitly specified in the apt install command as shown in the example above, or the nodejs package alone will be installed without npm at all. The advantage of this path is its simplicity and full integration with Ubuntu's update cycle, including unattended-upgrades from Chapter 6, but the Node.js version obtained almost always lags behind the latest official release, and only one version can be installed system-wide at a time.
The nodejs package in this path also includes the node-corepack dependency, which is Corepack, the official Node.js utility bridging to alternative package managers such as pnpm and Yarn without separate installation. Because Node.js in this path is installed system-wide in /usr/bin, enabling pnpm still requires sudo.
sudo corepack enable pnpm
pnpm -v15.2.2 NodeSource as a Third-Party Repository
NodeSource is a third-party APT repository provider specifically distributing official Node.js packages from nodejs.org, following official releases much faster than the Ubuntu repository. NodeSource explicitly provides major version choices, such as 22.x for the Maintenance LTS path or 24.x for the Active LTS path, allowing Sysadmins to select the exact version required by developer applications. Just like the Ubuntu repository, the resulting installation remains system-wide via apt, placed in standard paths like /usr/bin/node, and its nodejs package automatically includes npm without requiring separate installation. This combination of up-to-date versions and consistent file paths makes NodeSource the most common choice for production servers and serves as the path we practice in Section 15.3. Node.js packages from NodeSource also carry the same Corepack as the Ubuntu repository, enabling pnpm to be activated in a similar manner, fully demonstrated in Section 15.3.3.
15.2.3 NVM as a Per-User Version Manager
NVM (Node Version Manager) takes a completely different approach by not installing Node.js via apt, but instead downloading Node.js binaries directly into the .nvm directory inside the user's home directory. A single user can install multiple Node.js versions simultaneously and switch between them at any time, even configuring different versions automatically per project via a .nvmrc file. This pattern is ideal for developer workstations that must test applications across multiple Node.js versions, but it is less ideal as a foundation for production services because binary paths depend on the active user and version rather than a fixed path like /usr/bin/node, a consequence requiring special handling when connecting to systemd in Section 15.6.2. Corepack remains available in every Node.js version installed via NVM, allowing pnpm to be enabled per version without sudo, practiced in Section 15.4.3.
15.2.4 fnm as a Lightweight Alternative to NVM
fnm (Fast Node Manager) is another Node.js version manager with a concept similar to NVM, installing multiple Node.js versions side-by-side inside a user's home directory, complete with support for .node-version and .nvmrc files to lock versions per project. The primary difference lies in implementation: fnm is written in Rust and distributed as a single binary, making its initialization process in every new shell session significantly faster than NVM, which is written as a collection of shell script functions. This difference is generally unnoticeable for daily use, but becomes apparent on servers with many short SSH sessions opened and closed frequently, or in startup-sensitive CI/CD pipelines. As a trade-off, fnm is less popular than NVM, resulting in fewer community resources and troubleshooting guides. Complete installation steps are practiced in Section 15.5.
15.2.5 Comparison Table and Usage Recommendations
These four installation paths are not mutually exclusive replacements, but rather suit different scenarios.
| Aspect | Ubuntu Repository | NodeSource | NVM | fnm |
|---|---|---|---|---|
| Version Scope | Single fixed version (22.x, Maintenance LTS) | Explicit major version choice (LTS or Current) | Multiple versions side-by-side per user | Multiple versions side-by-side per user |
| Installation Scope | System-wide | System-wide | Per user (home directory) | Per user (home directory) |
| Release Speed | Slow, follows Ubuntu freeze cycle | Fast, directly from nodejs.org | Fast, directly from nodejs.org | Fast, directly from nodejs.org |
| Binary Path | Fixed (/usr/bin/node) | Fixed (/usr/bin/node) | Changes based on active version and user | Changes based on active version and user |
| pnpm Activation via Corepack | Requires sudo | Requires sudo | Without sudo | Without sudo |
| Systemd/Production Suitability | Fair, with version limitations | Most suitable | Requires explicit path adjustment | Requires explicit path adjustment |
The most common recommendation in the field is NodeSource for production servers running applications as services, whereas NVM and fnm are equally suitable for development or staging machines requiring rapid version switching, depending on preference for shell execution speed or community maturity. Subsequent sections demonstrate installation via NodeSource, NVM, and fnm sequentially, as the Ubuntu repository path has been sufficiently practiced in Section 15.2.1.
15.3 Node.js LTS Installation via NodeSource
This section installs Node.js via the NodeSource repository using version 24.x, which holds Active LTS status at the time of writing this module. Important note: Active LTS version numbers change approximately every year according to the official Node.js release schedule, so re-check the official release page at nodejs.org to ensure the relevant version number before executing the following steps on actual servers.
15.3.1 Adding NodeSource Repository and GPG Key
NodeSource distributes a setup script that handles GPG key addition and repository registration simultaneously, significantly reducing manual steps required by Sysadmins compared to manually adding third-party repositories.
Practical Steps
- Install dependencies required for the installation process.
sudo apt update sudo apt install -y ca-certificates curl gnupg - Download the setup script to a local file first rather than executing directly via a pipe from curl, allowing contents to be inspected before running with root privileges, following the same practice as the Composer installer verification in Section 14.5.1.
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh - Run the setup script using
sudo -E. The-Eoption preserves user environment variables, required by this script to detect network proxy configurations if present.
This script adds the NodeSource GPG key tosudo -E bash nodesource_setup.sh/etc/apt/keyrings/nodesource.gpg, registers the new repository in/etc/apt/sources.list.d/nodesource.list, and automatically runsapt updateunder the hood. - Verify that the repository is properly registered.
cat /etc/apt/sources.list.d/nodesource.list
Verification and Troubleshooting
- To install another major version, such as 22.x with Maintenance LTS status, change the number in the setup script URL to
setup_22.x. Avoid usingsetup_lts.xfor production servers because that alias tracks the latest LTS, which can switch to a new major version without explicit notice each time the script is re-run, unlike explicitly specifying version numbers which yields consistent results. - Errors related to GPG or invalid signatures usually indicate the script download process was interrupted mid-way. Remove
nodesource_setup.shand repeat the download process from the beginning.
15.3.2 Node.js Installation and Verification
The NodeSource repository registered in Section 15.3.1 makes the latest nodejs package directly available via apt, replacing old versions from Ubuntu repositories if previously installed.
Practical Steps
- Install Node.js.
sudo apt install -y nodejs - Check installed Node.js and npm versions.
Unlike thenode -v npm -vnodejspackage in Ubuntu repositories in Section 15.2.1, this package from NodeSource includesnpmwithout requiring separate installation. - Run a single line of JavaScript code directly from the terminal as a quick test.
node -e "console.log('Node.js is ready for use on this server')" - Record the location of the Node.js binary, as this path will be used directly in the systemd configuration in Section 15.6.2.
which node
Verification and Troubleshooting
- Output from
which nodeshould show/usr/bin/node. If another path appears, such as/usr/local/bin/node, another Node.js installation was likely installed previously on the server, for instance via a manual tarball, creating potential conflicts. - The
nodejspackage from NodeSource updates automatically viaunattended-upgradesfrom Chapter 6 as long as it remains within the same major version, but upgrading to a new major version must be done manually by repeating Section 15.3.1 with the new version number.
15.3.3 Enabling pnpm via Corepack
pnpm is an alternative package manager to npm that stores all dependencies in a centralized content-addressable store and connects them to each project via symlinks, drastically saving disk space and accelerating dependency re-installation compared to npm or classic Yarn, especially on servers running multiple Node.js projects simultaneously. From Node.js 14.19 up to version 25, official Node.js releases include Corepack by default, an official utility bridging Node.js with pnpm or Yarn without requiring manual installation via npm install -g, which risks producing inconsistent versions across servers.
Practical Steps
- Verify Corepack is available alongside the Node.js installation from Section 15.3.2.
corepack --version - Enable the pnpm shim. Since Node.js from NodeSource is installed system-wide in
/usr/bin, activation requiressudo.sudo corepack enable pnpm - Verify pnpm can be called.
pnpm -v
Verification and Troubleshooting
- The first attempt to run
pnpmusually displays aCorepack is about to download...prompt, because Corepack downloads specific pnpm binaries only when actually needed. For non-interactive automation such as CI/CD scripts, set the environment variableCOREPACK_ENABLE_DOWNLOAD_PROMPT=0to allow this process to run without waiting for confirmation. - Per-project pnpm versions can be locked via the
packageManagerfield inpackage.json, for example"packageManager": "[email protected]", configured automatically using thecorepack use pnpm@<version>command inside project directories. - Node.js 25 and above no longer include Corepack by default. If migrating to those versions, install Corepack manually first via
sudo npm install -g corepackbefore runningcorepack enable pnpmabove.
15.4 Multi-Version Installation via NVM
A common scenario Sysadmins face involves developers needing to test legacy applications bound to Node.js version 22.x, while other projects on the same machine use version 24.x. This section demonstrates installing NVM to satisfy multi-version requirements for regular users, separate from system-wide Node.js installations via NodeSource installed in Section 15.3.
15.4.1 Installing NVM
Practical Steps
- Run the official NVM install script as a regular user, not
root, as NVM is designed to be installed per user.
This script clones NVM into thecurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash~/.nvmdirectory and adds loader lines to shell profile files like~/.bashrcso that thenvmcommand is available automatically in every new terminal session. - Reload the shell profile without logging out, or open a new terminal session.
source ~/.bashrc - Verify that the
nvmcommand is recognized.command -v nvm
Verification and Troubleshooting
- If
command -v nvmproduces no output, check whether NVM loader lines were written to~/.bashrc, because alternative shells likefishdo not use this file and require separate NVM plugins not covered in this chapter. - NVM is intentionally not installed with
sudoor for therootuser. Re-installing as a different user on the same server produces completely separate.nvmdirectories.
15.4.2 Managing Multiple Node.js Versions with NVM
Practical Steps
- View available LTS versions for installation.
nvm ls-remote --lts - Install two versions simultaneously to simulate multi-version needs: the latest LTS version and version 22.x.
nvm install --lts nvm install 22 - View all versions installed via NVM for this user.
nvm ls - Switch between versions and check the active version each time.
nvm use 22 node -v nvm use --lts node -v - Set a default version that automatically activates in new terminal sessions.
nvm alias default 22 - Create a demo project directory that locks its own Node.js version via
.nvmrc, independent of the default version above.
Themkdir ~/demo-project && cd ~/demo-project echo "lts/*" > .nvmrc nvm usenvm usecommand without arguments inside a directory containing a.nvmrcfile automatically reads file contents and switches to the specified version.
Verification and Troubleshooting
- Active Node.js binary paths under NVM always follow the selected version and user running it, such as
~/.nvm/versions/node/v22.x.x/bin/node, distinctly different from fixed paths like/usr/bin/nodefrom NodeSource in Section 15.3.2. Check with the following command:which node - Both Node.js installations, via NodeSource and NVM, can coexist without conflicts on the same server because they use completely different paths and do not overwrite each other.
- This path difference is why NVM requires special handling when running applications as systemd services, discussed in Section 15.6.2.
15.4.3 Enabling pnpm via Corepack in NVM
Corepack is included with every Node.js version downloaded by NVM in Section 15.4.2, as binaries downloaded by NVM are identical to official distributions from nodejs.org. Unlike Section 15.3.3, activation here does not require sudo at all, because the entire Node.js installation via NVM resides within the user's home directory rather than system directories.
Practical Steps
- Ensure the Node.js version for which pnpm should be enabled is currently active.
nvm current - Enable the pnpm shim for the currently active Node.js version.
corepack enable pnpm - Verify.
pnpm -v
Verification and Troubleshooting
- This pnpm shim is placed inside the
bindirectory of the active Node.js version when executed, rather than a shared global location. Whenever installing a new Node.js version vianvm install, repeatcorepack enable pnpmfor that version before use. - Notes regarding download prompts and locking pnpm versions via
packageManagerinpackage.jsonfrom Section 15.3.3 apply identically here.
15.5 Multi-Version Installation via fnm
This section practices fnm as an alternative to NVM whose concepts were introduced in Section 15.2.4, suitable for Sysadmins or Developers prioritizing shell startup speed over NVM's established ecosystem maturity.
15.5.1 Installing fnm
Practical Steps
- Run the official fnm install script as a regular user, similar to NVM, as fnm is also designed to be installed per user.
This script downloads the fnm binary tocurl -fsSL https://fnm.vercel.app/install | bash~/.local/share/fnmand adds initialization lines to shell profile files like~/.bashrc. - Reload the shell profile or open a new terminal session.
source ~/.bashrc - Verify that the
fnmcommand is recognized.fnm --version
Verification and Troubleshooting
- If
fnm --versionis not recognized after opening a new terminal, check whether thefnm envinitialization line was written to~/.bashrc, following NVM troubleshooting patterns in Section 15.4.1. - fnm and NVM can be installed side-by-side for the same user without conflicts because both store Node.js binaries in completely different directories, but only one should be actively used to avoid confusion regarding active versions.
15.5.2 Installing and Managing Node.js Versions with fnm
Practical Steps
- Install Node.js version 24.x via fnm.
fnm install 24 - Check the active version.
Output displays full version numbers such asnode -vv24.20.0, matching the latest patch release of the Active LTS path when executed, so exact sub-version numbers may vary. - View all versions installed via fnm.
fnm list - Set a default version that automatically activates in new terminal sessions, equivalent to
nvm alias defaultin Section 15.4.2.fnm default 24 - Create a
.node-versionfile in a test project directory to lock its Node.js version, the default format used by fnm.mkdir ~/demo-project-fnm && cd ~/demo-project-fnm echo "24" > .node-version fnm use
Verification and Troubleshooting
- fnm also reads
.nvmrcfiles if.node-versionis missing, so project directories previously created for NVM in Section 15.4.2 remain automatically readable by fnm without modification. - Auto-switching versions on directory changes (
cd) is disabled by default, activating only wheneval "$(fnm env --use-on-cd --shell bash)"is present in shell profile files, typically added automatically by the official installer in Section 15.5.1 for detected shells. - Active Node.js binary paths under fnm can be inspected using the same method as NVM.
which node
15.5.3 Enabling pnpm via Corepack in fnm
Like NVM, Node.js installed via fnm includes built-in Corepack, allowing pnpm to be enabled without sudo.
Practical Steps
- Enable the pnpm shim for the active Node.js version.
corepack enable pnpm - Verify.
pnpm -v
Verification and Troubleshooting
- fnm provides a shortcut combining Node.js installation and Corepack activation in a single command using the
--corepack-enabledflag when installing new versions.fnm install 24 --corepack-enabled - Notes regarding pnpm download prompts and locking versions via
packageManagerinpackage.jsonfrom Section 15.3.3 apply identically here.
15.6 Running Applications as Services with Systemd
Node.js applications launched directly from terminal sessions terminate immediately when SSH sessions close or servers reboot, making direct invocation unsuitable for production environments. This section applies systemd unit file patterns learned in Section 5.2, tailored specifically for Node.js applications installed via NodeSource in Section 15.3, enabling applications to start automatically on boot and auto-recover from crashes.
15.6.1 Creating a Simple Node.js Application
As an example, we create a small HTTP server application using Node.js's built-in http module without external framework dependencies like Express, keeping focus on systemd configuration rather than application development.
Practical Steps
- Prepare the application directory.
sudo mkdir -p /opt/demo-node - Create the
server.jsfile.sudo nano /opt/demo-node/server.js - Insert the following code:
Theconst http = require('http'); const os = require('os'); const port = process.env.PORT || 3000; const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(`Node.js active on ${os.hostname()}, server time: ${new Date().toISOString()}\n`); }); server.listen(port, () => { console.log(`Server running on port ${port}`); });PORTenvironment variable is read viaprocess.envrather than hardcoded, allowing port changes later via systemd configuration without modifying application code. - Test running directly from terminal to verify there are no errors before creating a service.
node /opt/demo-node/server.js - From another terminal session, test with curl.
curl http://localhost:3000 - Stop the manual process using
Ctrl+Cbefore proceeding to the next step.
15.6.2 Creating a Systemd Unit for the Node.js Application
Practical Steps
- Create a dedicated system user for this application, following least privilege principles from Section 5.2.2.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapp - Set ownership for the application directory.
sudo chown -R nodeapp:nodeapp /opt/demo-node - Create a new unit file.
sudo nano /etc/systemd/system/demo-node.service - Insert the following configuration:
The[Unit] Description=Demo Node.js Application After=network-online.target Wants=network-online.target [Service] Type=simple User=nodeapp Group=nodeapp WorkingDirectory=/opt/demo-node Environment=NODE_ENV=production Environment=PORT=3000 ExecStart=/usr/bin/node /opt/demo-node/server.js Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target/usr/bin/nodepath inExecStartmatches the output ofwhich noderecorded in Section 15.3.2, providing a fixed system path independent of specific users. - Notify systemd that a new unit file exists.
sudo systemctl daemon-reload - Enable and start the service simultaneously.
sudo systemctl enable --now demo-node.service - Re-test using curl.
curl http://localhost:3000
Verification and Troubleshooting
- Check service status and recent log entries.
systemctl status demo-node.service - Follow application logs live, including
console.logoutput from Node.js code automatically captured byjournalctlwithout extra logging setup, similar to Python applications in Section 5.2.2.journalctl -u demo-node.service -f Address already in useerrors in logs typically indicate the manual process from Section 15.6.1 was not stopped prior to activating the service. Check processes occupying the port:ss -tlnp | grep 3000- The most common error when Node.js applications are installed via NVM or fnm instead of NodeSource is setting
ExecStartto justnodewithout a full path, relying on thePATHactive in interactive shell sessions. Systemd runs services without loading login shells ornvm/fnmfunctions from~/.bashrc, resulting in immediate failures withnode: command not foundmessages even ifnodeworks normally when tested manually. If using Node.js from NVM or fnm as a service foundation,ExecStartmust specify full paths to specific binary versions, such as/home/deploy/.nvm/versions/node/v22.x.x/bin/node(obtained vianvm which 22when logged in as that user). This is the primary reason NodeSource is recommended for production services over NVM or fnm, aligning with trade-offs listed in Section 15.2.5. - If real applications use third-party npm dependencies installed via pnpm from Section 15.3.3, 15.4.3, or 15.5.3, this unit file configuration requires no changes. Simply run
pnpm install --prodin theWorkingDirectorybefore first running the service or deploying new versions, asExecStartcallsnodedirectly rather than throughpnpm.
Node.js running as a systemd service functions as a standard application backend listening on a single port, identical to PHP-FPM in Chapter 14 or other applications hosting their own HTTP servers. Chapter 16 continues with reverse proxies and load balancing, treating backends such as Node.js, PHP-FPM, or other applications as upstream components whose traffic can be distributed and scaled using Nginx.

