Nodejs Deployment on Linux: How Do You Install Node.js and Go Live?

What is Nodejs deployment on a Linux server?
Nodejs deployment is the process of installing the Node.js runtime on a Linux server and making your application reachable from the internet. In practice you install Node.js, pull your code, define environment variables, put Nginx in front of the app, and let systemd keep it running.
In practice, this guide is for site owners and developers who manage their own VPS. If you follow the commands in order, you will run a simple Node.js app behind your domain. If your host manages the server, the guide shows you which questions to ask your provider.
We are a digital marketing and web team, not a hosting company. Therefore the guide relies on the official Node.js, Nginx and systemd documentation. Every address in the examples uses example.com or a documentation IP block. Never mix your real addresses and passwords into sample files.
Also, we do not repeat sibling topics here. Process managers such as PM2 get their own article, and so does deploying a Next.js app to a VPS. This article covers a plain Node.js application only.
What should you prepare before Nodejs deployment?
First, you need a Linux server with internet access, SSH access, and a user with sudo rights. Your domain also needs an A record that points to the server IP. Without it, you cannot test the Nginx and TLS steps.
To confirm that DNS has propagated, use our DNS lookup tool. To double check the server address, the IP lookup tool does the job.
Here is the preparation list:
- Log in with an SSH key instead of a password.
- Create a separate system user without root rights for the app.
- Update the package index and install security updates.
- Point the domain A record at the server IP address.
- Leave only SSH, 80 and 443 open in the firewall.
Also, do not open the application port, for example 3000, to the outside. The app will listen on the local address only, and Nginx will take the public traffic. We explain the reasons in a later section.
Which Node.js install method should you choose: distro repository, NodeSource or nvm?
There are three common paths, and each one has a different trade-off. The distribution repository is the simplest, but its version follows the distribution schedule. However, the NodeSource repository gives you a newer major version. nvm lets you keep several versions per user.
| Method | Upside | Downside | Best for |
|---|---|---|---|
| Distro repository | One command, updates arrive through the package manager | The version can lag behind | Simple, short-lived projects |
| NodeSource repository | You install your chosen major version as a package | You trust a third party repository | A production server with one app |
| nvm | Several versions, easy switching | It is a shell function, so systemd does not see it by default | Development and test machines |
On a single app production server, we suggest the package manager route, because updates arrive together with the rest of the system. In addition, you avoid path problems in the systemd unit. If you manage many projects and versions, nvm is more comfortable.
Which Node.js version should you run in production?
Run only an Active LTS or Maintenance LTS release in production. The official Node.js release status page says it plainly: production applications should only use LTS releases. The Current line exists so that library authors can try new features.
According to that page, the Current phase lasts six months, and LTS gives guaranteed critical bug fixes for 30 months in total. Version numbers change often, so we do not hard code one here. Instead, check the current LTS number on that page.
Two practical rules help with this decision:
- Write the supported version in the
enginesfield of yourpackage.json. - Do not stay on an end of life release, because it gets no security fixes.
- Test every upgrade on a copy first, then reinstall your dependencies.
As a result, development and production stay on the same major version. Most "it worked on my machine" problems end right there.
How do you install Node.js from the distro repository?
On a Debian based system, installing Node.js takes two commands. First you refresh the package index, then you install the nodejs and npm packages. Then you confirm the versions on the command line.
sudo apt update
sudo apt install nodejs npm
node --version
npm --version
The version in the distribution repository may not be an LTS release. So compare the number in the output with the official release page. If it shows an old major version, consider NodeSource or nvm instead.
The RHEL family uses a different package manager, but the logic stays the same. Check your distribution documentation and confirm there which module or package name carries Node.js. We do not invent commands for those systems here.
How do you install Node.js with the NodeSource repository?
NodeSource is a third party repository that offers the major version you want as a package. First, a setup script adds that repository to your package manager. After that, you install Node.js with apt or dnf as usual.
Take the current script address and commands from the NodeSource page itself. We do not print a fixed command here, because the script address and the version choices change over time. For example, a command copied from an old blog post may install a different version today.
Do not pipe scripts straight into a shell. Instead, download the file, read it, and then run it. Also, this habit applies to every installer, not only to NodeSource.
Once the repository is in place, updates arrive through your package manager. Moreover, you upgrade on purpose, because switching the major version means changing the repository setup. That prevents surprise upgrades.
How do you install Node.js with nvm, and what should you watch in services?
nvm manages Node.js versions inside your home directory. The official README tells you to fetch the install script from an address that contains a version number. At the time of writing the README showed v0.40.8, but you should confirm the current number in the repository.
curl -o install-nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh
less install-nvm.sh
bash install-nvm.sh
nvm install --lts
nvm alias default node
Saving the script and reading it with less is safer than running it blind. After the install, first open a new session or reload your shell. Otherwise the nvm command stays invisible.
One point is critical. According to the nvm README, nvm is a sourced shell function, not an executable binary. Services and systemd do not load your profile files automatically. Therefore you must write the full path of node into the systemd unit.
If you add a .nvmrc file to the project root, nvm use reads the version from it. In production, prefer one fixed path over frequent switching. That consistency also saves time as the team grows.
How do you get your application code onto the server?
For every nodejs deployment, the cleanest way is to pull the code from a Git repository. Copying files by hand causes errors, because you cannot track which version runs on the server. With Git, every release has a commit ID and rollback becomes easy.
For the commands, see our Git and GitHub guide. On the server, use a read only deploy key instead. Do not place your personal account key on the machine.
sudo useradd --system --create-home --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo -u nodeapp git clone https://git.example.com/team/nodeapp.git /srv/nodeapp/current
cd /srv/nodeapp/current
sudo -u nodeapp npm ci --omit=dev
We pick npm ci for a reason. The Node.js security guide recommends it instead of npm install to enforce the lockfile. In other words, the command installs exactly the versions in the lockfile.
If your project needs a build step, such as TypeScript, run the build during the release. However, some projects need their dev dependencies to build. In that case, install everything first, build, and then keep only the production dependencies.
How do you manage the .env file and environment variables?
In a nodejs deployment, environment variables keep secrets such as passwords and API keys out of your code. For example, never commit these values to the Git repository. A key that enters the repository stays in the history even if you delete it later. So add a .env line to your .gitignore.
Node.js can read such a file without an extra package. The official CLI documentation says the --env-file option arrived in v20.6.0 and lost its experimental label in newer releases. If the same variable exists in both the environment and the file, the environment value wins.
PORT=3000
NODE_ENV=production
DATABASE_URL=postgres://appuser:PASSWORD@127.0.0.1:5432/appdb
In production, however, the systemd EnvironmentFile setting is often more practical. You put the file outside the code directory, for example at /etc/nodeapp/nodeapp.env. systemd reads it before it drops to the service user. Therefore you can keep the file owned by root with 600 permissions.
Also, keep the format simple: one KEY=value pair per line. PASSWORD in the example is a placeholder. Never copy a real value from this article, a screenshot or a chat.
How do you run the app by hand for the first time?
Before you create the service, run the app by hand in your nodejs deployment and confirm it works. If something breaks, you see the error directly in the terminal, not behind systemd. The small server below is enough for a test.
const http = require('node:http');
const port = process.env.PORT || 3000;
const server = http.createServer(function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('App is running\n');
});
server.listen(port, '127.0.0.1');
Notice that the listen call uses 127.0.0.1. As a result, the app answers only from inside the server. Nobody can connect directly from outside, and traffic passes through Nginx.
First, start the app, then open a second terminal and run curl -i http://127.0.0.1:3000/. If you see a 200 status and the text, the app is ready. Then stop it with Ctrl+C and move on.
Why should you not run Node.js directly on ports 80 and 443?
Because ports below 1024 need extra privileges, and running the app as root is a big risk. In addition, Nginx handles TLS, compression, static files and request limits far more maturely. As a result, your application can focus on business logic.
The Node.js security guide points in the same direction. For protection against denial of service attacks on the HTTP server, it advises a reverse proxy that receives and forwards requests. The same section stresses proper server timeouts such as headersTimeout and requestTimeout. Source: Node.js security best practices.
In short, the architecture looks like this:
- Nginx listens on ports 80 and 443 and ends the TLS connection.
- The Node.js app listens on a local address, for example port 3000.
- systemd runs the app as a non-root user and restarts it after a crash.
For the wider security picture, read our OWASP Top 10 article. To understand server side architecture better, what is backend development is a good start.
How do you put a Node.js app behind an Nginx reverse proxy?
You open a server block in Nginx and forward requests to the local address of the app. On Debian based systems, configuration files usually live under sites-available. Other distributions may use the conf.d folder. Therefore, check the layout of your own distribution.
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
After you save the file, test the syntax first and then reload. A broken file can take down a working site, because Nginx applies it on reload. So never skip the nginx -t step.
sudo nginx -t
sudo systemctl reload nginx
According to the Nginx documentation, the proxy_pass directive sets the protocol and the address of the proxied server. If you add a URI to the address, it replaces the matching part of the location. If you leave it out, Nginx passes the normalized request URI as is. Source: ngx_http_proxy_module.
For HTTPS, you define the certificate in Nginx. We explained what a certificate is in our SSL certificate article. After setup, you can check the chain with the SSL checker.
Which Nginx headers and timeouts matter most?
Watch three things: the Host header, the client IP, and timeouts. According to the Nginx documentation, Host defaults to $proxy_host. If your app needs the original domain, you must pass $host explicitly.
Second, look at the client IP. Behind a proxy, the app always sees 127.0.0.1 as the caller. The X-Forwarded-For and X-Forwarded-Proto headers carry the real information. Your framework may need a proxy setting to trust them. For example, Express has a trust proxy setting for this.
| Setting | What it does | Status in the official docs |
|---|---|---|
| proxy_set_header Host | Passes the original domain to the app | Default is $proxy_host |
| proxy_http_version | HTTP version of the proxy connection | 1.1 or 2 recommended, the default depends on the Nginx version |
| proxy_read_timeout | Wait between two successive reads | Default is 60 seconds |
However, if your app has long requests or uses WebSockets, review proxy_read_timeout. For WebSockets you also need to pass the Upgrade and Connection headers. See the Nginx WebSocket proxying page for details.
How does a systemd service keep the Node.js app running?
In a nodejs deployment, systemd starts your app in the background, restarts it after a crash, and launches it when the server boots. For that, you write a unit file. Place it at /etc/systemd/system/nodeapp.service.
[Unit]
Description=Example Node.js application
After=network.target
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/nodeapp.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Find the path in the ExecStart line on your own system with command -v node. systemd wants the full path. If you used nvm, then the path sits under your home directory.
According to the systemd documentation, Type=simple is the default, and the service counts as started once the main process forks. Restart=on-failure restarts the service on a non-zero exit code. RestartSec sets the pause before the restart. Source: systemd.service manual.
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemctl status nodeapp
sudo journalctl -u nodeapp -f
The last command streams live logs. If the app does not start, then look there first.
How do you harden the systemd service?
You can narrow the app privileges by adding a few lines to the unit file. The systemd.exec manual describes these lines. Each one shrinks the attack surface, but strict settings can also block file writes. So add one, then test.
NoNewPrivileges=truestops processes from gaining new privileges.PrivateTmp=truegives the service its own /tmp and /var/tmp.ProtectSystem=strictmakes the file system read only, and you can write only to paths that you open withReadWritePaths=.
If your app writes to an upload folder or a log file, remember to open that path with ReadWritePaths. Otherwise the app throws a permission error, and the cause is not obvious at first glance.
Add the settings one at a time, and run systemctl restart plus a log check after each. This method feels slow. However, you quickly find which line causes a problem.
Should you deploy the Node.js app with Docker instead?
Docker is a way to run an app together with its dependencies in one package. The method in this article runs the app directly on the server. Both are valid, though they suit different teams. Your choice depends on team habits and the size of your infrastructure.
| Criterion | Directly on the server (systemd) | With Docker |
|---|---|---|
| Learning curve | Lower, fewer moving parts | Higher, you need image and network concepts |
| Environment consistency | Server and development can differ | The same image runs everywhere |
| Updates | Package manager and npm | You rebuild the image |
| Resource overhead | Lower | Slightly higher |
For one small app, plain nodejs deployment with systemd is often enough and simple. If you run several services, or want an exact match with your local setup, Docker makes sense. We covered container basics in our Docker guide.
How do you keep dependencies and the firewall under control?
Two habits protect you the most: audit your dependencies and keep the number of open ports small. The Node.js security guide recommends lockfiles and automating vulnerability checks with npm audit in your CI process.
Also, the guide covers malicious third party modules. It suggests pinning exact versions. In other words, you use exact versions and a lockfile instead of loose ranges. That way, every release installs the same dependency tree.
In the firewall, keep only the necessary ports open:
- The SSH management port, ideally for known addresses only.
- Ports 80 and 443 for HTTP and HTTPS.
- The application port, port 3000 in our example, stays closed.
The firewall tool differs by distribution. Follow the documentation of your own system, and make sure your SSH session will survive before you apply rules. One wrong rule at this step can therefore lock you out of the server.
How do you ship a new release to the server?
The update flow of a nodejs deployment is short: pull the code, install dependencies, build if needed, and restart the service. If you collect these steps in a small script, you lower the error rate. Moreover, every release then runs in the same order.
cd /srv/nodeapp/current
sudo -u nodeapp git pull --ff-only
sudo -u nodeapp npm ci --omit=dev
sudo systemctl restart nodeapp
sudo systemctl status nodeapp
A restart causes a short outage. For small sites, that is often acceptable. If you need a zero downtime switch, you need a process manager or several copies of the app. We cover that approach in the PM2 article.
Prepare a rollback plan too. For example, tag every release, and if something breaks, go back to the previous tag and restart the service. However, releases that change the database schema make rollback harder. In that case, take backups beforehand.
For a backup routine, read our website backup strategy guide.
How do you watch logs and resource use?
systemd writes the output of the service to the journal. You do not need a separate log file. The command journalctl -u nodeapp --since "1 hour ago" lists the last hour. So you can see when the error started.
Memory leaks are a common problem after any nodejs deployment in Node.js apps. The service eats more memory over time and finally crashes. Restart=on-failure hides this for a while, but it does not fix the cause. Therefore track the restart count as well.
For basic monitoring, set up an external uptime check. For example, a service that polls your address at fixed intervals and alerts you when no answer comes back is enough. A check from inside the server cannot warn you when the whole server goes down.
What should you check after the Nodejs deployment?
First, confirm that the service is up. Then confirm that it answers from the outside world. Next, look for errors in the logs. Finally, take speed and security measurements. These four checks catch most surprises after a release.
systemctl status nodeappshows that the service runs.curl -I https://example.com/returns the outside response and headers.journalctl -u nodeapplists the application logs.- Verify the certificate chain with the SSL tool and the DNS record with the DNS tool.
For performance, we suggest a Google Lighthouse performance test. We explained how the results relate to search visibility in how site speed affects SEO.
If your app reads the same data often, then a cache also helps. You can find the Redis and Memcached difference in our caching article.
Which mistakes show up most often in Node.js deployments?
The most frequent mistakes are port mismatches, permission problems and missing environment variables. Most of them show up as a 502 Bad Gateway in the browser. That error means Nginx cannot reach the app. The cause is usually that the app is down or listens on a different port.
| Symptom | Likely cause | First check |
|---|---|---|
| 502 Bad Gateway | App is down or on the wrong port | systemctl status and the proxy_pass port |
| EADDRINUSE error | Another process holds the port | Look for the process that owns the port |
| EACCES permission error | The service user cannot write to a path | File ownership and ReadWritePaths |
| Empty environment variable | Wrong EnvironmentFile path or format | File path and line format |
| Command not found | The nvm path is invisible to systemd | Full path in ExecStart |
When you debug, first read the log. Instead of guessing, read the journalctl output. In many cases the answer sits in the last few lines. If you are still stuck, stop the service and run the app by hand to reduce the problem to one variable.
When should you not do Nodejs deployment yourself?
Leave the job to your hosting provider when you face a live database change without a backup, a server under attack, or a store that takes payments. Also, server management needs constant care. If you have no time for updates, monitoring and incident response, a managed service is safer.
We are not a hosting company, so this article makes no operating claims. It relies on official documentation. If you will run a business critical app, ask your provider in writing for backups, monitoring and support hours.
Turn to a provider or a managed platform in these cases:
- Nobody on your side can apply security patches every week.
- An outage means direct revenue loss.
- You process personal data or payment details and carry compliance duties.
For hosting choices, see our guide to choosing web hosting. If you want help building or taking over the application itself, our custom software development service covers that.
Conclusion: a short checklist for Nodejs deployment
In short, the flow is this: install an LTS release, pull the code with Git, keep secrets out of the code, let the app listen on the local address only, and leave public traffic to Nginx. Use systemd for continuity, and check the logs after every release.
- Does the server have a non-root service user?
- Is the Node.js version Active or Maintenance LTS?
- Do the secrets live outside Git, in a file with 600 permissions?
- Is the app listening on 127.0.0.1?
- Is
nginx -tpassing without errors? - Does the service start on its own after a reboot?
Then repeat this list for every new project. With repetition, the process becomes routine.



