Linux

Covers Ubuntu/Debian and RHEL/Fedora.

Before starting, read Before You Deploy and complete the standalone output setup.

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 / 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

Deploy

  1. On your build machine, run:

    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:

    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:

    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:

    sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/

    On both distros, test and reload:

    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:

    # 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.

Verify

Open your site in a browser. It should load over HTTPS without a port number.

Last updated

Was this helpful?