When you deploy a Node.js (or any web) application on a Linux server, the biggest risk is not your code — it’s server misconfiguration.
This tutorial shows how to turn a fresh Ubuntu server into a secure, production-ready environment, where:
- Root login is disabled
- SSH is key-only
- Each app runs as its own locked-down user
- Even if your app is hacked, the system stays safe
This guide is beginner-friendly and does not require Docker.
🧠 Security Philosophy (Simple Explanation)
We follow three basic rules:
- Never run apps as root
- Each app gets its own user
- The app can only access its own files
If an attacker breaks into your app, they should not be able to:
- install crypto miners
- modify system files
- add users
- affect other apps
Assumptions
- Ubuntu 22.04 / 24.04
- You can SSH into the server
- You start as
root(fresh cloud server)
Step 1: Create a Non-Root Admin User
First, create a normal user for yourself (example: dev).
adduser dev
Give it a strong password.
Then allow this user to administer the system without logging in as root:
usermod -aG sudo dev
Why?
- Root SSH access is dangerous
- A normal user + sudo is safer and auditable
Step 2: Set Up SSH Key Login
On your local machine, generate an SSH key (if you don’t have one):
ssh-keygen -t ed25519
Copy the public key to the server:
mkdir -p /home/dev/.ssh
nano /home/dev/.ssh/authorized_keys
Paste your public key.
Fix permissions:
chown -R dev:dev /home/dev/.ssh
chmod 700 /home/dev/.ssh
chmod 600 /home/dev/.ssh/authorized_keys
Now test from your computer:
ssh dev@YOUR_SERVER_IP
If this works, you’re safe to continue.
Step 3: Lock Down SSH (Very Important)
Edit SSH config:
sudo nano /etc/ssh/sshd_config
Ensure these lines exist and are not commented:
PermitRootLogin no
PubkeyAuthentication yes
At the bottom of the file, add:
Match all
PasswordAuthentication no
Reload SSH safely:
sudo systemctl reload ssh
What this does
- Root SSH login ❌ disabled
- Password login ❌ disabled
- SSH keys ✅ required
💡 Cloud recovery consoles still work — you won’t get locked out.
Step 4: Enable the Firewall
Allow required ports before enabling:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
Enable firewall:
sudo ufw enable
Check status:
sudo ufw status verbose
Why?
- Blocks random internet scans
- Only allows what you explicitly need
Step 5: Install Node.js (System-Wide)
Install Node.js using the official repository (example: Node 24 LTS):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
Verify:
node -v
which node
Expected:
/usr/bin/node- owned by
root
Why not nvm?
- System services (systemd) don’t work well with nvm
- Root-owned Node is safer and predictable
Step 6: Create a Dedicated App User
Never run apps as your admin user.
Create a service user (example: svc-nextjs):
sudo adduser \
--system \
--no-create-home \
--group \
--shell /usr/sbin/nologin \
svc-nextjs
What this means
- No SSH access
- No shell
- No sudo
- Only exists to run the app
Step 7: Isolate App Files
Create a directory for your app:
sudo mkdir -p /var/apps/nextjs
sudo chown -R svc-nextjs:svc-nextjs /var/apps/nextjs
sudo chmod 750 /var/apps/nextjs
Test as admin:
cd /var/apps/nextjs
This should fail ❌ — that’s correct.
Test as service user:
sudo -u svc-nextjs ls /var/apps/nextjs
This should work ✅.
Step 8: Run the App with systemd (Hardened)
Create a service file:
sudo nano /etc/systemd/system/nextjs.service
Paste:
[Unit]
Description=Next.js Application (Hardened)
After=network.target
[Service]
Type=simple
User=svc-nextjs
Group=svc-nextjs
WorkingDirectory=/var/apps/nextjs
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/apps/nextjs
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
CapabilityBoundingSet=
AmbientCapabilities=
UMask=0077
[Install]
WantedBy=multi-user.target
Reload systemd:
sudo systemctl daemon-reload
Step 9: Check Security Score
Run:
systemd-analyze security nextjs.service
A score around 2–3 is excellent for a web service.
What this protects against
- Privilege escalation
- System file modification
- Kernel abuse
- Crypto miners
- Lateral movement
Even if the app is hacked, the OS remains safe.
Step 10: Final Sanity Checks
sudo sshd -T | egrep 'permitrootlogin|passwordauthentication|pubkeyauthentication'
Expected:
permitrootlogin no
passwordauthentication no
pubkeyauthentication yes
Check firewall:
sudo ufw status
What You Achieved
✅ Root SSH disabled
✅ Password login disabled
✅ Key-only access
✅ Firewall enabled
✅ Per-app service users
✅ Filesystem isolation
✅ Strong systemd sandbox
This is real production security, not just theory.
Final Advice
- One app = one service user
- Never run apps as root
- Let systemd enforce security
- Keep SSH boring and locked down
You now have a secure Linux foundation for Node.js, Python, or any backend service.
Happy shipping 🚀
And welcome to proper server hardening.
Top comments (0)