Skip to content

Linux

Covers Ubuntu/Debian and RHEL/Fedora.

Before starting, read Before You Deploy and complete the standalone output 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.

Terminal window
# Ubuntu / Debian
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs nginx
# RHEL / CentOS / Fedora
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs nginx
  1. On your build machine, run:

    Terminal window
    npm --prefix web ci && npm --prefix web run build
  2. Copy the three folders into /opt/my-app/ on the server. Copy the contents of web/.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
  3. Decide which user the app will run as. On Debian/Ubuntu, nginx runs as www-data. On RHEL/Fedora, it runs as nginx. The systemd unit and folder ownership both need to reference the same user. The examples below use www-data; substitute nginx for RHEL/Fedora.

    Set ownership of the deployment folder:

    Terminal window
    sudo chown -R www-data:www-data /opt/my-app
  4. Create a systemd service file at /etc/systemd/system/my-app.service:

    [Unit]
    Description=My App
    After=network.target
    [Service]
    Type=simple
    User=www-data
    WorkingDirectory=/opt/my-app
    EnvironmentFile=/opt/my-app/.env
    Environment=NODE_ENV=production
    Environment=PORT=3000
    Environment=HOSTNAME=127.0.0.1
    ExecStart=/usr/bin/node server.js
    Restart=on-failure
    RestartSec=5
    [Install]
    WantedBy=multi-user.target

    Check the path to node with which node before using this. NodeSource packages install to /usr/bin/node, but nvm or a manual build typically lives elsewhere. Update ExecStart to match what which node prints.

    Start and enable the service:

    Terminal window
    sudo systemctl daemon-reload
    sudo systemctl enable --now my-app
    sudo systemctl status my-app
    sudo journalctl -u my-app -f
  5. 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
  6. Get a TLS certificate with certbot. The --nginx plugin 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 / Ubuntu
    sudo apt install -y certbot python3-certbot-nginx
    # RHEL / Fedora
    sudo dnf install -y certbot python3-certbot-nginx
    sudo certbot --nginx -d app.your-domain.com

    After certbot runs, check the updated nginx config to confirm the proxy headers from step 5 are still present inside the new listen 443 server block.

Terminal window
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.