Linux
Covers Ubuntu/Debian and RHEL/Fedora.
Before starting, read Before You Deploy and complete the standalone output setup.
One-time server setup
Section titled “One-time server setup”Run the block that matches your OS — not both. The curl command adds the NodeSource repository to your package manager so it can find Node.js 22, then the install command pulls down Node.js and nginx in one step.
# Ubuntu / Debiancurl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -sudo apt install -y nodejs nginx
# RHEL / CentOS / Fedoracurl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -sudo dnf install -y nodejs nginxDeploy
Section titled “Deploy”-
On your build machine, run:
Terminal window npm --prefix web ci && npm --prefix web run build -
Copy the three folders into
/opt/my-app/on the server. Copy the contents ofweb/.next/standalone/(not the folder itself) into the deployment root:/opt/my-app/├── server.js├── package.json├── node_modules/├── .next/server/├── .next/static/ ← from web/.next/static/├── public/ ← from web/public/└── .env ← create this on the server -
Decide which user the app will run as. On Debian/Ubuntu, nginx runs as
www-data. On RHEL/Fedora, it runs asnginx. The systemd unit and folder ownership both need to reference the same user. The examples below usewww-data; substitutenginxfor RHEL/Fedora.Set ownership of the deployment folder:
Terminal window sudo chown -R www-data:www-data /opt/my-app -
Create a systemd service file at
/etc/systemd/system/my-app.service:[Unit]Description=My AppAfter=network.target[Service]Type=simpleUser=www-dataWorkingDirectory=/opt/my-appEnvironmentFile=/opt/my-app/.envEnvironment=NODE_ENV=productionEnvironment=PORT=3000Environment=HOSTNAME=127.0.0.1ExecStart=/usr/bin/node server.jsRestart=on-failureRestartSec=5[Install]WantedBy=multi-user.targetCheck the path to
nodewithwhich nodebefore using this. NodeSource packages install to/usr/bin/node, butnvmor a manual build typically lives elsewhere. UpdateExecStartto match whatwhich nodeprints.Start and enable the service:
Terminal window sudo systemctl daemon-reloadsudo systemctl enable --now my-appsudo systemctl status my-appsudo journalctl -u my-app -f -
Configure nginx as the reverse proxy. On Debian/Ubuntu, create
/etc/nginx/sites-available/my-app. On RHEL/Fedora, create/etc/nginx/conf.d/my-app.conf.Start with an HTTP-only config. Certbot adds the HTTPS block and redirect in the next step:
server {listen 80;server_name app.your-domain.com;client_max_body_size 20M;location / {proxy_pass http://127.0.0.1:3000;proxy_http_version 1.1;proxy_set_header Host $host;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";}}On Debian/Ubuntu, enable the config with a symlink:
Terminal window sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/On both distros, test and reload:
Terminal window sudo nginx -t && sudo systemctl reload nginx -
Get a TLS certificate with certbot. The
--nginxplugin fetches the certificate, updates the nginx config to add the HTTPS server block and an HTTP→HTTPS redirect, and installs a systemd timer to auto-renew before expiry:Terminal window # Debian / Ubuntusudo apt install -y certbot python3-certbot-nginx# RHEL / Fedorasudo dnf install -y certbot python3-certbot-nginxsudo certbot --nginx -d app.your-domain.comAfter certbot runs, check the updated nginx config to confirm the proxy headers from step 5 are still present inside the new
listen 443server block.
Verify
Section titled “Verify”sudo systemctl status my-app # is the Node process running?sudo journalctl -u my-app -f # any app-level errors?sudo nginx -t # is the nginx config valid?sudo tail -f /var/log/nginx/error.log # any proxy-level errors?Open your site in a browser. It should load over HTTPS without a port number.