This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Manage client sites with AI. Join our free live webinar and see Cloudways MCP in action.Register now!

How to Deploy a Node.js Application (From Development to Production)

Updated on July 20, 2026

13 Min Read
How to Deploy a Node JS application

Key Takeaways

  • Moving from local development to production means handling environment variables, error handling, process management, and security in a way your local setup never required.
  • Managed Node.js hosting takes care of most of the underlying infrastructure, so you can focus on code instead of setting up PM2, Nginx, and SSL certificates by hand.
  • Deployment options range from unmanaged VPS setups where you handle everything yourself to fully serverless platforms, each with different tradeoffs on control, cost, and complexity.

Getting a Node.js app to run on your laptop is one thing. Getting it live on a real server, safely, and in a way that doesn’t fall over the moment real users hit it, is a whole different challenge.

There’s environment variables to handle, security to think about, a process manager to keep the app alive, and the actual question of where to host it in the first place. Most tutorials skip right past all of that.

In this blog, I’ll break down the practical stuff that goes into deploying a Node.js app, cover the different places you can deploy it, and then build a small URL shortener from scratch and get it live on Cloudways.

What Actually Matters When Deploying a Node.js App

Deploying isn’t just running npm start on a different machine. A few practical things matter more than people usually give them credit for.

Environment variables are the first. Anything sensitive or environment-specific, API keys, ports, base URLs, shouldn’t be hardcoded in the source. Locally, that stuff lives in a .env file. In production, the hosting platform holds onto those values and passes them into the app at runtime.

Error handling is another one. One unhandled error in production can take down the whole app for every user, or worse, leak a raw stack trace that exposes internal details. A basic error-handling middleware catches all that and shows users something sensible instead.

Then there’s the security side. Live apps get scanned by bots almost the second they’re online. A couple of easy wins go a long way here, Helmet for sensible HTTP headers, rate limiting so no one can flood a route, and basic input validation so the app doesn’t just trust whatever a random visitor types in.

And finally, keeping the app running. When you close your terminal, node server.js dies with it, obviously not going to fly on a live server. PM2 is one way to handle this yourself, but on a managed platform, this bit’s already sorted for you.

Skip the Server Setup — Deploy on Cloudways

Cloudways Managed Node.js Hosting handles the server, process management, and deployment plumbing so you can focus on the app itself.

Where to Actually Deploy Your Node.js App

There’s no single right place to deploy a Node.js app, it really depends on how much of the underlying stuff you feel like managing yourself.

Unmanaged VPS — You get a bare server and control over everything on it. That also means you’re installing Node yourself, setting up PM2, configuring Nginx, sorting SSL, and handling OS updates. Full flexibility, but also full responsibility.

Managed Node.js hosting — The provider handles the server, process management, and deployment plumbing. You hand it a Git repo, pick your framework and Node version, and the app goes live. Way less to worry about.

Serverless — Instead of a persistent server, your code runs on demand whenever a request comes in. Nice for spiky, low-traffic apps, but it usually means rewriting parts of the code to fit that model.

For this walkthrough, I’ll use Cloudways’ Managed Node.js Hosting, mainly because it lets me focus on the actual app instead of spending time configuring a server from scratch.

How to Deploy a Node.js App: A Practical Walkthrough

To pull all of this together, I’ll build a small URL shortener, an app that takes a long URL, gives you a short one back, and redirects to the original whenever someone hits the short link.

By the time I’m done, this app will be live on a real server, protected by the basic security stuff mentioned earlier, and pulling its config from environment variables instead of anything hardcoded.

What I’ll Be Using

  • Node.js
  • VS Code
  • Express, EJS, and SQLite
  • Helmet, express-rate-limit, and nanoid

Step 1: Setting Up the Node.js Project

Before I can write any code, I need Node.js on my machine.

I’m working on my office laptop, which has IT restrictions, so I’ll grab the standalone binary version of Node.js from nodejs.org instead of running a normal installer.

Screenshot of the Node.js download page showing Windows options: MSI installer and Standalone Binary (.zip). The arrow points to the Standalone Binary button.

Once it downloads, I’ll unzip it into my Downloads folder.

Context menu in Windows Explorer showing 'Extract All...' for a ZIP file in Downloads folder (ZIP extraction option highlighted).

Opening the project folder in Command Prompt

I’ll create a folder for this project on my desktop and just call it node-url-shortener.

Then I’ll open up the Command Prompt and move into my project folder:

cd C:\Users\abdulrehman\Desktop\node-url-shortener

Command Prompt window on Windows showing the user navigating to the node-url-shortener folder on the Desktop, with the command: cd C:UsersabdulrehmanDesktopnode-url-shortener

Pointing Command Prompt to the Node binary

Command Prompt doesn’t automatically know where the Node files are on my machine, so I’ll point it there manually using this command:

set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64

Command Prompt showing a directory change to the node-url-shortener folder and updating PATH to include the Node v24.18.0 binary path.

To make sure it’s actually working, I’ll run:

node -v
npm -v

Version No for Node JS

Both commands returned the version numbers, so I’m good to continue.

Creating the Node.js project

I’ll initialize a plain Node.js project first:

npm init -y

Command Prompt showing npm init for a node-url-shortener project; package.json with name 'url-shortener', version 1.0.0, and a test script.

Then I’ll install everything this project actually needs: Express for routing, EJS to render the HTML, better-sqlite3 for a tiny file-based database, nanoid to generate short codes, dotenv to load environment variables, and Helmet plus express-rate-limit for the two most basic security concerns any live app should have.

npm install express ejs better-sqlite3 nanoid dotenv helmet express-rate-limit

Command prompt displaying npm install express with several package messages and deprecation warnings, showing a Windows-style path and prompts.

Step 2: Building the URL Shortener

Now I’ll open my project folder in VS Code. To do this, I’ll go to File > Open Folder and select my node-url-shortener folder.

VS Code Explorer showing a project named NODE-URL-SHORTENER with a node_modules folder and files package-lock.json and package.json

Alright, my project’s opened in VS Code, so now it’s time to actually build the thing.

Setting Up the Database

First thing I want to do is get the database set up so short links can actually stick around when the app restarts. To do this, in the root of my project folder, I’ll create a folder called db, and inside it, a file called database.js. I’ll paste this in:

const path = require('path');
const Database = require('better-sqlite3');

const db = new Database(path.join(__dirname, 'urls.db'));

db.exec(`
  CREATE TABLE IF NOT EXISTS urls (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT UNIQUE NOT NULL,
    original_url TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

module.exports = db;

VS Code editor showing a Node.js project named NODE-URL-SHORTENER; left file explorer with db/database.js, and right pane displaying code to create a SQLite database and a CREATE TABLE IF NOT EXISTS statement for urls.

This creates a SQLite database file the first time the app runs, with a table for the original URL, its short code, and when it was created.

Adding Environment Variables

Next, I need somewhere to keep config that shouldn’t be hardcoded. To do this, in my project root, I’ll create a new file called .env and paste in:

BASE_URL=http://localhost:3000
NODE_ENV=development

VS Code showing a .env file with BASE_URL=http://localhost:3000 and NODE_ENV=development in a Node URL shortener project.

BASE_URL is what gets used when building each short link. I’ll switch it to the live Cloudways URL later. NODE_ENV=development is fine for now while I’m testing locally, but I’ll flip it to production on the live server.

Now, since I don’t want any of this ending up in my public GitHub repo, I’ll create one more file in my project root called .gitignore, and paste in:

node_modules
.env
db/urls.db

Screenshot of VS Code explorer showing a project with .gitignore open; ignores: node_modules, .env, db/urls.db

This tells Git to skip all three whenever I push. node_modules because it’s just downloaded packages. .env because it has my actual config values. And db/urls.db because that’s the local database file, which shouldn’t ship with the code.

Setting Up the Server

This is where I’ll build the actual app: it takes the long URL, generates a short code for it, stores both in the database, and handles the redirect when someone hits the short link.

I’ll create a file called server.js in my project root, and paste this in:

VS Code Explorer showing a Node.js project named NODE-URL-SHORTENER with server.js highlighted at the root.

require('dotenv').config();

const express = require('express');
const path = require('path');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { nanoid } = require('nanoid');
const db = require('./db/database');

const app = express();
const PORT = process.env.PORT || 3000;
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));

app.use(helmet());
app.use(express.urlencoded({ extended: true }));

// Limit the shorten route to 10 requests per minute per IP
const shortenLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 10,
  message: 'Too many requests, please slow down.',
});

app.get('/', (req, res) => {
  res.render('index', { shortUrl: null, error: null });
});

app.post('/shorten', shortenLimiter, (req, res) => {
  const { url } = req.body;

  try {
    new URL(url);
  } catch {
    return res.render('index', { shortUrl: null, error: 'Please enter a valid URL.' });
  }

  const code = nanoid(7);
  db.prepare('INSERT INTO urls (code, original_url) VALUES (?, ?)').run(code, url);

  res.render('index', { shortUrl: `${BASE_URL}/${code}`, error: null });
});

app.get('/:code', (req, res) => {
  const row = db.prepare('SELECT original_url FROM urls WHERE code = ?').get(req.params.code);

  if (!row) {
    return res.status(404).render('not-found');
  }

  res.redirect(row.original_url);
});

// Basic error handler so users never see a raw stack trace
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).send('Something went wrong on our end.');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Helmet’s applied across the whole app, the shorten route is wrapped in a rate limiter, and there’s an error handler at the bottom that makes sure users never end up looking at a stack trace if things break.

That server.js points to a template called index that doesn’t exist yet, so next I’ll create the page it needs.

Building the UI

I’ll create a folder called views in my project root, and inside it, a file called index.ejs. I’ll paste this in:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Shorten a URL</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <main>
    <div class="card">
      <h1>Shorten a URL</h1>
      <form action="/shorten" method="POST">
        <input type="text" name="url" placeholder="Paste a long URL here" required>
        <button type="submit">Shorten</button>
      </form>

      <% if (error) { %>
        <p class="error"><%= error %></p>
      <% } %>

      <% if (shortUrl) { %>
        <div class="result">
          <p>Here's your short link:</p>
          <a href="<%= shortUrl %>" target="_blank"><%= shortUrl %></a>
        </div>
      <% } %>
    </div>
  </main>
</body>
</html>

VS Code window showing a NODE-URL-SHORTENER project with index.ejs open and a URL-shortening form in dark theme.

Inside the same views folder, I’ll create another file called not-found.ejs, this is what shows up when someone hits a short code that doesn’t exist. I’ll paste this in:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Link Not Found</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <main>
    <div class="card">
      <h1>Link Not Found</h1>
      <p>That short link doesn't exist, or it may have expired.</p>
      <a href="/">Go back</a>
    </div>
  </main>
</body>
</html>

VS Code screenshot showing a 404 Not Found page: 'Link Not Found' with explanatory text and a 'Go back' link

And last thing for this step, I’ll create a folder called public in my project root, and inside it, a file called style.css. I’ll paste this in:

* {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
  background: linear-gradient(135deg, #667eea, #764ba2);
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

main {
  width: 100%;
  max-width: 500px;
}

.card {
  background: #ffffff;
  border-radius: 12px;
  padding: 40px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}

h1 {
  margin-top: 0;
  color: #1f2937;
}

form {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

input {
  flex: 1;
  padding: 12px 14px;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  font-size: 14px;
}

button {
  background: #667eea;
  color: #ffffff;
  border: none;
  padding: 12px 20px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
}

button:hover {
  background: #5568d3;
}

.error {
  color: #dc2626;
  margin-top: 15px;
}

.result {
  margin-top: 25px;
  padding: 15px;
  background: #f3f4f6;
  border-radius: 8px;
}

.result a {
  color: #667eea;
  word-break: break-all;
}

Style css

This gives the whole thing a clean, card-based look, nothing crazy, just enough styling so the app doesn’t look like plain, unstyled text.

Testing What We Built

Alright, time to test. I’ll add a start script to my package.json so I don’t have to keep typing the full node command:

"scripts": {
  "start": "node server.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}

Package.json for a Node URL shortener: name 'url-shortener', version 1.0.0, main index.js, start script 'node server.js', test script echoes an error, and dependencies like express, ejs, dotenv, nanoid, better-sqlite3, and helmet.

Then, back in Command Prompt:

npm start

I’ll verify it started by opening my browser and going to http://localhost:3000. I’ll paste in some long URL, hit Shorten, and I should get a short link back. Clicking it takes me straight to the original.

Everything’s flowing exactly how I want it to.

Centered white modal card on a purple background with the headline 'Shorten a URL', a URL input field containing a sample link, and a blue 'Shorten' button.

Step 3: Pushing the Project to GitHub

Before I deploy anywhere, I want this pushed to GitHub first.

I’ll head over to GitHub and create a new repository called node-url-shortener.

Then, back in Command Prompt, I’ll run these one at a time to upload my project:

git init
git add .
git commit -m "First commit"
git branch -M main
git remote add origin https://github.com/abdulrehman293/node-url-shortener
git push -u origin main

Now I’ll go back to my GitHub repo and refresh. And as expected, all my files are uploaded, minus the .env, node_modules, and the local database file.

Select Node URL Shortener

Step 4: Deploying to Cloudways

At this point, my project’s working on the local setup and it’s also pushed to GitHub. Now I’ll move it from local to a live server. For this, I’ll use Cloudways’ Managed Node.js Hosting, built exactly for deploying projects like this directly from a Git repo.

From the Cloudways dashboard, I’ll click Node.js from the menu, and then click Launch Now.

Cloudways dashboard showing a Node.js Application panel with a green Node.js logo and a blue 'Launch Now' button

From here, I’ll pick the Starter plan since this project doesn’t need heavy resources to run.

Pricing plan selection page with five plan cards (Starter, Professional, Growth, Scale, Plus); each shows monthly price and specs, with a blue Proceed button at bottom-right to continue.

Now from the Deploy Your Node.js Web App screen, I’ll click Connect Via Git, and sign into my GitHub account when it asks.

Once signed in, I’ll pick my node-url-shortener repo from the list and continue.

Select Node URL Shortner

Step 5: Configuring and Going Live

This next part is where I tell Cloudways how to run the project.

I’ll choose Express as the Framework Preset. For the Node Version, I’ll choose Node 24 (LTS). I’ll leave the Root Directory as default.

Interface for importing a GitHub project with build settings: language like node-url-shortener, Express preset, main branch, Node 24 (LTS).

Next, I’ll click on the Change option under Build and output settings. Then, I’ll set Package Manager to npm, and the Entry File to server.js.

Modal titled 'Change build and output settings' with fields for Package Manager set to npm and Entry File set to server.js, and actions 'Save Changes' and 'Cancel' visible in a dimmed background screen.

Now, since my .env file intentionally didn’t get uploaded to GitHub, I need to add those values manually. I’ll go to the Environment Variables section and hit Add. Here, I can add my variables.

For the first one, I’ll set Key to NODE_ENV and Value to production.

Then I’ll click + Add more and set Key to BASE_URL. And for the value, for now, I’ll put in a placeholder value like http://placeholder.com since I don’t have my temporary Cloudways URL yet. I’ll come back and update this after the first deploy.

Once both are in, I’ll click Save.

Build settings panel: GitHub import for node-url-shortener, Express preset, main branch, Node 24 (LTS), root ./

Modal for setting environment variables, showing two entries: NODE_ENV=production and BASE_URL=http://placeholder.com, with Remove options and a Save button.

Once that’s filled in, I’ll hit Deploy Now.

Now, Cloudways will pull the code from GitHub, install the dependencies, and start the app using the entry file I pointed it to.

Deployment status UI for a Node.js app: server setup complete, building the application, with a build logs panel and a warning about no live logs in this browser session.

Deployment status UI for a Node.js app: server setup complete, building the application, with a build logs panel and a warning about no live logs in this browser session.

Moment of Truth

Once the deployment shows as successful, I’ll grab the temporary Cloudways URL from the app’s Overview page.

Application Domain

I’ll head back to Environment Variables, update BASE_URL to that URL, and redeploy so the change actually takes effect.

Environment Variables panel showing NODE_ENV=production and BASE_URL=http://placeholder.com with an Edit menu open.

Modal titled Set environment variables with Key 'BASE_URL' and a long URL in the Value field, plus Save and Cancel buttons.

Saved Environment variable

Now if I open my temporary Cloudways URL in the browser, paste in some long URL, and hit Shorten, I get back a short link pointing to my live Cloudways domain. Clicking it takes me straight to the original.

URL Shortening process

The whole thing’s up, and any short link I make from here on out will keep working as long as the app’s running.

Wrapping Up

That wraps up this run-through on deploying a Node.js application. In this blog, I covered what really goes into moving from local development to a live server, environment variables, error handling, security, and keeping the app alive, and then went through the different places you can actually deploy.

I also built a small URL shortener with a working UI, added basic production security, and pushed the finished project to my GitHub. Feel free to clone it and reuse the code.

And if you also want to try deploying something similar yourself, our Managed Node.js Hosting handles this exact kind of workflow, connect your repo, pick your framework, and you’re live.

Ready to Deploy Your Own Node.js Project?

Connect your Git repo, pick your framework, and get your app live on Cloudways in minutes.

Q. Do I need a special server to deploy a Node.js application?

Not really. A plain VPS works if you’re comfortable managing the server yourself, or you can go with a managed platform like Cloudways where the server, process management, and deployment plumbing is handled for you. Serverless is a third option, though it tends to fit certain kinds of apps more than others.

Q. What’s the difference between running a Node.js app locally and in production?

Locally, you’re the only user, crashes aren’t a big deal, and config lives in a file on your machine. In production, real users are on the app, one unhandled error can bring the whole thing down, and config has to live outside the code so different environments can use different values.

Q. Do I need PM2 to keep my Node.js app running?

Only if you’re managing the server yourself. PM2 restarts the app if it crashes and keeps it running after you close the terminal. On a managed platform, that’s already handled behind the scenes.

Q. Is it safe to deploy a Node.js app straight from GitHub?

Yep, as long as anything sensitive lives in environment variables and not in the code. Your .env file should be excluded via .gitignore so it never ends up in the repo, and the deployment platform stores those same values separately and passes them in at runtime.

Share your opinion in the comment section. COMMENT NOW

Share This Article

Start Growing with Cloudways Today.

Our Clients Love us because we never compromise on these

Abdul Rehman

Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour