Node.js is a popular runtime to write apps for. These apps are often production quality apps that are used by many people. To make maintaining them easier, we’ve to set some guidelines for people to follow.
In this article, we’ll look at deployment and security-related best practices for Node apps.
Set NODE_ENV=production
We should set NODE_ENV
to production
for production environments and development
for development. Setting NODE_ENV
to production
makes production-related optimizations activate.
Omitting this single property may degrade performance significantly. For instance, using Express without NODE_ENV
set to production
makes it slower by a factor of 3.
In Express, if NODE_ENV
is production
, it’ll cache view templates and CSS generated from CSS extensions, and it’ll also generate less verbose error messages to keep them from being exposed to the public.
Design Automated, Atomic and Zero-Downtime Deployments
Deployments need to be automated so that we don’t have to worry about it once the automated deployment pipeline is set up. This way, we don’t have to worry about running into issues caused by human error. Manual deployments are also much slower than automatic.
Even better, we can create automated sandbox environments with Docker so that each app runs in its own environment. This solves lots of issues with conflicting runtimes, packages, and environment variables.
Otherwise, we have to deploy manually every time, which may cause issues. Also, we have to watch it to make sure that it’s done, and so people are less likely to deploy apps since it’s such a painful and error-prone process.
Use an LTS Release of Node.js
Long Term Service (LTS) releases of Node are supported for much longer than non-LTS Node versions. They constantly receive critical bug fixes, security updates, and performance improvements. Non-LTS releases are only supported for a few months after its releases with updates.
Therefore, to avoid having to upgrade Node all the time, we should use the LTS versions.
Embrace Linter Security Rules
ESLint plugins like eslint-plugin-security
checks for security issues with our code so that we can fix them as early as possible. This helps catch security weaknesses like eval
, invoking a child process, or importing a module with user inputted string.
With it, we can check before our code is committed or pushed. With this, we also follow security best practices all the time by everyone in the team.
Limit Concurrent Requests Using a Middleware
Denial of Service (DOS) attacks are very popular and easy to do. Therefore, we should implement rate-limiting using a service like load balances, cloud firewalls, reverse proxy, or a package in our app.
To implement rate-limiting in our app endpoints, we can use packages like rate-limiter-flexible
or express-rate-limit
.
For instance, we can use express-rate-limit
as follows to add a rate limit to all the endpoints in our Express app:
const express = require('express');
const bodyParser = require('body-parser');
const rateLimit = require("express-rate-limit");
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(limiter);
app.get('/', (req, res, next) => {
res.send('hello');
});
app.listen(3000, () => console.log('server started'));
module.exports = app;
As we can see, we only have to add a simple middleware to prevent denial of service attacks on our app. Therefore, we should do it from the beginning for apps of any size.
Extract Secrets from Configuration Files or Use Packages to Encrypt Them
Configuration files often have secret data that shouldn’t be seen by most people. Therefore, we should never store them in our source code. Instead, we have to make sure of secret-management systems like Vault, Docker secrets, or use environment variables.
We should have pre-commit or push hooks to prevent accidentally committing secrets to code. Source control can be mistakenly made public, which exposes the secrets to other people that shouldn’t see them.
For example, we can use the dotenv
package to store environment variables for Node apps. To use it, we write:
const express = require('express');
const bodyParser = require('body-parser');
require('dotenv').config();
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.send(process.env.DB_HOST);
});
app.listen(3000, () => console.log('server started'));
Given that our .env
file has:
DB_HOST=foo
Once we ran:
require('dotenv').config();
Then process.env.DB_HOST
returns 'foo'
, so we’ll see foo
displayed on the screen if we go to /
.
Conclusion
We should make sure that NODE_ENV
is set to production
so that optimizations are applied for packages like Express.
Any deployment should be automated. Also, Node.js should be the LTS version. Also, we should check for security vulnerabilities in our code and also rate limit our API endpoints to prevent denial of service attacks.
Finally, we should keep secrets out of our code.