Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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=dev

npm 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 startup

pm2 save remembers the list of applications, and pm2 startup prints a command that, once run, makes pm2 start together with the server.

CommandWhat it does
pm2 statusList of applications and their state
pm2 logs blogThe application log in real time
pm2 restart blogRestart, for example after a code update
pm2 stop blogStop

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 blog

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

Thanks for reading!

Donate to the site — we work hard for you.