Deploying to a server
Deployment means moving the application to a server where it runs around the clock and is reachable at a domain. A Node.js application needs three things: production mode, a process manager that restarts the server after a crash, and a reverse proxy — a web server that receives requests from the internet and passes them to the application.
Production mode
The NODE_ENV=production environment variable switches Express into production mode: templates are cached, and the default error page does not show the stack trace. Our src/config.js reads it too, to enable secure cookies and a detailed log.
Preparing the server
The server needs Node.js 22 or newer. Copy the project with Git and install only the production dependencies:
git clone https://github.com/anna/blog.git
cd blog
npm ci --omit=devnpm ci installs exactly the versions recorded in package-lock.json, and --omit=dev skips development packages such as supertest. Create the .env file on the server by hand, with new long secrets.
The pm2 process manager
If you run npm start in a terminal, the server stops together with the SSH session and does not come back after a crash. pm2 keeps the application running:
npm install --global pm2
NODE_ENV=production pm2 start src/server.js --name blog --node-args="--env-file=.env"
pm2 save
pm2 startuppm2 save remembers the list of applications, and pm2 startup prints a command that, once run, makes pm2 start together with the server.
| Command | What it does |
|---|---|
pm2 status | List of applications and their state |
pm2 logs blog | The application log in real time |
pm2 restart blog | Restart, for example after a code update |
pm2 stop blog | Stop |
The nginx reverse proxy
The application listens on port 3000 only on the server itself, while nginx accepts requests on ports 80 and 443, handles HTTPS certificates and passes requests on. The site file, for example /etc/nginx/sites-available/blog:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}After enabling the site and reloading nginx, the blog opens at http://example.com. A free HTTPS certificate is issued by Let's Encrypt via the certbot utility.
Trusting the proxy
Now every request reaches Express from nginx, and req.ip shows 127.0.0.1. Tell Express that there is one proxy in front of it that can be trusted:
// src/app.js
app.set('trust proxy', 1);Express starts taking the visitor's address and protocol from the X-Forwarded-For and X-Forwarded-Proto headers. Without this, rate limiting by IP and cookies with the secure flag do not work.
Updating
git pull
npm ci --omit=dev
pm2 restart blogCommon mistake
Do not run a Node.js application as the root user and do not expose port 3000 to the outside. The application must be reachable only through nginx: that way all traffic goes over HTTPS, and a bug in the code does not give an attacker administrator rights on the server.