Skip to content

Deploying a Next.js / Node.js app#

On your laptop you run npm run dev, localhost:3000 opens, everything is fine. Then you need it on the internet, and four separate problems show up at once.

This page solves all four in order. At each step you get what is happening and what to tell Morpheus.

The four problems#

Problem Fix
The app must run on the server Node.js install + production build
It must not die when you close the terminal systemd service (or PM2)
Visitors hit 443, not 3000 Nginx reverse proxy
The browser must not say "not secure" Let's Encrypt certificate

0. Point the domain first#

Do this before anything else, because DNS takes time to propagate and the certificate step depends on it.

Your domain's A record must point at your server's IP. If your domain is with VeriTeknik you can do this from the DNS section of the panel.

With Morpheus

"point the A record for example.com at 185.96.x.x"

DNS record management is one of the structured operations Morpheus is reliable at; unlike in-server work it is not running free-form commands here.

Verify propagation:

dig +short example.com

1. Install Node.js#

The version in your distribution's own repository is usually old. Use NodeSource.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y nodejs
node -v

With Morpheus

"install Node.js 22 on my server"

Morpheus detects the distribution itself (cat /etc/os-release) and picks the right command. You do not have to think about apt versus dnf.


2. Get the code onto the server and build it#

sudo mkdir -p /var/www/myapp
sudo chown -R $USER:$USER /var/www/myapp
cd /var/www/myapp

git clone https://github.com/user/repo.git .
npm ci
npm run build

Environment variables

Your .env file is not in git, and should not be. Create it on the server:

nano /var/www/myapp/.env.production
chmod 600 /var/www/myapp/.env.production

Do not have Morpheus read this file. Keep your server secrets out of the conversation; there is no need for them there.


3. Turn it into a service#

So the app survives closing your terminal and comes back after a reboot, write a systemd unit.

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My App (Next.js)
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/myapp
EnvironmentFile=/var/www/myapp/.env.production
ExecStart=/usr/bin/npm run start
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

Restart=always matters: if the app crashes, systemd brings it back.

With Morpheus

"create a systemd service for myapp that runs npm run start in /var/www/myapp and restarts on failure"

Writing unit files is something Morpheus is good at, but read the file it produces once. Your server, your responsibility.


4. Nginx reverse proxy#

Your app listens on 3000. Visitors arrive on 443. Nginx connects the two.

sudo apt install -y nginx     # or: sudo dnf install -y nginx
sudo nano /etc/nginx/sites-available/myapp
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 Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Do not expose port 3000

Your app should listen on 127.0.0.1:3000 only. Opening 3000 in the firewall makes Nginx and the certificate you are about to install bypassable. Only 80 and 443 should be open:

sudo ufw allow 80,443/tcp
sudo ufw enable

5. HTTPS certificate#

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run

Certbot edits your Nginx config and installs the auto-renewal timer itself.

With Morpheus

"get a Let's Encrypt certificate for example.com and set up auto-renewal"


6. Shipping updates#

cd /var/www/myapp
git pull
npm ci
npm run build
sudo systemctl restart myapp

Teach Morpheus this as a habit

"from now on when I say 'update the app': run git pull, npm ci, npm run build in /var/www/myapp and restart the myapp service"

Morpheus stores this as a procedure. Next time "update the app" is enough.


Checklist#

  • [ ] dig +short example.com returns your server's IP
  • [ ] systemctl status myappactive (running)
  • [ ] sudo nginx -tsyntax is ok
  • [ ] https://example.com opens with the padlock
  • [ ] sudo ufw status → only 22, 80, 443 open
  • [ ] The site comes back on its own after a reboot
  • [ ] sudo certbot renew --dry-run passes

If something did not work: When something breaks.