Core Panel

How to Deploy a Node.js App on Ubuntu with Nginx and systemd

CP
Written by Core Panel Team Product and documentation team · Published Sep 22, 2026 · 9 min read
How to Deploy a Node.js App on Ubuntu with Nginx and systemd
On this page · 2 sections

A production Node.js deployment is more than copying files to a server and running node server.js. The application needs a predictable runtime, a supervisor that restarts it after failure, a reverse proxy that handles public traffic, and a release process you can reverse when something goes wrong.

What this deployment includes

  • Ubuntu as the host operating system
  • A dedicated, non-root service account
  • systemd for startup, restart, and log collection
  • Nginx for HTTP handling and reverse proxying
  • TLS certificates for HTTPS
  • A repeatable release and rollback workflow

The examples assume your application listens on 127.0.0.1:3000 and exposes a lightweight /health endpoint.

1. Prepare the server and application user

Update the server, install Nginx, and create a dedicated account for the application. Avoid running the Node.js process as root.

sudo apt update
sudo apt upgrade -y
sudo apt install -y nginx
sudo adduser --system --group --home /srv/myapp myapp
sudo mkdir -p /srv/myapp/releases /srv/myapp/shared
sudo chown -R myapp:myapp /srv/myapp

Install a supported Node.js LTS release from a source you trust. Confirm the runtime before deploying:

node --version
npm --version

2. Place the application in a versioned release directory

Versioned directories make rollback straightforward. Put each build in a timestamped folder, install production dependencies, then point a stable current symlink at the active release.

sudo -u myapp mkdir -p /srv/myapp/releases/20260923-1000
# Copy the tested application build into this directory
cd /srv/myapp/releases/20260923-1000
sudo -u myapp npm ci --omit=dev
sudo -u myapp ln -sfn /srv/myapp/releases/20260923-1000 /srv/myapp/current

Keep secrets out of the release directory. Store environment values in a protected file such as /srv/myapp/shared/app.env, owned by root and readable only by the service group.

sudo install -o root -g myapp -m 0640 /dev/null /srv/myapp/shared/app.env

3. Run Node.js with systemd

Create /etc/systemd/system/myapp.service:

[Unit]
Description=My Node.js application
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/srv/myapp/shared/app.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Reload systemd, enable the service at boot, and verify that it starts cleanly:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
curl -f http://127.0.0.1:3000/health

Application logs are available through the journal:

sudo journalctl -u myapp -n 100 --no-pager
sudo journalctl -u myapp -f

4. Put Nginx in front of the application

Create /etc/nginx/sites-available/myapp and replace the example domain:

server {
    listen 80;
    listen [::]:80;
    server_name app.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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 60s;
    }
}

Enable the site only after the configuration test passes:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx

Keep port 3000 bound to localhost. Public traffic should reach the application through Nginx, not by connecting directly to the Node.js process.

5. Add HTTPS and verify the full path

Point the domain's DNS records at the server, obtain a certificate with your preferred ACME client, and configure automatic renewal. After HTTPS is active, test from outside the server:

curl -I https://app.example.com
curl -f https://app.example.com/health

Also test a real application route, a static asset, any upload limits, and WebSocket connections if the app uses them. A passing local health check does not prove that DNS, TLS, proxy headers, and public routing are all correct.

6. Make releases safe and reversible

For each release, install dependencies and run migrations before switching the current symlink. Restart the service only after the new release is ready.

sudo -u myapp ln -sfn /srv/myapp/releases/NEW_RELEASE /srv/myapp/current
sudo systemctl restart myapp
curl -f http://127.0.0.1:3000/health

If validation fails, point current back to the previous release and restart the service. Database changes require extra care: use backward-compatible migrations so the old application version can still run during rollback.

7. Monitor what users actually depend on

  • Alert when the public HTTPS health check fails, not only when the process stops.
  • Track response time, error rate, memory use, disk space, and certificate expiry.
  • Rotate and retain logs long enough to investigate incidents.
  • Test backups by restoring them to an isolated environment.
  • Patch Ubuntu, Node.js, dependencies, and Nginx on a planned cadence.

Production checklist

  • The Node.js process runs as a dedicated non-root user.
  • The application port is bound to localhost.
  • systemd starts the service at boot and restarts it after failure.
  • Nginx configuration passes nginx -t.
  • HTTPS works and certificate renewal is monitored.
  • Secrets are outside the release directory with restrictive permissions.
  • A health check validates the public request path.
  • The previous release remains available for rollback.

This structure keeps the deployment understandable: Nginx owns the public edge, systemd owns the application process, and versioned releases make changes traceable. Whether you operate the server directly or through a control panel, those boundaries are the foundation of a reliable Node.js hosting workflow.

Related posts

Stay in the loop

New posts and release notes, delivered to your inbox. No spam.

Ready to switch?
14-day free trial
Get Started