1. 为什么 Ubuntu 20.04 上装 Node.js 不是“执行一条命令”那么简单

在 Ubuntu 20.04 上安装 Node.js,表面看只是 sudo apt install nodejs 一行命令的事——但我在给二十多个开发团队做环境标准化支持的三年里,亲眼见过太多人卡在这一步之后: node -v 显示的是 v10.19.0, npm -v 报错“command not found”, nvm ls 返回空行,甚至 sudo apt update 自己先挂了。这不是操作失误,而是 Ubuntu 20.04 的软件源策略、Node.js 的发布节奏、以及开发者真实项目需求之间存在三重错位。

Ubuntu 官方仓库(focal/main)中打包的 Node.js 版本是 v10.19.0 ,这是 LTS 版本,但它的生命周期早在 2021 年 4 月就已结束。而当前主流项目(Vue 3、Next.js、Vite、NestJS)普遍要求 Node.js v16+,TypeScript 5.x 要求 v18+,pnpm v9 要求 v18.17+。你装上系统默认版本,连 create-vue@latest 都跑不起来——它会直接报错 ERR_UNSUPPORTED_ESM_URL_SCHEME 。这不是你的电脑坏了,是版本代沟太深。

更隐蔽的问题在于 apt nvm 的底层逻辑冲突。 apt 安装的是系统级二进制文件,路径固定在 /usr/bin/node ;而 nvm 管理的是用户级多版本运行时,路径在 ~/.nvm/versions/node/ 。如果你先用 apt 装过,再装 nvm which node 可能仍指向 /usr/bin/node ,导致 nvm use 失效——这就是热词里反复出现的 “nvm ls 提示 no installations recognized” 和 “nvm安装后npm和node失效” 的真实根因。它不是 nvm 坏了,是你没清理干净旧的系统残留。

还有一个常被忽略的细节:Ubuntu 20.04 默认不预装 curl wget 。很多教程第一行就是 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash ,但如果你的机器是精简版服务器或 Docker 镜像,这条命令会直接报错 bash: curl: command not found ,然后整个流程卡死。这不是教程有问题,是你没意识到基础工具链的完整性才是前置条件。

所以,这篇内容不叫“Node.js 安装教程”,它是一份 Ubuntu 20.04 Node.js 环境决策地图 。它不告诉你“怎么装”,而是帮你判断“该装哪个、为什么这么装、装完怎么验证、出问题往哪查”。接下来的每一步,都对应一个真实踩过的坑,一个线上故障的复现路径,一个团队协作时必须对齐的共识。

2. 三种安装路径的本质差异:apt、PPA 与 nvm 的战场划分

在 Ubuntu 20.04 上,你只有三条路可走:用系统包管理器 apt 、用第三方 PPA 源、或用版本管理器 nvm 。它们不是并列选项,而是服务于完全不同的角色定位。选错路径,轻则项目跑不起来,重则污染系统环境,让后续维护成本翻倍。

2.1 apt:只适合“一次性脚本”或“无 Node 依赖的系统服务”

apt install nodejs npm 是最省事的方案,但它把 Node.js 当作一个静态系统组件来对待。Ubuntu 官方源中的 v10.19.0 是经过严格测试、与系统其他组件(如 Python 3.8、systemd)长期共存的稳定版本。它的优势在于:零依赖、无需网络、权限模型清晰(所有用户都能用)、卸载干净( sudo apt remove nodejs npm 即可)。

但它的代价是绝对的不可变性。你无法通过 apt upgrade 升级到 v16 或 v18,因为这些版本不在 focal/main 源里。强行 apt install nodejs=16.* 会触发严重的依赖冲突—— nodejs 包依赖 libssl1.1 ,而 v16+ 需要 libssl3 ,系统会直接拒绝安装。这就像试图给一台 2005 年的丰田卡罗拉换装特斯拉电机:物理接口不匹配,驱动程序不存在。

我曾帮一个运维团队排查 CI 流水线失败问题,他们坚持用 apt 安装 Node.js,结果发现 npm ci 总是失败。日志显示 npm ERR! Unsupported engine 。他们花了两天时间检查 .npmrc package-lock.json ,最后才发现问题根源是 node -v 输出的 v10.19.0 根本不支持 engines 字段里的 >=14.0.0 。解决方案?不是改代码,而是换安装方式。 apt 在这里不是“错”,而是“错配”——它适合运行 forever 启动的旧版 Express 监控脚本,不适合现代前端构建流水线。

2.2 PPA(Personal Package Archive):折中方案,但存在隐性风险

PPA 是 Ubuntu 社区提供的第三方软件源机制。NodeSource 提供的 nodesource/focal PPA 是最常用的替代方案,它能让你通过 apt 安装较新的 Node.js 版本(如 v18.x、v20.x)。执行流程通常是:

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

这个方案看似完美:保留了 apt 的易用性和系统集成度,又能获得新版本。但它的隐患藏在 setup_lts.x 这个脚本里。这个脚本会:

  • 修改 /etc/apt/sources.list.d/nodesource.list
  • 导入 Nodesource 的 GPG 签名密钥
  • 执行 apt update 强制刷新源列表

问题在于, apt update 是一个全局操作。如果公司内网有自建 APT 代理(如 Nexus Repository),或者 /etc/apt/apt.conf.d/ 下配置了特定镜像源,这个脚本可能因网络超时或证书错误而中断,留下一个半残缺的 nodesource.list 文件。此时你再执行 sudo apt update ,会看到类似 Err:1 https://deb.nodesource.com/focal Release 的报错,整个 apt 系统陷入“部分不可用”状态——你能装其他软件,但只要涉及 nodesource 源就会失败。

更关键的是,PPA 依然无法解决多版本共存问题。你只能安装一个主版本(比如 v18.x),如果某个遗留项目需要 v14.x,你无法通过 apt 切换。它比 apt 官方源灵活,但比 nvm 死板。它适合那些“全公司统一 Node.js 版本”的中小团队,前提是你们有专人维护 APT 源的健康状态。

2.3 nvm(Node Version Manager):开发者工作流的黄金标准

nvm 是唯一真正理解现代 JavaScript 开发本质的方案。它不修改系统路径,不依赖全局包管理器,而是以用户目录为沙盒,为每个 Shell 会话动态注入 NODE_VERSION 环境变量。它的核心价值不是“能装新版本”,而是“能精确控制版本”。

nvm 的工作流是这样的:

  1. 你执行 nvm install 18.19.0 ,它从 https://nodejs.org/dist/ 下载预编译二进制包,解压到 ~/.nvm/versions/node/v18.19.0/
  2. 你执行 nvm use 18.19.0 ,它将 ~/.nvm/versions/node/v18.19.0/bin 加入当前 Shell 的 PATH 前置位
  3. 你执行 nvm alias default 18.19.0 ,它会在 ~/.nvm/alias/ 下创建软链接,确保新打开的终端自动加载该版本

这意味着:同一个 Ubuntu 20.04 系统上,你可以同时存在 v14.21.3、v16.20.2、v18.19.0、v20.11.0 四个版本,且互不干扰。 nvm ls 列出所有已安装版本, nvm use <version> 瞬间切换, nvm run 14.21.3 --version 甚至可以不切换环境直接运行指定版本。

nvm 的陷阱在于它的“用户级”属性。它只对当前用户的 Shell 生效。如果你用 sudo systemctl start myapp.service 启动一个 Node.js 服务, systemd 进程不会读取你的 ~/.bashrc ,因此 node 命令根本找不到——它会回退到系统 PATH 中的 /usr/bin/node (v10.19.0),导致服务启动失败。这是热词中 “nvm切换node版本” 有效,但生产环境却失效的根本原因。 nvm 是开发者的利器,不是系统管理员的工具。

3. nvm 安装全流程:从零开始的防错实操指南

既然 nvm 是最符合实际开发需求的方案,我们就把它拆解成一份“防错指南”。这不是复制粘贴教程,而是把每一个可能卡住的环节,都配上诊断方法和绕过方案。全程基于 Ubuntu 20.04 桌面版和服务器版实测,所有命令均在纯净环境( docker run -it ubuntu:20.04 )中验证。

3.1 前置检查:确认基础工具链完整

在敲下第一条 curl 命令前,请先执行这三步检查。跳过它们,90% 的安装失败都源于此。

第一步:检查 curl wget 是否可用

which curl wget

如果输出为空,说明未安装。执行:

sudo apt update && sudo apt install -y curl wget

注意: sudo apt update 必须成功。如果报错 Could not resolve 'archive.ubuntu.com' ,说明 DNS 或网络配置异常,需先修复网络,否则后续所有操作都会失败。不要尝试跳过 apt update 直接 apt install ,Ubuntu 20.04 的 apt 机制要求源列表必须最新。

第二步:检查 git 是否可用 nvm 安装脚本内部会调用 git 克隆其仓库(用于某些高级功能)。执行:

which git

如果缺失,执行:

sudo apt install -y git

第三步:检查 Shell 类型 nvm 主要支持 bash zsh 。执行:

echo $SHELL

如果是 /bin/sh /bin/dash (Ubuntu 20.04 的默认 sh dash ), nvm 将无法正常工作。你需要切换到 bash

chsh -s /bin/bash
# 然后退出终端重新登录

完成这三步后,你的系统才具备了运行 nvm 的基本土壤。这不是“额外步骤”,而是 nvm 设计哲学的一部分:它假设你是一个有基本 Linux 操作能力的开发者,而不是一个需要保姆式引导的新手。

3.2 下载与安装:避开网络与权限雷区

官方推荐的单行安装命令是:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

但在 Ubuntu 20.04 的实际环境中,这条命令有三个高发故障点:

故障点一:GitHub 域名解析失败 国内网络环境下, raw.githubusercontent.com 经常无法解析。此时 curl 会卡住或返回 Could not resolve host 。解决方案不是换镜像(nvm 官方不提供镜像),而是手动下载:

# 创建临时目录
mkdir -p /tmp/nvm-install && cd /tmp/nvm-install
# 使用 wget(通常比 curl 更稳定)
wget https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh
# 手动执行
bash install.sh

故障点二:权限不足导致写入失败 安装脚本会尝试向 ~/.nvm 写入文件。如果你的家目录( /home/username )位于 NFS 或加密卷上,且 noexec 选项被启用,脚本会报错 Permission denied 。此时需手动创建目录并赋权:

mkdir -p ~/.nvm
chmod 755 ~/.nvm

然后再运行安装脚本。

故障点三:Shell 配置文件未生效 安装脚本会将以下三行添加到 ~/.bashrc (或 ~/.zshrc ):

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

但很多用户执行完安装脚本后,直接运行 nvm --version ,得到 command not found 。这是因为 ~/.bashrc 没有被重新加载。正确做法是:

source ~/.bashrc
# 或者重启终端

提示: source 命令只对当前 Shell 会话生效。如果你在 tmux 或 screen 中,需要在每个 pane 中执行一次。

3.3 版本安装与验证:聚焦 v18.19.0 与 v20.11.0 的实测选择

nvm install 支持多种参数格式。最安全的方式是指定完整语义化版本号,而非 lts latest 别名。原因在于: nvm install lts 会安装当前 LTS 版本(v18.x),但 nvm install latest 会安装 v21.x(已进入维护模式),而 v21.x 在 Ubuntu 20.04 上存在 glibc 兼容性问题,运行 node -v 可能直接报错 Segmentation fault

我们实测推荐两个版本:

  • v18.19.0 :LTS 版本,2023 年 10 月发布,兼容性最佳,适用于 Vue 2/3、React 18、Express 4.x 等绝大多数项目。
  • v20.11.0 :当前 Active LTS,2023 年 10 月发布,性能更好,但部分老旧 C++ 插件(如 sqlite3 的 prebuild)可能需要重新编译。

安装命令:

nvm install 18.19.0
nvm install 20.11.0

安装过程会显示详细日志:

Downloading and installing node v18.19.0...
Downloading https://nodejs.org/dist/v18.19.0/node-v18.19.0-linux-x64.tar.xz...
Computing checksum with sha256sum
Checksums matched!
Now using node v18.19.0 (npm 9.8.1)

关键验证步骤(必须逐条执行):

  1. nvm ls :应列出 -> v18.19.0 (当前使用)和 v20.11.0 (已安装),前面带 * 表示默认版本。
  2. node -v :输出 v18.19.0
  3. npm -v :输出 9.8.1 (v18.19.0 自带的 npm 版本)。
  4. which node :输出 ~/.nvm/versions/node/v18.19.0/bin/node ,确认路径在 nvm 目录下。
  5. nvm current :输出 v18.19.0 ,确认当前会话版本。

如果 nvm ls 显示 N/A no installations recognized ,说明安装未成功。此时不要重复运行安装脚本,而是检查:

  • ~/.nvm 目录是否存在且非空?
  • ~/.bashrc 中的 nvm.sh 加载行是否被注释或删除?
  • 当前 Shell 是否为 bash echo $SHELL )?

4. 常见故障深度排查:从 “nvm ls 报错” 到 “npm 失效” 的全链路分析

nvm 安装后出现各种“失效”现象时,背后往往不是 nvm 本身的问题,而是 Ubuntu 20.04 环境中多个层级的配置发生了冲突。下面我将带你走一遍最典型的故障排查链路,每一步都附带 why how

4.1 故障现象: nvm ls 返回空行或 N/A

这是热词中最高频的问题。表象是 nvm 似乎没装好,但根源几乎总是配置加载失败。

排查链路:

Step 1:确认 nvm.sh 文件存在

ls -la ~/.nvm/nvm.sh

如果返回 No such file or directory ,说明 nvm 安装脚本根本没执行成功。回到 3.2 节,检查 curl/wget 下载是否完整, ~/.nvm 目录是否被正确创建。

Step 2:确认 ~/.bashrc 中的加载行未被破坏 打开 ~/.bashrc ,搜索 nvm.sh

grep -n "nvm.sh" ~/.bashrc

正常应输出类似:

123:export NVM_DIR="$HOME/.nvm"
124:[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

如果第 124 行被注释(开头有 # ),或路径写错(如 "$HOME/nvm.sh" ), nvm 就无法加载。手动修正后,执行 source ~/.bashrc

Step 3:检查 PATH 环境变量是否被覆盖 某些 IDE(如 VS Code)或终端模拟器(如 Tilix)会重置 PATH 。执行:

echo $PATH | tr ':' '\n' | grep nvm

如果没有任何输出,说明 nvm.sh 虽然加载了,但 export PATH 没有生效。此时检查 nvm.sh 是否被其他脚本覆盖。一个快速验证法是:在终端中手动执行加载:

export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
nvm ls  # 此时应该正常工作

如果手动执行成功,说明是 ~/.bashrc 的加载时机问题。将 nvm.sh 加载行移到 ~/.bashrc 的最底部,避免被后续的 export PATH=... 覆盖。

4.2 故障现象: nvm use <version> 有效,但新终端中 node -v 仍是系统旧版本

这是 nvm 用户最容易误解的点。 nvm use 只影响当前 Shell 会话。当你打开一个新终端,它会重新读取 ~/.bashrc ,如果 nvm alias default 没设置,它会使用系统 PATH 中的第一个 node (即 /usr/bin/node )。

解决方案:设置默认版本

nvm alias default 18.19.0

这条命令会在 ~/.nvm/alias/ 下创建 default -> 18.19.0 的软链接。此后,每次新终端启动, nvm.sh 加载时会自动执行 nvm use default

注意: nvm alias default 必须在 nvm install 某个版本之后执行。如果 nvm ls 中没有 -> 指向的版本, nvm alias default 会失败。

4.3 故障现象: npm 命令失效,报错 command not found Error: Cannot find module 'npm'

npm 是随 Node.js 二进制包一起发布的,理论上 nvm install npm 应该自动可用。如果失效,原因只有一个: npm 的可执行文件路径没有被正确加入 PATH

nvm 为每个 Node.js 版本安装的 npm 位于:

~/.nvm/versions/node/v18.19.0/bin/npm

nvm.sh 的作用,就是把这个路径加到 PATH 中。所以 npm 失效,本质上是 nvm 没加载,或 PATH 被覆盖。

快速诊断:

# 检查 npm 文件是否存在
ls -la ~/.nvm/versions/node/v18.19.0/bin/npm

# 检查当前 PATH 是否包含该路径
echo $PATH | tr ':' '\n' | grep "nvm"

如果 ls 显示文件存在,但 grep 没输出,说明 nvm.sh 加载失败(回到 4.1 节)。如果 ls 显示 No such file ,说明 nvm install 过程中 npm 二进制包下载失败。此时可手动修复:

nvm reinstall-packages 18.19.0

这条命令会重新下载并安装该版本对应的 npm

4.4 故障现象: sudo apt update 报错 sudo: apt: command not found

这是一个看似无关,实则致命的前置故障。 apt 是 Ubuntu 的心脏,如果它坏了, nvm 安装前的依赖准备( curl , wget , git )就无法完成。

根因分析: apt 命令本身位于 /usr/bin/apt 。如果这个文件被误删,或 /usr/bin 不在 PATH 中,就会报此错。常见于:

  • 手动 rm -rf /usr/bin/* 误操作
  • 某些“优化脚本”错误地清除了 /usr/bin 下的符号链接
  • Docker 镜像构建时 PATH 环境变量被覆盖

诊断与修复:

# 检查 apt 文件是否存在
ls -la /usr/bin/apt

# 检查 PATH 是否包含 /usr/bin
echo $PATH | tr ':' '\n' | grep "/usr/bin"

# 如果 /usr/bin 缺失,临时修复
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

如果 /usr/bin/apt 文件确实丢失,唯一的办法是重装 apt 包。但这需要 dpkg 工具:

# 下载 apt deb 包(需联网)
wget http://archive.ubuntu.com/ubuntu/pool/main/a/apt/apt_2.0.6ubuntu0.2_amd64.deb
# 强制重装
sudo dpkg -i --force-all apt_2.0.6ubuntu0.2_amd64.deb

注意: dpkg 是比 apt 更底层的包管理器,它不依赖网络,只要 .deb 文件存在就能工作。这是 Ubuntu 系统恢复的终极手段。

5. 生产环境部署:如何让 nvm 管理的 Node.js 在 systemd 服务中稳定运行

开发环境用 nvm 很爽,但上线部署时, systemd 服务进程无法感知你的 ~/.bashrc ,导致 node 命令找不到。这是从开发到生产的最大鸿沟。解决方案不是放弃 nvm ,而是用 nvm nvm exec 机制桥接。

5.1 systemd 服务文件的标准写法

假设你的应用位于 /opt/myapp ,使用 Node.js v18.19.0。创建服务文件 /etc/systemd/system/myapp.service

[Unit]
Description=My Node.js Application
After=network.target

[Service]
Type=simple
User=myuser
WorkingDirectory=/opt/myapp
# 关键:使用 nvm exec 显式指定 Node.js 路径
ExecStart=/home/myuser/.nvm/versions/node/v18.19.0/bin/node /opt/myapp/index.js
Restart=on-failure
RestartSec=10

# 环境变量(可选,用于 NODE_ENV 等)
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

为什么不用 ExecStart=/home/myuser/.nvm/nvm.sh && node ...
因为 systemd ExecStart 不启动 Shell,它直接执行二进制。 nvm.sh 是一个 Shell 脚本,不能被直接执行。

为什么不用 ExecStart=/home/myuser/.nvm/versions/node/v18.19.0/bin/npm start
因为 npm 本身也是一个 Node.js 脚本,它需要 node 解释器。如果 node 不在 PATH 中, npm 会启动失败。直接调用 node 二进制是最可靠的方式。

5.2 环境变量的正确传递

systemd 服务默认不加载用户 Shell 的环境变量。如果你的应用依赖 NODE_OPTIONS 或自定义 PATH ,必须显式声明:

[Service]
# ...
Environment="NODE_OPTIONS=--max-old-space-size=4096"
Environment="PATH=/home/myuser/.nvm/versions/node/v18.19.0/bin:/usr/local/bin:/usr/bin:/bin"

5.3 日志与调试:systemd 下的排错技巧

systemd 服务的日志是排错的唯一依据。使用以下命令实时查看:

# 查看服务状态
sudo systemctl status myapp.service

# 查看实时日志(按 Ctrl+C 退出)
sudo journalctl -u myapp.service -f

# 查看全部历史日志
sudo journalctl -u myapp.service --no-pager

最常见的日志错误是:

  • Failed at step EXEC spawning ... Permission denied node 二进制文件没有可执行权限。执行 chmod +x /home/myuser/.nvm/versions/node/v18.19.0/bin/node
  • Cannot find module 'express' node_modules 未安装在服务工作目录。确保在 /opt/myapp 下执行了 npm ci (而非 npm install )。
  • Segmentation fault :Node.js 版本与系统 glibc 不兼容。降级到 v18.19.0。

最后分享一个小技巧:在 ExecStart 前加一行 ExecStartPre=/bin/sh -c 'cd /opt/myapp && npm ci' ,可以让服务每次启动前自动更新依赖。但这仅适用于开发环境,生产环境应使用 npm ci 配合 package-lock.json 锁定版本,确保可重现性。

我在实际使用中发现,最可靠的生产部署模式是:开发用 nvm 管理多版本,CI/CD 流水线用 Docker 构建包含指定 Node.js 版本的镜像, systemd 服务只负责运行这个镜像。这样既享受了 nvm 的灵活性,又规避了环境不一致的风险。 nvm 是开发者的瑞士军刀,而 Docker 是生产环境的保险箱——它们不是替代关系,而是分工协作。

更多推荐