pm2-selinux

PM2 Startup Keeps Failing on Oracle Cloud Free Tier? It’s SELinux — Here’s the Fix

The silent permission killer that breaks your Node.js auto-restart on reboot

📹 Watch the Video Tutorial

Video coming soon — subscribe to TechScriptAid on YouTube to get notified!

🎯 The Problem

You’ve deployed your Node.js app on Oracle Cloud’s free tier. PM2 is running perfectly. You set up pm2 startup, pm2 save, everything looks green. Then the server reboots.

Your app is dead. PM2 never started.

You check systemctl status pm2-<user> and see:

BASH

Active: failed (Result: exit-code)

You dig into the journal logs:

BASH

pm2-opc.service: Failed to locate executable /home/opc/.nvm/versions/node/v20.20.0/lib/node_modules/pm2/bin/pm2: Permission denied
pm2-opc.service: Failed at step EXEC spawning ...pm2: Permission denied

Permission denied. But the file is rwxr-xr-x. You own it. You can run it manually. What’s going on?

The answer: SELinux.

🔍 Why This Happens

Oracle Cloud instances (Oracle Linux 8/9) run SELinux in Enforcing mode by default:

BASH

$ getenforce
Enforcing

When systemd starts your PM2 service, it tries to execute the PM2 binary. But that binary lives deep inside your home directory under ~/.nvm/. SELinux has strict policies about what systemd can execute — and files inside user home directories under NVM paths don’t have the right SELinux context.

The standard pm2 startup command generates a service file like this:

BASH

ExecStart=/home/opc/.nvm/versions/node/v20.20.0/lib/node_modules/pm2/bin/pm2 resurrect

SELinux blocks it. Every time. The file permissions are fine — it’s the SELinux security context that’s wrong.

⚠️ The Common “Fix” That’s Actually Bad

Many Stack Overflow answers suggest:

BASH

sudo setenforce 0  # DON'T DO THIS

This disables SELinux entirely. On a cloud server exposed to the internet, that’s a terrible idea. You lose a critical security layer.

✅ The Real Fix: Wrapper Scripts

Instead of fighting SELinux, work with it. Files in /usr/local/bin/ have the correct SELinux context for systemd execution. So we create wrapper scripts there.

Step 1: Create the Start Script

BASH

sudo bash -c 'cat > /usr/local/bin/pm2-start.sh << "EOF"
#!/bin/bash
export HOME=/home/opc
export PM2_HOME=/home/opc/.pm2
export NVM_DIR=/home/opc/.nvm
export PATH=/home/opc/.nvm/versions/node/v20.20.0/bin:$PATH

# IMPORTANT: cd to your app directory so dotenv finds .env
cd /home/opc/your-app-directory

/home/opc/.nvm/versions/node/v20.20.0/bin/node \
  /home/opc/.nvm/versions/node/v20.20.0/lib/node_modules/pm2/bin/pm2 \
  start server.js --name your-app --max-memory-restart 200M
EOF
chmod +x /usr/local/bin/pm2-start.sh'

Step 2: Create the Stop Script

BASH

sudo bash -c 'cat > /usr/local/bin/pm2-stop.sh << "EOF"
#!/bin/bash
export HOME=/home/opc
export PM2_HOME=/home/opc/.pm2
export PATH=/home/opc/.nvm/versions/node/v20.20.0/bin:$PATH

/home/opc/.nvm/versions/node/v20.20.0/bin/node \
  /home/opc/.nvm/versions/node/v20.20.0/lib/node_modules/pm2/bin/pm2 kill
EOF
chmod +x /usr/local/bin/pm2-stop.sh'

Step 3: Create the Reload Script

BASH

sudo bash -c 'cat > /usr/local/bin/pm2-reload.sh << "EOF"
#!/bin/bash
export HOME=/home/opc
export PM2_HOME=/home/opc/.pm2
export PATH=/home/opc/.nvm/versions/node/v20.20.0/bin:$PATH

/home/opc/.nvm/versions/node/v20.20.0/bin/node \
  /home/opc/.nvm/versions/node/v20.20.0/lib/node_modules/pm2/bin/pm2 reload all
EOF
chmod +x /usr/local/bin/pm2-reload.sh'

Step 4: Replace the Systemd Service

BASH

sudo bash -c 'cat > /etc/systemd/system/pm2-opc.service << "EOF"
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
User=opc
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=HOME=/home/opc
Environment=PM2_HOME=/home/opc/.pm2
Environment=PATH=/home/opc/.nvm/versions/node/v20.20.0/bin:/usr/local/bin:/usr/bin:/bin
Restart=on-failure
RestartSec=10

ExecStart=/usr/local/bin/pm2-start.sh
ExecReload=/usr/local/bin/pm2-reload.sh
ExecStop=/usr/local/bin/pm2-stop.sh

[Install]
WantedBy=multi-user.target
EOF'

sudo systemctl daemon-reload
sudo systemctl enable pm2-opc

Step 5: Test It

BASH

# Stop everything
sudo systemctl stop pm2-opc
sleep 2

# Start via systemd (simulates reboot)
sudo systemctl start pm2-opc
sleep 3

# Verify
systemctl status pm2-opc --no-pager
pm2 status
curl -s http://localhost:3000/health

You should see Active: active (exited) and your app running in PM2.

🐛 Two Gotchas That Will Bite You

Gotcha 1: Type=forking vs Type=oneshot

The default PM2 startup uses Type=forking with a PIDFile. SELinux also blocks reading PID files from user home directories. You'll see:

BASH

Can't convert PID files /home/opc/.pm2/pm2.pid O_PATH file descriptor to proper file descriptor: Permission denied

Using Type=oneshot with RemainAfterExit=yes avoids this entirely. The service starts PM2, PM2 daemonizes itself, and systemd just tracks that the start command succeeded.

Gotcha 2: pm2 resurrect Loses Your Working Directory

The default startup uses pm2 resurrect which restores processes from a dump file. But it doesn't preserve the working directory (cwd). If your app uses dotenv (or any relative file path), it won't find your .env file.

Your app will start, look healthy, but silently connect to localhost instead of your actual database. You'll get ECONNREFUSED errors and wonder why.

The fix is in the start script above: we cd into the app directory and use pm2 start instead of pm2 resurrect.

✅ Quick Verification Checklist

BASH

# 1. SELinux is still enforcing (good!)
getenforce
# Should output: Enforcing

# 2. Service is enabled for boot
systemctl is-enabled pm2-opc
# Should output: enabled

# 3. Service is currently active
systemctl is-active pm2-opc
# Should output: active

# 4. App is running
pm2 status
# Should show your app as "online"

# 5. Full reboot test (when you're ready)
sudo reboot
# After reconnecting:
pm2 status  # App should be running

💡 Why This Matters

Oracle Cloud's free tier is arguably the best free hosting available — you get a full ARM instance with up to 24GB RAM. But Oracle Linux's SELinux defaults trip up almost everyone deploying Node.js apps with NVM + PM2.

The typical journey is: app works manually, pm2 startup generates the service, reboot kills everything, hours of debugging. This article exists so that journey takes 5 minutes instead.

Adjust the username (opc), Node version, and app paths to match your setup, and you're good to go.

🎓 Conclusion

SELinux on Oracle Cloud isn't your enemy — it's doing its job protecting your server. The problem is that pm2 startup wasn't designed with SELinux-enforced environments in mind. By creating wrapper scripts in /usr/local/bin/ (which has the correct SELinux context) and using Type=oneshot in systemd, you get reliable auto-restart on reboot without compromising security.

The two "gotchas" — PID file permission denial and the silent resurrect + dotenv failure — are what make this problem especially frustrating. Now you know both, and you can deploy with confidence.