Skip to content
DevOps devops app-deployment 6 min read

Deploying a Node.js App on Ubuntu

Getting a Node.js app onto a real server is the moment your code stops being a project on your laptop and starts being something other people can actually use. This page walks you through the full path on a fresh Ubuntu server: installing Node, pulling down your code, installing dependencies cleanly, setting your environment variables, and running the app. We will do every step by hand so you understand what each piece does, then point you to the pages that turn this into a proper, always-on production setup.

This guide targets Ubuntu 22.04 LTS and 24.04 LTS. Every command is real and runs on a fresh server.

Before you start

You need a server you can log into over SSH (Secure Shell — an encrypted way to run commands on a remote machine) and a user with sudo (the command that lets a normal user run admin-level commands). If you only have the root user, that works too, but running apps as root is a security risk, so we will assume a regular user named deploy.

First update the package lists and install Git and a build toolchain. Many npm packages compile native code, and build-essential provides the compilers they need.

sudo apt update
sudo apt install -y git build-essential

Output:

Reading package lists... Done
Building dependency tree... Done
git is already the newest version (1:2.43.0-1ubuntu7.1).
build-essential set up (12.10ubuntu1).
0 upgraded, 0 newly installed.

Installing Node.js

There are two good ways to install Node on Ubuntu. Pick one.

MethodWhat it isWhen to use it
nvmNode Version Manager — installs Node per-user in your home folder, lets you switch versions instantlyYou want to run different Node versions, or test upgrades safely. Best for most developers.
NodeSourceAn official apt repository that installs Node system-wideYou want one fixed version managed like any other system package, installed for all users and services.

Do not use the plain apt install nodejs from Ubuntu’s default repos — it is often years out of date.

nvm (Node Version Manager) installs Node inside your home directory, so you never need sudo to change versions.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm use --lts
node -v

Output:

Now using node v22.16.0 (npm v10.9.2)
v22.16.0

Gotcha: nvm only loads inside an interactive shell that reads ~/.bashrc. A systemd service or a cron job does not get nvm’s Node on its PATH. If you plan to run your app under systemd (covered on the systemd page), use NodeSource instead, or point the service at the full Node path like /home/deploy/.nvm/versions/node/v22.16.0/bin/node.

This adds an official Node apt repository and installs Node for the whole machine, which is cleaner for background services.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v

Output:

node -v
v22.16.0
npm -v
10.9.2

Getting your code onto the server

Clone your repository with Git. For a private repo you will need a deploy key or an access token; for a public repo the HTTPS URL just works.

cd ~
git clone https://github.com/your-org/your-app.git
cd your-app

Output:

Cloning into 'your-app'...
remote: Enumerating objects: 482, done.
Receiving objects: 100% (482/482), 1.21 MiB | 4.30 MiB/s, done.
Resolving deltas: 100% (210/210), done.

Tip: Clone into a normal user’s home directory (here /home/deploy), not somewhere owned by root. Your app should run as an unprivileged user so a bug or exploit cannot take over the whole machine.

Installing dependencies the production way

On a server, always use npm ci instead of npm install. The ci stands for “clean install”: it deletes any existing node_modules, then installs the exact versions pinned in your package-lock.json. This makes deploys reproducible — you get the same bytes every time — and it fails loudly if the lockfile and package.json disagree, which catches mistakes early.

npm ci --omit=dev

The --omit=dev flag skips devDependencies (test tools, linters, build-only packages) because you do not need them in production. If your app needs a build step (TypeScript, a bundler), run npm ci without that flag first, build, then prune.

Output:

added 214 packages, and audited 215 packages in 6s

38 packages are looking for funding
found 0 vulnerabilities

Setting environment variables

Environment variables are key-value settings the operating system hands to your program when it starts. They are how you keep secrets (database passwords, API keys) and per-environment settings out of your code. The single most important one in production is NODE_ENV.

Setting NODE_ENV=production tells Node and most libraries (Express in particular) to run in fast, secure mode: caching templates, hiding detailed error stacks from users, and skipping development-only checks. Always set it.

The simplest approach for now is a .env file that your app reads at startup (most apps use the dotenv package, or Node’s built-in --env-file flag in Node 20+).

nano .env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://appuser:s3cret@localhost:5432/appdb
SESSION_SECRET=change-this-to-a-long-random-string

Lock the file down so other users on the server cannot read your secrets:

chmod 600 .env

Never commit .env to Git. Add it to .gitignore. For a deeper look at managing config across environments, see the environment config page linked below.

Running the app

Now start it. With Node 20+ you can load the env file directly without any extra package:

node --env-file=.env server.js

Output:

Server listening on http://0.0.0.0:3000 (production)
Connected to database appdb

Test it from the server itself in a second SSH session:

curl -i http://localhost:3000/health

Output:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15

{"status":"ok"}

If you let the firewall through and visit the server’s IP on port 3000, you would see the app — but do not expose Node directly to the internet. The next step is to put a reverse proxy (a server that sits in front of your app and forwards requests to it, usually Nginx) on ports 80 and 443, and keep Node listening only on localhost. Open only the ports you actually need with ufw (Uncomplicated Firewall):

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

What’s missing for real production

Running node server.js in your terminal works for a quick test, but the moment you close SSH the app dies. And if it crashes, nothing restarts it. Production needs two more things:

  • A process manager so the app starts on boot and restarts on crash — either PM2 or a systemd service.
  • A reverse proxy (Nginx) in front for HTTPS, a friendly domain, and to keep Node off the public internet.

Both have dedicated pages below. Wire those up next.

Best Practices

  • Always set NODE_ENV=production — it is the single biggest free performance and security win for Node apps.
  • Use npm ci, not npm install, on servers so deploys are reproducible from the lockfile.
  • Run the app as an unprivileged user (e.g. deploy), never as root.
  • Keep secrets in a .env file with chmod 600 permissions, and never commit it to Git.
  • Bind Node to localhost and put Nginx in front; only open ports 22, 80, and 443 in ufw.
  • Pin a specific Node LTS version and document it, so the next deploy uses the same runtime.
  • Add a /health endpoint so your proxy and monitoring can check the app is alive.
Last updated June 15, 2026
Was this helpful?