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 portsetsebool -P httpd_can_network_connect 1— allows the app to make outbound connectionsfirewall-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.

