Setting up an Express.js server on Ubuntu is simple and efficient. Here’s what you’ll learn in this guide:
- Install Node.js and npm: Use the latest NodeSource repository to ensure compatibility.
- Set up an Express.js server: Create a basic server with routing and middleware.
- Deploy with PM2 and Nginx: Ensure a stable production environment.
- Secure with HTTPS: Use Let’s Encrypt for SSL certificates.
This step-by-step guide will help you create a secure, production-ready server for APIs, real-time apps, or single-page applications. Whether you’re a beginner or experienced, this process ensures a reliable setup.
How to Deploy a Node.js App to Your Ubuntu VPS
System Requirements and Setup
Make sure your Ubuntu system is ready with the necessary tools and configurations to avoid any setup problems and ensure smooth performance.
Required Software Setup
You’ll need to install a few key components on your Ubuntu system:
- Essential tools:
build-essential
,curl
,git
,nano
orvim
,PM2
, andNginx
Start by updating your system and installing the required packages:
sudo apt update sudo apt upgrade -y sudo apt install build-essential curl git -y
VPS Setup Guide
Choose a VPS plan that aligns with the resource demands of your application. Here’s a quick comparison of options:
Resource Requirements | Recommended VPS Plan | Monthly Cost | Best For |
---|---|---|---|
Entry Level | KVM1-US (1GB RAM) | $10 | Development, Testing |
Small Production | KVM2-US (2GB RAM) | $20 | Small Applications |
Medium Production | KVM4-US (4GB RAM) | $40 | Medium Traffic Sites |
High Performance | KVM8-US (8GB RAM) | $80 | High Traffic Applications |
Standard Features:
- Network Speed: 1 Gbps port
- Traffic: Unmetered bandwidth
- Storage: NVMe SSD (20GB to 80GB)
- Control: Full root access
- Support: 24/7 technical help
For most Express.js applications, the KVM2-US plan ($20/month) is a solid choice. It offers 2 vCores, 2GB RAM, and 25GB of fast NVMe storage. This setup works well for development and small to medium production environments, delivering reliable performance.
Tips for Securing Your VPS:
- Set up SSH key authentication
- Configure UFW firewall rules
- Regularly update packages
- Create a non-root deployment user for added security
Once your system and VPS are ready, you can move on to installing Node.js and npm in the next steps.
Node.js and npm Installation
To run Express.js applications on Ubuntu, you’ll need to set up Node.js and npm. Using the NodeSource repository ensures you’re working with the latest stable version.
Setting Up the Node.js Repository
Ubuntu’s default repository often includes outdated Node.js versions that may lack important security updates [2]. To get a newer version, you’ll use the NodeSource repository, which offers official builds for Debian-based systems.
Start by downloading and running the NodeSource setup script:
curl -sL https://deb.nodesource.com/setup_18.x -o /tmp/nodesource_setup.sh sudo bash /tmp/nodesource_setup.sh
Installing Node.js and npm
Follow these steps to install and verify Node.js and npm:
Step | Command | Purpose |
---|---|---|
Install Node.js | sudo apt install nodejs | Installs both Node.js and npm |
Verify Node.js | node -v | Confirms the installed Node.js version |
Verify npm | npm -v | Confirms the installed npm version |
Update npm | sudo npm install -g npm@latest | Updates npm to the latest version |
When working with Express.js, make sure you’re using a compatible Node.js version:
- Express 4.x needs Node.js 0.10 or higher
- Express 5.x requires Node.js 18 or higher [1]
Once Node.js and npm are set up, you’re ready to start configuring your Express.js server.
Express.js Server Setup

Once you’ve installed Node.js and npm, you can start building your Express.js application.
Project Setup
Begin by creating a new project directory and initializing it:
mkdir express-ubuntu-server cd express-ubuntu-server npm init -y
Installing Express.js
Install Express.js locally using npm:
npm install express
Setting Up a Basic Server
Now, create a file named app.js
and include the following code:
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); });
To test your server:
- Run the server with:
node app.js
- Open your browser and go to http://localhost:3000 to see the message “Hello World!”
- Stop the server anytime by pressing
Ctrl+C
Setting Up for Development Testing
For testing purposes, install Jest, SuperTest, and cross-env:
npm install --save-dev jest supertest cross-env
Update your package.json
file to include the following test script:
{ "scripts": { "test": "cross-env NODE_ENV=test jest --testTimeout=5000" } }
Ignoring Node Modules
To prevent committing unnecessary files, create a .gitignore
file with the following content:
echo "node_modules/" > .gitignore
With your basic setup complete, your server is ready for additional configurations in the next steps.
sbb-itb-0ad7fa2
Server Features and Settings
Once your basic Express.js server is set up, the next step is to add features and configurations to prepare it for production.
API Routes Setup
Define your API routes to handle different functionalities. Here’s an example:
const express = require('express'); const router = express.Router(); // User routes router.get('/users', (req, res) => { res.json({ message: 'Get all users' }); }); router.get('/users/:userId', (req, res) => { const userId = req.params.userId; res.json({ message: `Get user with ID: ${userId}` }); }); // Apply routes to main app app.use('/api', router);
For better organization, create a routes
directory and separate files based on features:
/express-ubuntu-server /routes users.js products.js orders.js
This structure makes managing and scaling your app much easier.
Static File Management
To efficiently serve static files like images, CSS, and JavaScript, use the express.static
middleware:
const path = require('path'); // Serve files from multiple directories app.use(express.static(path.join(__dirname, 'public'))); app.use('/assets', express.static(path.join(__dirname, 'assets')));
Organize your static files in a well-structured directory:
/express-ubuntu-server /public /images /css /js /assets /uploads /documents
This setup ensures your server can easily locate and serve static resources.
Middleware Setup
Middleware plays a key role in request handling and boosting security. Here’s how to set up some essential middleware:
// Request parsing app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Custom logger middleware app.use((req, res, next) => { console.log(`${new Date().toLocaleString('en-US')} - ${req.method} ${req.path}`); next(); }); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!', timestamp: new Date().toLocaleString('en-US') }); });
Here’s a quick overview of some commonly used middleware:
Middleware | Purpose | Implementation |
---|---|---|
express.json() | Parse JSON payloads | app.use(express.json()) |
express.urlencoded() | Parse URL-encoded bodies | app.use(express.urlencoded({ extended: true })) |
cors | Enable Cross-Origin Resource Sharing | app.use(cors()) |
helmet | Add security headers | app.use(helmet()) |
compression | Compress response bodies | app.use(compression()) |
To use third-party middleware like cors
, helmet
, and compression
, install them via npm:
npm install cors helmet compression
“Middleware functions are functions that have access to the request object (
req
), the response object (res
), and the next middleware function in the application’s request-response cycle.” [3]
When implementing middleware, remember to handle errors properly and log important details. The order of middleware matters: start with general middleware, then add route handlers, and finish with error-handling middleware. This ensures a smooth and secure production environment.
Server Deployment Steps
This section outlines how to deploy a dependable Ubuntu setup for your application.
Local Testing
Before deployment, ensure your Express.js app works as expected by testing it locally with Jest and SuperTest:
const request = require('supertest'); const app = require('../app'); describe('API Endpoints', () => { test('GET /api/products returns 200', async () => { const response = await request(app) .get('/api/products') .expect('Content-Type', /json/) .expect(200); expect(response.body).toHaveProperty('products'); }); });
Add test scripts to your package.json
file:
{ "scripts": { "test": "NODE_ENV=test jest --detectOpenHandles", "test:watch": "NODE_ENV=test jest --watch" } }
Once your local tests confirm everything is working, move on to managing your app with PM2.
PM2 Process Management

PM2 is a tool that helps manage your Express.js app in production.
Install PM2 globally:
sudo npm install pm2@latest -g
Set up a configuration file, ecosystem.config.js
, in your project directory:
module.exports = { apps: [{ name: "express-app", script: "./app.js", instances: "max", exec_mode: "cluster", env: { NODE_ENV: "production", PORT: 3000 }, error_file: "./logs/err.log", out_file: "./logs/out.log" }] }
Launch and manage your app with the following commands:
pm2 start ecosystem.config.js pm2 startup systemd pm2 save
Here are some useful PM2 commands:
PM2 Command | Purpose |
---|---|
pm2 list | View running applications |
pm2 monit | Monitor CPU and memory usage |
pm2 logs | View application logs |
pm2 reload all | Reload apps with zero downtime |
These commands help keep your app running smoothly and allow it to scale as needed.
Nginx Setup
To set up Nginx as a reverse proxy for your application:
- Install Nginx:
sudo apt update sudo apt install nginx
- Create a configuration file at
/etc/nginx/sites-available/express-app
:
server { listen 80; server_name your-domain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }
- Enable the configuration and restart Nginx:
sudo ln -s /etc/nginx/sites-available/express-app /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx
“PM2 makes it possible to daemonize applications so that they will run in the background as a service.” [4]
Common Problems and Solutions
When setting up an Express.js server on Ubuntu, certain issues can pop up. Here’s how to tackle them head-on.
Permission Issues
Permission errors often arise during package installation or while accessing directories. Here’s a quick fix:
mkdir ~/.npm-global npm config set prefix '~/.npm-global' echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.profile source ~/.profile
For system-level tasks, use sudo
only when necessary:
Task | Command Example | When to Use |
---|---|---|
System Updates | sudo apt update | Managing system packages |
Software Installation | sudo apt install nodejs | Adding new software |
Global npm Packages | Use ~/.npm-global | Avoid using sudo with npm |
Port Conflicts
The “EADDRINUSE” error happens when multiple applications try to use the same port. Here’s how to resolve it:
- Find processes using the port:
sudo netstat -tulpn | grep LISTEN
- End the conflicting process:
sudo kill -9 $(sudo lsof -t -i:3000)
To avoid conflicts in production, set an alternate port in your code:
const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server running on port ${port}`); });
Server Start Issues
If your server refuses to start, try these steps:
- Allow Node.js to use lower ports:
sudo apt-get install libcap2-bin sudo setcap cap_net_bind_service=+ep `readlink -f \`which node\``
- Add error handling to your app:
process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); app.listen(port, (error) => { if (error) { console.error('Failed to start server:', error); process.exit(1); } });
For better stability and security, you can:
- Use
authbind
to let non-root users access lower ports. - Set up robust error logging and monitoring.
- Configure automatic restarts for your app in case of failure.
Taking these steps will make your deployment process much smoother.
Building a Reliable Express.js Server on Ubuntu
Setting up an Express.js server on Ubuntu requires attention to both development and operational details. To keep your server running smoothly, it’s crucial to have solid monitoring and maintenance tools in place.
Here are some tools you should consider:
Tool | Purpose | What It Does |
---|---|---|
PM2 | Process Management | Tracks hardware metrics and logs exceptions |
Express Status Monitor | Real-Time Metrics | Adds a /status endpoint with visual data using Socket.io |
New Relic | Performance Monitoring | Delivers pre-configured tools for tracking server performance |
Clinic.js | Diagnostics | Pinpoints and fixes performance issues |
By integrating these tools, you can maintain a stable and efficient server environment. But tools alone aren’t enough – best practices are just as essential.
Development Tips
- Use
DEBUG=express:* node index.js
to enable debugging. - Set up structured logging for better traceability.
- Add error-tracking services like Sentry.io or AppSignal to catch issues early.
Operational Tips
- Stick to a regular maintenance schedule.
- Centralize your logs for easier monitoring and troubleshooting.
- Use performance metrics tools to gain insights into server health.
For complex setups or critical applications, you may need to consider managed IT services. These can help ensure your server meets performance expectations, especially if your team lacks the necessary expertise.
With these tools and strategies in place, your Ubuntu-based Express.js server will be well-prepared for production workloads.