API, Next.js or Express site, Discord bot: a Node.js application is not launched with a simple node index.js on a server. It has to restart after a crash, start again when the VPS boots, keep its logs, and be published over HTTPS. PM2 handles the first three points, Nginx the last.
1. Install Node.js
The Node.js version shipped by Ubuntu and Debian is often old. Install the current LTS (long-term support) version from the NodeSource repository, then check:
curl -fsSL https://deb.nodesource.com/setup_lts.x -o nodesource_setup.sh
bash nodesource_setup.sh
apt install -y nodejs
node -v && npm -v
Always use an LTS version in production: intermediate versions are only maintained for a few months.
2. Deploy the code
Run the application under a dedicated user, never as root. Fetch the code, ideally from a Git repository, then install the dependencies:
adduser --disabled-password --gecos "" app
su - app
git clone https://github.com/your-account/your-project.git myapp
cd myapp
npm ci --omit=dev
npm ci installs exactly the versions in package-lock.json, which guarantees the server uses the same dependencies as your machine. If the project needs building, such as a Next.js or TypeScript application, install all dependencies and then run npm run build.
Put secrets, such as a bot token or database credentials, in a .env file outside the Git repository, readable by this user only: chmod 600 .env.
3. Launch with PM2
Install PM2 globally, as root, then start the application as the app user:
npm install -g pm2
su - app
cd myapp
pm2 start index.js --name myapp
pm2 status
If the application crashes, PM2 restarts it immediately. For a project started with an npm script, use pm2 start npm --name myapp -- start.
4. Automatic restart when the VPS boots
Still as the app user, ask PM2 for the command that registers it as a system service:
pm2 startup systemd
PM2 prints a command starting with sudo env PATH=...: copy it and run it as root. Then, as app again, save the list of applications to restart:
pm2 save
Reboot the VPS once to check that the application comes back on its own. Remember to run pm2 save again after adding or removing an application.
5. One configuration file to describe everything
Rather than long command lines, describe your applications in ecosystem.config.js at the root of the project:
module.exports = {
apps: [{
name: "myapp",
script: "index.js",
instances: 1,
max_memory_restart: "400M",
env: { NODE_ENV: "production", PORT: 3000 }
}]
};
pm2 start ecosystem.config.js
pm2 save
max_memory_restart restarts the application if it exceeds the given memory, a useful safeguard against memory leaks. For a stateless API, instances: "max" and exec_mode: "cluster" start one copy per CPU core. Do not do this for a Discord bot: each copy would connect separately and reply twice.
6. Logs
pm2 logs myapp --lines 100 # live logs
pm2 monit # live CPU and memory
By default, PM2 keeps logs without limit. Install its rotation module so they do not fill the disk:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 20M
pm2 set pm2-logrotate:retain 7
7. Publish the application with Nginx and HTTPS
A Discord bot does not need this step: it connects to Discord itself. For an API or a website, make the application listen on the local address only, for example 127.0.0.1:3000, and put Nginx in front. Create /etc/nginx/sites-available/api.mysite.com:
server {
listen 80;
server_name api.mysite.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
ln -s /etc/nginx/sites-available/api.mysite.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d api.mysite.com --redirect
With Express, add app.set("trust proxy", 1) so the application sees the visitors' real address rather than Nginx's. Certificate details in Nginx and HTTPS.
8. Update the application
su - app
cd myapp
git pull
npm ci --omit=dev
pm2 reload myapp
pm2 reload restarts instances one by one in cluster mode, without downtime. In single mode, it amounts to a quick restart. For safer deployments, containerise the application: see Docker and Docker Compose.
Frequently asked questions
How do I host a Discord bot 24/7?
On a VPS, start it with PM2 as in this guide, then run pm2 startup and pm2 save: the bot restarts automatically after a crash or a server reboot. A bot needs neither Nginx nor an open port, since it connects to Discord itself.
PM2 or systemd?
Both work. PM2 is handier for Node.js projects: logs, monitoring, cluster mode and zero-downtime reloads in one command. systemd is enough for a single application and avoids an extra dependency.
Which Node.js version should I use in production?
Always an LTS version, whose number is even: it receives security fixes for about three years. Avoid odd-numbered versions, which are only maintained for a few months.
How much memory does a Node.js application need?
A simple Discord bot or a small API uses between 50 and 200 MB. A Next.js application in production needs more like 300 MB to 1 GB. A 2 GB VPS comfortably hosts several small projects.



