Cài Node.js trên Ubuntu server: chọn cách nào?
So sánh bốn cách cài Node.js trên Ubuntu server: gói apt của Ubuntu, kho NodeSource, nvm và fnm. Ai chịu trách nhiệm nâng cấp, và vì sao unit systemd không tìm thấy node.
Câu trả lời ngắn
Cài Node.js trên Ubuntu server có bốn con đường: gói nodejs của chính Ubuntu, kho apt của NodeSource, nvm và fnm. Lựa chọn đúng phụ thuộc vào một câu hỏi duy nhất: có tiến trình nào chạy nền dưới systemd cần Node không.
Nếu có, hãy cài ở mức hệ thống bằng apt, để /usr/bin/node tồn tại với mọi user và mọi unit file. Nếu bạn chỉ gõ lệnh trong phiên SSH, ví dụ chạy một agent CLI hoặc một bước build, thì nvm hoặc fnm gọn hơn và không cần sudo. Trộn lẫn hai nhóm này là nguồn gốc của lỗi phổ biến nhất trong bài: một unit chạy tốt khi bạn thử bằng tay, rồi chết ngay sau lần reboot đầu tiên.
Các lệnh tải phần mềm từ NodeSource, nvm và fnm trong bài là lệnh bạn tự chạy trên máy mình. Chúng lấy tệp từ host của bên thứ ba, nên bài này không mô tả output cụ thể của chúng. Sau mỗi bước cài, hãy tự xác minh bằng node -v và đối chiếu với dòng phiên bản bạn định cài.
Ubuntu đang cho bạn Node.js phiên bản nào?
Trước khi cài thêm bất cứ thứ gì, hãy xem kho của Ubuntu đang có sẵn bản nào.
apt policy nodejs
node -v 2>/dev/null || echo "chua co node"Trên Ubuntu 24.04 LTS (noble), gói nodejs là phiên bản 18.19.1+dfsg-6ubuntu5. Đó là dòng Node 18, và dòng này đã hết hạn hỗ trợ ở thượng nguồn từ tháng 4 năm 2025. Trên Ubuntu 26.04 LTS (resolute), gói nodejs là 22.22.1+dfsg+~cs22.19.15-1ubuntu1, tức dòng Node 22. Hai con số cách nhau bốn phiên bản major chỉ vì hai bản LTS cách nhau hai năm. Ubuntu đóng băng số major tại thời điểm phát hành và giữ nguyên nó suốt vòng đời bản phát hành đó.
Hậu tố +dfsg nghĩa là Debian đã gỡ một số tệp khỏi tarball gốc vì lý do giấy phép, thường là các bản build sẵn. Nó không có nghĩa là tính năng bị cắt. Điều quan trọng hơn: số phiên bản đứng yên không có nghĩa là gói đứng yên. Đội bảo mật Ubuntu backport bản vá vào chính số phiên bản cũ đó, nên nodejs 18.19.1 trên 24.04 vẫn nhận vá bảo mật. Nó chỉ không bao giờ nhận engine mới hay cú pháp mới.
Tính đến tháng 9 năm 2026, tình hình ở thượng nguồn như sau. Node 24 (Krypton) là Active LTS, chuyển sang chế độ maintenance ngày 20 tháng 10 năm 2026 và hết hạn ngày 30 tháng 4 năm 2028. Node 22 (Jod) đang ở maintenance LTS và hết hạn ngày 30 tháng 4 năm 2027. Node 26 là bản Current và dự kiến thành LTS ngày 28 tháng 10 năm 2026. Node 25 đã hết hạn từ tháng 6 năm 2026. Trên server, chỉ nên chạy các dòng major chẵn, vì chỉ chúng mới có giai đoạn LTS.
Nếu bạn đang cân nhắc nâng cả hệ điều hành thay vì chỉ nâng Node, những thay đổi trong Ubuntu 26.04 và thời điểm nên nâng cấp trả lời câu hỏi đó trực tiếp hơn, vì 26.04 mang sẵn Node 22 và bạn có thể không cần thêm kho nào cả.
Cài Node.js trên Ubuntu server: bốn con đường và tiêu chí chọn
Trên laptop, tiêu chí chọn là đổi phiên bản nhanh và không đau. Trên server thì khác hẳn. Năm câu hỏi sau quyết định, và cả năm đều không liên quan gì tới sự tiện tay:
- Ai chịu trách nhiệm nâng cấp, bạn hay apt?
- Bản cài có nằm trong phạm vi của
unattended-upgradeskhông? - Một unit systemd có tìm thấy binary không?
- Cài cho toàn máy dưới quyền root, hay chỉ cho một user?
- Nó tốn bao nhiêu RAM và đĩa trên một VPS 1 GB?
Bốn phần dưới đây trả lời năm câu hỏi này cho từng con đường.
Con đường 1: gói nodejs của Ubuntu
sudo apt update
sudo apt install -y nodejs npm
node -v
npm -vUbuntu tách npm thành một gói riêng, nên apt install nodejs một mình sẽ cho bạn node mà không có npm. Đây là khác biệt hay làm người mới bối rối, vì bản tải từ nodejs.org luôn đi kèm cả hai.
Ưu điểm rất thật. Binary nằm ở /usr/bin/node, thuộc root, và mọi user cùng mọi unit systemd đều thấy nó mà không cần cấu hình gì. Bản vá bảo mật đến qua đúng đường mà phần còn lại của hệ thống dùng, nên nếu unattended-upgrades đang hoạt động thì Node được vá mà bạn không phải nhớ gì. Nếu bạn chưa chắc dịch vụ đó có thật sự chạy trên máy mình, cách kiểm tra xem unattended-upgrades có thực sự chạy hay không chỉ mất vài phút và đáng làm trước khi tin vào nó.
Nhược điểm cũng rất thật. Trên 24.04 bạn bị kẹt ở Node 18 cho tới khi nâng cả hệ điều hành. Nhiều công cụ hiện nay khai báo trường engines yêu cầu Node 20 hoặc 22 trở lên, và npm sẽ in cảnh báo dạng npm warn EBADENGINE Unsupported engine. Cảnh báo đó không chặn việc cài, nên gói vẫn nằm trên đĩa và trông như đã cài xong. Nó hỏng muộn hơn, lúc chạy, thường là một SyntaxError ở cú pháp mà engine cũ chưa hiểu. Đây là kiểu lỗi tốn thời gian nhất vì thông báo không chỉ vào nguyên nhân thật.
Chọn con đường này khi bạn đang ở 26.04, hoặc khi phần mềm bạn chạy chấp nhận Node 18, hoặc khi bạn muốn số việc vận hành nhỏ nhất có thể.
Con đường 2: kho apt NodeSource
NodeSource phát hành kho apt riêng cho từng dòng major. Cách cấu hình thủ công dài hơn cách chạy script, nhưng nó bày ra đúng ba thứ bạn cần nhìn trước khi để apt tin một nguồn mới: khóa ký, URL, và tên suite.
sudo apt-get install -y ca-certificates curl gnupg
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpgNODE_MAJOR=24
printf '%s\n' \
'Types: deb' \
"URIs: https://deb.nodesource.com/node_${NODE_MAJOR}.x/" \
'Suites: nodistro' \
'Components: main' \
'Signed-By: /etc/apt/keyrings/nodesource.gpg' \
| sudo tee /etc/apt/sources.list.d/nodesource.sources
sudo apt-get update
sudo apt-get install -y nodejs
node -vDòng Suites: nodistro trông như lỗi đánh máy nhưng là đúng. NodeSource dùng một suite chung cho mọi bản Debian và Ubuntu thay vì noble hay resolute, nên đừng thay nó bằng codename của máy bạn. Nếu thay, apt update sẽ thất bại vì không tìm thấy tệp Release ở đường dẫn đó.
NodeSource cũng phát hành script cài sẵn, làm đúng các bước trên:
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt-get install -y nodejsScript này tải một tệp về rồi chạy nó bằng quyền root. Hãy mở nodesource_setup.sh ra đọc trước khi gõ sudo. Việc tải xuống trước rồi mới chạy, thay vì nối thẳng curl vào sudo bash, tồn tại chính là để bạn có cơ hội đọc.
Sau khi thêm kho này, có ba điều cần biết:
- Gói
nodejscủa NodeSource đã bao gồmnpmbên trong. Nếu trước đó bạn đã cài góinpmcủa Ubuntu, apt có thể báo xung đột. Gỡ góinpmcủa Ubuntu rồi cài lại là cách xử lý. - Kho này ghim theo major. Tệp
.sourcestrỏ vàonode_24.x, nên apt sẽ nâng bạn lên bản 24.x mới nhất và không bao giờ tự nhảy sang 26.x. Muốn đổi dòng, bạn sửa dòngURIsrồi chạy lạiapt update. unattended-upgradesmặc định chỉ xử lý các origin được liệt kê trongUnattended-Upgrade::Allowed-Origins, và origin của NodeSource không có sẵn ở đó. Nghĩa là vá Node từ kho này là việc của bạn, trừ khi bạn tự thêm origin vào cấu hình.
Còn một cái bẫy nữa. Nếu bạn từng chạy script cài của NodeSource rồi sau đó tự viết thêm tệp .sources, máy sẽ có hai nguồn cùng trỏ về một nơi và apt sẽ than phiền ở mỗi lần update. Bài lỗi nguồn apt bị trùng lặp ở định dạng deb822 mô tả đúng thông báo đó và cách dọn sạch.
Con đường 3: nvm
nvm quản lý nhiều phiên bản Node trong thư mục home của một user, không cần sudo. Phiên bản nvm mới nhất tính đến tháng 9 năm 2026 là v0.40.8, và URL cài luôn chứa số phiên bản đó.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bashTrình cài thêm hai dòng vào tệp profile của shell, thường là ~/.bashrc:
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"Sau đó mở shell mới rồi cài một dòng LTS:
source ~/.bashrc
nvm install 24
nvm alias default 24
command -v nvm
which nodecommand -v nvm in ra chữ nvm chứ không phải một đường dẫn tệp. Đó không phải lỗi: nvm là một hàm shell được nạp từ nvm.sh, không phải một tệp thực thi. Đây là sự thật quan trọng nhất về nvm khi bạn dùng nó trên server, và phần systemd bên dưới giải thích hậu quả.
Mỗi phiên bản Node là một bản sao đầy đủ nằm trong ~/.nvm/versions/node/. Đo bằng du -sh ~/.nvm để biết bạn đang giữ bao nhiêu. Ngoài ra, các gói cài toàn cục bằng npm install -g nằm bên trong thư mục của từng phiên bản, nên ngay sau khi bạn chạy nvm install 26, mọi CLI đã cài ở Node 24 biến mất khỏi PATH. Lệnh nvm reinstall-packages 24 cài lại chúng cho phiên bản đang dùng.
Con đường 4: fnm
fnm giải quyết cùng bài toán nhưng viết bằng Rust và là một tệp thực thi thật.
curl -fsSL https://fnm.vercel.app/install | bashSau đó thêm dòng khởi tạo vào ~/.bashrc, rồi cài một phiên bản:
eval "$(fnm env --use-on-cd --shell bash)"fnm install 24
fnm default 24
fnm listMặc định fnm đặt dữ liệu trong ~/.local/share/fnm. Nó khởi động nhanh hơn nvm vì không phải nạp một thư viện shell ở mỗi lần mở terminal, và cờ --use-on-cd tự đổi phiên bản khi bạn cd vào thư mục có tệp .node-version hoặc .nvmrc. Trên máy build hay máy CI, hai điểm đó là lý do chính để chọn fnm.
Nhưng cơ chế đặt PATH thì giống nvm ở đúng chỗ quan trọng nhất. fnm env in ra một loạt lệnh export mà shell của bạn phải eval thì chúng mới có hiệu lực. Hãy chạy fnm env một lần và đọc output của nó. Đường dẫn Node mà nó đưa vào PATH trỏ tới một thư mục tạm riêng cho từng shell, không phải một vị trí cố định trên đĩa. Một tiến trình không sinh ra từ shell đó sẽ không có gì trong tay.
Vì sao unit systemd báo không tìm thấy node?
systemd không đọc ~/.bashrc, không đọc ~/.profile, và không đọc bất kỳ tệp profile nào của shell. Nó khởi chạy tiến trình trực tiếp với một môi trường nhỏ do chính nó dựng. Bạn có thể xem đúng PATH mà một service nhận được:
sudo systemd-run --pipe --wait /usr/bin/printenv PATHKết quả là danh sách mặc định của systemd, đại khái /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin. Trong đó không có ~/.nvm, không có ~/.local/share/fnm, và không có thư mục tạm mà fnm tạo cho từng shell.
Hậu quả rất cụ thể. Một unit viết như thế này chạy được khi bạn thử bằng tay trong SSH, vì lúc đó PATH của bạn đã có Node:
[Service]
ExecStart=/usr/bin/env node /srv/app/server.js
User=deployDưới systemd nó hỏng, và journalctl -u app -n 30 --no-pager cho bạn hai dòng:
/usr/bin/env: 'node': No such file or directory
app.service: Main process exited, code=exited, status=127env không tìm thấy node bởi vì node chỉ tồn tại trong PATH do nvm.sh hoặc fnm env dựng lên, và ở đây không có ai nạp chúng.
Cách vá vội là ghi đường dẫn tuyệt đối vào ExecStart:
readlink -f "$(command -v node)"Nó chạy được, cho tới lần bạn nvm install một bản mới rồi gỡ bản cũ. Lúc đó đường dẫn kia không còn tồn tại, và systemd đổi sang một thông báo khác:
app.service: Failed to locate executable /home/deploy/.nvm/versions/node/v24.10.0/bin/node: No such file or directory
app.service: Main process exited, code=exited, status=203/EXECHai thông báo, một nguyên nhân gốc: phiên bản Node đang do một công cụ dành cho từng user quản lý, còn unit thì chạy hoàn toàn bên ngoài mọi shell của user đó. Vì vậy quy tắc rất gọn. Nếu có systemd trong sơ đồ, hãy cài Node ở mức hệ thống. /usr/bin/node không đổi chỗ khi bạn đổi phiên bản trong phiên SSH của mình.
Một unit tối thiểu chạy được trông như sau:
[Unit]
Description=App
After=network-online.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/srv/app
ExecStart=/usr/bin/node /srv/app/server.js
Restart=on-failure
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now app
systemctl is-active appsystemctl is-active app phải in active. Nếu nó in failed, hãy đọc journalctl -u app -n 30 --no-pager: dòng ngay trước Main process exited luôn là dòng nói lý do.
Vì sao không nên dùng sudo npm install -g?
Hai lý do, và không lý do nào mang tính lý thuyết.
Thứ nhất, npm chạy script vòng đời của gói, tức preinstall và postinstall, cho gói bạn cài và cho cả cây phụ thuộc của nó. Thêm sudo nghĩa là toàn bộ các script đó chạy bằng quyền root, từ mã nguồn bạn vừa tải về vài giây trước và chưa đọc. Đây là bề mặt tấn công mà các vụ tấn công chuỗi cung ứng npm trên server nhắm vào, nên đừng mở rộng nó vô cớ.
Thứ hai, nó để lại tệp thuộc root trong thư mục home của bạn, chủ yếu là cache ~/.npm. Lần sau khi bạn chạy npm mà không có sudo, npm không ghi được vào chính cache của mình và dừng với EACCES: permission denied. Người ta thường chữa bằng cách lại thêm sudo, và vòng lặp đó chỉ làm tình hình tệ hơn.
Cách thay thế là đổi thư mục cài toàn cục sang home, rồi không bao giờ cần sudo cho npm nữa:
npm config set prefix "$HOME/.local"
npm config get prefix
npm install -g <ten-goi>Trên Ubuntu, tệp ~/.profile mặc định đã tự thêm ~/.local/bin vào PATH nếu thư mục đó tồn tại lúc bạn đăng nhập. Nếu thư mục vừa được tạo trong phiên hiện tại, hãy đăng xuất rồi đăng nhập lại, sau đó kiểm tra bằng which <ten-lenh>. Với những công cụ chỉ dùng một lần, npx <ten-goi> chạy thẳng mà không cài gì cố định. Với công cụ thuộc về một dự án, hãy để nó trong devDependencies và gọi qua npm run. Phía Python có đúng câu chuyện này với một bộ công cụ khác, và so sánh venv, pipx và uv trên server áp dụng cùng một nguyên tắc: đừng cài phần mềm của người khác vào không gian của hệ điều hành.
Claude Code có bắt bạn chọn Node.js không?
Rất nhiều người tới câu hỏi này chỉ vì một lý do: chạy một agent CLI trên VPS. Với Claude Code, câu trả lời tính đến tháng 9 năm 2026 là không bắt buộc. Trình cài gốc không cần Node:
curl -fsSL https://claude.ai/install.sh | bashNó đặt binary vào ~/.local/bin/claude. Gói npm @anthropic-ai/claude-code vẫn tồn tại và yêu cầu Node 22 trở lên kể từ bản 2.1.198, nhưng gói đó chỉ tải về đúng binary gốc kia, và binary không dùng Node lúc chạy. Tài liệu chính thức cũng khuyến cáo không dùng sudo npm install -g cho gói này, vì đúng hai lý do ở phần trên.
Node vẫn cần cho phần còn lại của hệ sinh thái. Rất nhiều MCP server và công cụ dòng lệnh được phát hành dưới dạng gói npm và chạy qua npx, nên máy vẫn phải có một Node đủ mới. Nếu bạn chưa rõ agent này làm gì, Claude Code là gì và nó hoạt động ra sao giải thích phần đó, còn bộ công cụ dòng lệnh nên cài sẵn cho một agent trên VPS liệt kê những thứ đáng có bên cạnh. Nếu bạn định để agent chạy dài trong một phiên tách rời, chạy Claude Code trên VPS bên trong tmux là cách đơn giản nhất. Nó cũng là ví dụ ngược cho quy tắc systemd ở trên: tmux giữ một shell thật, nên PATH do nvm hay fnm dựng vẫn còn nguyên trong đó.
Trên VPS 1 GB thì chọn gì?
RAM là giới hạn thật, và nó cắn ở bước cài chứ không phải bước chạy. Khi npm install gặp một gói native, node-gyp sẽ biên dịch mã C++ ngay trên máy, và trình biên dịch có thể bị OOM killer giết trước khi xong. Bằng chứng nằm ở đây:
free -h
sudo dmesg -T | grep -i "killed process"Nếu bạn thấy một dòng như Out of memory: Killed process ... (cc1plus), thứ bị giết là trình biên dịch, không phải npm. npm chỉ báo lại một lỗi build khó hiểu ở phía trên. Cách xử lý là thêm swap, hoặc build ở một máy lớn hơn rồi chép node_modules sang.
Đĩa thì âm thầm hơn. Cache npm và các phiên bản Node do nvm giữ lớn dần theo tháng:
du -sh ~/.npm ~/.nvm 2>/dev/null
npm cache clean --forceTrên một VPS 1 GB, cấu hình ít phiền nhất là một phiên bản Node duy nhất cài bằng apt, cộng với việc dọn cache npm định kỳ. Giữ ba phiên bản Node song song trên một máy chỉ để chạy một dịch vụ là chi phí không đổi lấy gì.
Chọn đường nào
- Có dịch vụ systemd: dùng gói của Ubuntu nếu phiên bản đủ mới, dùng NodeSource nếu không.
- Chỉ dùng tương tác trong SSH và cần nhiều phiên bản: fnm, hoặc nvm nếu bạn đã quen tay.
- Máy build hoặc CI: fnm, vì shell khởi động nhanh và nó đọc
.node-versioncủa từng dự án. - Cần một CLI toàn cục: đặt
prefixcủa npm trong home, không dùng sudo.
Không có luật nào cấm bạn dùng cả hai nhóm trên cùng một máy. Điều cần làm là luôn biết cái nào đang trả lời. which -a node liệt kê mọi node trong PATH hiện tại theo thứ tự ưu tiên, và readlink -f "$(command -v node)" cho bạn đường dẫn thật sau khi đã đi hết các symlink. Nếu kết quả không phải /usr/bin/node, thì unit systemd của bạn đang chạy một Node khác với Node bạn vừa thử trong terminal.
FAQ
Ubuntu 24.04 cài apt install nodejs ra Node 18, có dùng được không?
Dùng được nếu phần mềm của bạn chấp nhận Node 18. Vấn đề là dòng Node 18 đã hết hạn hỗ trợ ở thượng nguồn từ tháng 4 năm 2025, nên nhiều gói npm hiện nay khai báo engines yêu cầu Node 20 hoặc 22 trở lên. Khi đó npm in npm warn EBADENGINE Unsupported engine nhưng vẫn cài, và lỗi thật chỉ xuất hiện lúc chạy. Nếu bạn gặp cảnh báo này, hãy chuyển sang kho NodeSource với NODE_MAJOR=24 thay vì bỏ qua nó.
Tại sao unit systemd của tôi báo lỗi khi tôi dùng nvm hoặc fnm?
Vì systemd không đọc ~/.bashrc hay bất kỳ tệp profile nào, mà cả nvm lẫn fnm đều đặt PATH từ đó. Với ExecStart=/usr/bin/env node ..., bạn nhận /usr/bin/env: 'node': No such file or directory và status=127. Nếu bạn ghi đường dẫn tuyệt đối vào nvm rồi sau đó gỡ phiên bản cũ, bạn nhận Failed to locate executable ... và status=203/EXEC. Chạy sudo systemd-run --pipe --wait /usr/bin/printenv PATH để thấy PATH thật của môi trường service. Cách sửa đúng là cài Node ở mức hệ thống và trỏ ExecStart vào /usr/bin/node.
Có cần gỡ gói nodejs của Ubuntu trước khi thêm kho NodeSource không?
Gói nodejs thì apt tự thay thế được, vì hai bên cùng tên gói và NodeSource có số phiên bản cao hơn. Vướng mắc thường nằm ở gói npm riêng của Ubuntu, bởi gói nodejs của NodeSource đã chứa sẵn npm bên trong. Nếu apt install nodejs báo xung đột, hãy gỡ gói npm của Ubuntu rồi cài lại. Sau khi xong, kiểm tra bằng apt policy nodejs để chắc chắn phiên bản đang cài đến từ deb.nodesource.com.
nvm hay fnm tốt hơn trên server?
Trên server có dịch vụ chạy nền thì cả hai đều không phải lựa chọn đúng, vì lý do PATH ở trên. Trong phạm vi dùng tương tác, fnm nhanh hơn khi mở shell vì nó là binary Rust thay vì một thư viện shell phải nạp lại mỗi lần, và nó tự đổi phiên bản theo .node-version khi bạn cd. nvm bù lại bằng việc phổ biến hơn, nên hướng dẫn và câu trả lời có sẵn nhiều hơn. Nếu bạn bắt đầu từ đầu trên một máy build, hãy chọn fnm.
Làm sao cài gói npm toàn cục mà không cần sudo?
Đặt thư mục cài toàn cục vào home bằng npm config set prefix "$HOME/.local", rồi chạy npm install -g như bình thường. Trên Ubuntu, ~/.profile mặc định đã thêm ~/.local/bin vào PATH nếu thư mục đó tồn tại lúc đăng nhập, nên hãy đăng nhập lại rồi xác minh bằng which <ten-lenh>. Nếu bạn đã lỡ chạy sudo npm install -g trước đó, cache ~/.npm có thể đang thuộc root và npm sẽ báo EACCES: permission denied; trả quyền sở hữu cache về cho user của bạn trước khi làm tiếp.