No dashboards, no CI pipelines to babysit: push to a branch, and the server updates itself with zero downtime. Here's the full setup — PM2, a deploy hook and Nginx.
PM2 keeps your app alive, restarts it on crash and survives reboots. Start once, then save the process list and hook it into boot:
npm install -g pm2 pm2 start app.js --name myapp pm2 save && pm2 startup
From here on, deploys are pm2 reload — rolling restarts, no dropped connections.
On the server, create a bare repo outside the web root. Its post-receive hook checks out the code, installs deps and reloads — every push deploys:
#!/bin/bash TARGET=/var/www/myapp GIT_DIR=/opt/repos/myapp.git git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f cd $TARGET && npm install --production pm2 reload ecosystem.config.js --env production
Then locally: git remote add live ssh://user@server/opt/repos/myapp.git and git push live main. That's the whole pipeline.
Never expose Node directly. Nginx handles TLS, compression and static files, and proxies only dynamic traffic — with the two headers everyone forgets:
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-Forwarded-For $proxy_add_x_forwarded_for;
}
Without Host and X-Forwarded-For, your app sees every visitor as the same IP — breaking rate limits, logs and geo logic. (Yes, we check this on every site we migrate.)
Database URLs, API keys and session secrets go in a .env file on the server, loaded at boot — and .env goes in .gitignore on day one. Leaked secrets in git history are forever; rotate anything that ever got committed.
Run a second copy on a staging URL with its own database. Push there first, click through, then push to production. Every WebDeps Business+ site includes a staging URL for exactly this.
Business and Cloud Pro include SSH, Git push deploys, Node/Python/PHP and staging URLs — or take a VPS with root and run the whole stack yourself. See VPS →