< Back to Blog

nvm, Node Version Hell, and Why I Always Run `nvm alias default 18` First

TL;DR / Key Takeaways

  • nvm installs Node versions but does not set a persistent default automatically. New SSH sessions inherit whatever your shell's PATH resolves first.
  • nvm alias default 18 is the command that actually fixes this. Run it once after install.
  • Environment reproducibility matters more than people think. A build that breaks because of a session-level Node version mismatch is a stupid way to lose an hour.
  • The Predict & Profit blog generator runs on this same VPS. When Node version hell breaks the build pipeline, no posts go out. So this is solved infrastructure, not a hypothetical.

I have set up nvm on a fresh VPS maybe a dozen times across different projects. Every single time, I forget to run the alias command. Every single time, I open a new SSH session and node --version spits back something I did not expect.

It is always Node 16. It is never the version I just installed.

Here is why that happens and how to stop it permanently.


Why nvm Doesn't Stick Across Sessions

nvm is not a system-level Node install. It is a shell function. When you run source ~/.nvm/nvm.sh or let your .bashrc load it at session start, nvm becomes available. But "available" and "configured the way you want" are two different things.

When you install a new version with nvm install 18, nvm downloads it and makes it active for that terminal session. It does not touch the default alias unless you tell it to. Close the terminal, open a new one, and nvm loads again from scratch. If you never set the default alias, it falls back to whatever default points to, which out of the box is often nothing, or worse, an older version you installed previously.

That older version sitting quietly in ~/.nvm/versions/node/v16.x.x did not go anywhere. nvm just loads it because it is what default points to.


The Fix

nvm install 18
nvm alias default 18
nvm use default

Three commands. Run them in order. That is it.

The alias default line is the one that actually persists. After this, every new SSH session that loads nvm will see default pointing to Node 18 and activate it automatically.

Verify it stuck:

node --version
# v18.x.x

Open a new terminal, SSH back in, check again. Should still be 18.


What Your .bashrc Needs to Look Like

nvm only loads if your shell initialization file actually sources it. On Ubuntu with bash, that means .bashrc needs these lines somewhere near the bottom:

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

The nvm install script usually adds these automatically. Usually. If you cloned nvm manually or something went sideways during setup, check that they are actually there.

tail -20 ~/.bashrc

If those lines are missing, paste them in. Then:

source ~/.bashrc
nvm alias default 18

The Broader Problem: Environment Reproducibility

Here is the thing that actually matters. A VPS is not your laptop. You do not have a GUI telling you which version of Node is active. You are jumping between SSH sessions, cron jobs are firing, CI-style scripts are running as different users, and none of them share the same interactive shell context you tested in.

A script that runs fine when you execute it manually can break when cron runs it at 2am because cron does not load .bashrc the same way an interactive session does. The Node version cron sees might be completely different from the one you verified by hand.

The fix for that is not more nvm configuration. The fix is using the full path.

Find it with:

nvm which 18
# /home/steve/.nvm/versions/node/v18.20.4/bin/node

In any cron job or systemd service that needs Node, use the absolute path instead of relying on shell initialization to get the environment right:

/home/steve/.nvm/versions/node/v18.20.4/bin/node /home/steve/content/predictandprofit/scripts/generate.js

Verbose. Annoying. Works every time.


Making the Dev Environment Idempotent

Idempotent means: run the setup twice and you get the same result both times. No new SSH session should leave you wondering which Node version you are on.

My setup script for this VPS now includes this block at the end:

#!/bin/bash

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

# Install Node 18 if not present
nvm install 18

# Set it as default regardless of current state
nvm alias default 18
nvm use default

echo "Node version: $(node --version)"
echo "npm version: $(npm --version)"

Every time I run this script, it ends in the same state. Node 18 is active, it is the default, and the version is printed to confirm it. Running it a second time on an already-configured machine does nothing harmful.

That is what idempotent looks like in practice.


One More Thing: .nvmrc

If you have a project that needs a specific Node version and multiple people touch it (or you deploy it to multiple machines), put a .nvmrc file in the project root:

echo "18" > .nvmrc

Then when anyone navigates into that directory and runs nvm use, it reads the file and activates the right version without an argument. No guessing.

You can also configure nvm to do this automatically on cd by adding a shell hook to .bashrc, but that is a rabbit hole for another day. The .nvmrc file alone solves the "which version does this project need" problem without any magic.


The Real Cost of Not Doing This

I ran into this on the VPS that powers the blog generator for this site. New SSH session, node --version came back 16, the build script failed silently, and I spent 20 minutes checking the wrong thing. The script looked fine. The dependencies were installed. The problem was one level below the code.

Twenty minutes is nothing. But the next time it happens it will be at a worse moment. Automated infrastructure should not have version-sensitive surprises lurking in it. Set the alias, use absolute paths in cron, put a .nvmrc in the project. Take the ten minutes now.

The environment should be boring. Boring means it works.