Skip to content

PM2 as systemd — force Node.js version

When PM2 is started by systemd, it does not read ~/.bashrc the way an interactive SSH session does. The node binary comes from PATH in the [Service] unit unless you pin interpreter in the ecosystem file.

If npm / EBADENGINE still sees an old Node while nvm shows a new one in your SSH session, two common causes:

  1. systemd PATH still has /usr/bin before the nvm bin directory.
  2. nvm is only loaded from .bashrc, and non-interactive shells exit .bashrc early — so sudo -u coaching bash -lc 'node -v' never loads nvm (you see distro /usr/bin/node).

nvm — why sudo … bash -lc shows Node 20 but SSH shows Node 24

Interactive SSH runs interactive bash → .bashrc runs fully → nvm loads.

bash -lc 'cmd' is a login shell but non-interactive. On Ubuntu/Debian, ~/.profile often sources ~/.bashrc. The default .bashrc starts with “if not interactive, return”, so nvm never loads and command -v node is /usr/bin/node (distro).

Check like systemd (non-interactive)

sudo -u coaching -H bash -lc 'command -v node && node -v'

Check with nvm loaded explicitly (matches “what I expect”)

Either interactive:

sudo -u coaching -H bash -lic 'command -v node && node -v'

Or source nvm in one line:

sudo -u coaching -H bash -lc 'export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" && command -v node && node -v'

Fix .profile so login + non-interactive shells get nvm (optional)

Edit /home/coaching/.profile: load nvm before the block that sources .bashrc, so even when .bashrc returns early, nvm is already on PATH.

Add above any source ~/.bashrc line:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

Then verify:

sudo -u coaching -H bash -lc 'command -v node && node -v'

You should see …/.nvm/versions/node/v24…/bin (or your default nvm alias default).

Do not rely on nvm inside systemd. Put the exact version bin directory first in the unit PATH:

/home/coaching/.nvm/versions/node/v24.15.0/bin

(Adjust version folder if you nvm install another release.)

Drop-in override

sudo systemctl edit pm2-coaching.service
# use your real unit name from: systemctl list-units '*pm2*'

Example:

[Service]
Environment=PATH=/home/coaching/.nvm/versions/node/v24.15.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

Then:

sudo systemctl daemon-reload
sudo systemctl restart pm2-coaching.service

2. Confirm PM2 and the app

After fixing PATH, confirm under the same conditions systemd uses:

sudo -u coaching -H env PATH=/home/coaching/.nvm/versions/node/v24.15.0/bin:/usr/bin:/bin bash -lc 'node -v && pm2 describe coaching'

Adjust the PATH prefix to match your installed version directory.

3. Bulletproof: interpreter in ecosystem

Pin the binary so pm2 resurrect always uses the right Node:

module.exports = {
  apps: [
    {
      name: 'coaching',
      script: 'node_modules/next/dist/bin/next',
      args: 'start',
      interpreter: '/home/coaching/.nvm/versions/node/v24.15.0/bin/node',
      cwd: '/var/www/coaching',
    },
  ],
};

Then pm2 reload ecosystem.config.cjs --update-env and pm2 save.

4. Stale PM2 dump

If pm2 describe coaching still shows an old interpreter:

sudo -u coaching -H bash -lic 'cd /var/www/coaching && pm2 delete coaching && pm2 start ecosystem.config.cjs && pm2 save'

(bash -lic loads nvm so pm2 on PATH is the one you expect.)

Repository example

documentation/examples/systemd/pm2-node-path.conf.example — replace the first PATH segment with your nvm bin directory.