Installing Node.js and PM2 on Fedora Server: Deploying JavaScript Applications in Production with SELinux and firewalld

Fedora tutorial - IT technology blog
Fedora tutorial - IT technology blog

When Deploying Node.js on Fedora, SELinux Greets You First

I’ve been using Fedora as my main development machine for 2 years and I’m pretty happy with its fast package update cycle. But the first time I deployed a Node.js app to a Fedora Server in a production environment, the first thing I ran into wasn’t Node.js or PM2 — it was SELinux. The app ran fine on an Ubuntu test environment beforehand, but on Fedora, curl http://server-ip:3000 timed out completely, logs were blank, and there were no clear error messages anywhere.

If you’re setting up a Fedora Server for production, don’t disable SELinux — even though many “quick fixes” on StackOverflow suggest exactly that. This article walks through each step: installing Node.js, PM2, and then handling both SELinux and firewalld the right way. The SELinux part is the most commonly skipped because most tutorials run demos on Ubuntu/Debian — where SELinux isn’t enabled by default.

Concepts to Understand Before You Start

PM2 — Why Not Just Run node app.js Directly?

node app.js works, but it’s not a production setup. PM2 is a process manager built specifically for Node.js, and it solves 3 real-world problems:

  • Auto-restart: If the app crashes, it restarts automatically — no need to watch the screen at 3 AM
  • Cluster mode: On a 4-core server, PM2 runs 4 instances in parallel — throughput increases ~3.5x compared to a single process because Node.js is single-threaded
  • Log management: Centralized logging with automatic rotation, so logs never fill up the disk

SELinux Context and Port Labeling

SELinux assigns a label (context) to every file, process, and port in the system. Port 3000 is not on the default allowed list — the kernel blocks all incoming connections even if your app is actively listening on that address. No errors are thrown; requests just silently time out. You need to add the http_port_t label to your application’s port.

firewalld Zones

Fedora uses firewalld instead of raw iptables. To open a port, you add it to the public zone or whichever zone your interface belongs to, and you must use the --permanent flag along with --reload so the change survives a reboot.

Detailed Step-by-Step Walkthrough

Step 1: Install Node.js

Fedora includes Node.js in its default repository, but it’s usually an older version. Use the NodeSource repository to get the latest LTS release:

# Add NodeSource repo for Node.js 20 LTS
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -

sudo dnf install -y nodejs

# Verify
node --version   # v20.x.x
npm --version

Or use dnf module if you prefer to stay within the Fedora ecosystem:

# List available Node.js streams
sudo dnf module list nodejs

# Enable stream 20 and install
sudo dnf module enable nodejs:20 -y
sudo dnf install nodejs -y

Step 2: Install PM2

sudo npm install -g pm2
pm2 --version

Step 3: Deploy a Sample Application

Create a simple app to test the entire flow:

sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
// /var/www/myapp/app.js
const http = require('http');
const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello from Fedora Server!\n');
});

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
cd /var/www/myapp
pm2 start app.js --name "myapp"
pm2 status

PM2 reports online, but curl http://server-ip:3000 from outside still times out — this is where you need to handle SELinux and firewalld.

Step 4: Handle SELinux

Check whether SELinux is blocking connections:

sudo ausearch -m avc -ts recent | grep node
# Or
sudo tail -50 /var/log/audit/audit.log | grep denied

Seeing a denied line points you in the right direction — at least you know the cause instead of guessing blindly. Fix it like this:

# Install policycoreutils if not already present
sudo dnf install -y policycoreutils-python-utils

# View which ports are currently allowed for HTTP
sudo semanage port -l | grep http_port_t

# Add port 3000 to http_port_t
sudo semanage port -a -t http_port_t -p tcp 3000

# Verify
sudo semanage port -l | grep 3000

If your app needs to write files (uploads, cache, temp files, etc.), those directories need an SELinux context that permits writes:

# Set write context for the uploads directory
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/myapp/uploads(/.*)?" 
sudo restorecon -Rv /var/www/myapp/uploads/

Does your app need to call an external API or connect to a database on another host? This boolean is mandatory:

# Allow Node.js to make outbound network connections
sudo setsebool -P httpd_can_network_connect 1

The -P flag makes this persist across reboots. Without it, any attempt to connect to MongoDB or call a third-party API will fail silently.

Step 5: Open Ports with firewalld

# Check which zone is currently active
sudo firewall-cmd --get-active-zones

# Open port 3000
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-ports

If you’re placing Nginx as a reverse proxy in front (recommended for production), only open 80/443 and keep the app port internal:

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Step 6: Configure PM2 to Start with systemd

This step is easy to forget — when the server reboots, the app must restart on its own without you having to SSH in and start it manually:

# Generate startup script
pm2 startup systemd

# The command above prints a sudo command — copy and run it, for example:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u myuser --hp /home/myuser

# Save the current process list
pm2 save

# Check the service
sudo systemctl status pm2-myuser

Step 7: Ecosystem File for Production Configuration

Instead of remembering long flag strings, use an ecosystem file to version-control your configuration alongside your code:

// /var/www/myapp/ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 'max',         // Use all available CPU cores
    exec_mode: 'cluster',     // Cluster mode for high availability
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss'
  }]
};
# Start with the ecosystem file
pm2 start ecosystem.config.js

# Zero-downtime reload (no in-flight requests are dropped)
pm2 reload myapp

# Real-time monitoring
pm2 monit

Conclusion

Installing Node.js and PM2 goes quickly. What actually takes time is SELinux and firewalld — two things that almost no Ubuntu/Debian tutorial mentions. Missing any one of the three commands below will leave your app running fine locally but refusing external connections, or worse: HTTP works but database connections fail:

  • semanage port -a -t http_port_t -p tcp <port> — grants SELinux permission to accept incoming connections on that port
  • setsebool -P httpd_can_network_connect 1 — allows the app to make outbound connections
  • firewall-cmd --permanent --add-port=<port>/tcp && firewall-cmd --reload — opens the port in the firewall

With PM2, pm2 startup and pm2 save are steps you cannot skip — if the app doesn’t restart automatically after a reboot, you won’t find out until the next morning. Put Nginx as a reverse proxy in front to keep the app port internal and avoid exposing it directly to the internet.

Share: