Do not use NODE_ENV for staging

Use a separate environment variable name to avoid NPM tricking you.

I write software locally, push it to remote Git server, where if the tests pass it gets deployed to staging environment. If the staging environment works correctly, then I will deploy the software to production. I often use NODE_ENV environment variable to flag these three environments. By default, the environment variable is unset and defaults to development

1
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

Depending on the NODE_ENV my program could load different settings: urls, logging parameters, server routes. Often, it is a YAML or a JSON file with environment names as keys

1
2
3
4
5
6
7
8
9
10
11
{
"development": {
"server": "http://localhost:1234"
},
"staging": {
"server": "https://staging.server.com"
},
"production": {
"server": "https://server.com"
}
}

When running on staging or production, I set NODE_ENV variable on the server to staging or production. This value then lets my code load right config for the environment. If NODE_ENV=production then npm install and npm ci install production NPM dependencies. So there is a catch:

  • On staging with NODE_ENV=staging npm install and npm ci will install production AND dev dependencies
  • Thus staging will NOT be exactly like production.

Staging would run just fine if one of the dependencies was saved as dev dependency by mistake, but the same application would crash in production because that dependencies would not be present.

Modifying npm install call with conditional to add --production flag when running on staging and production would create a nasty shell command. Luckily NPM thought about this. There is an additional environment variable we can set to install only the production dependencies on staging - it is NPM_CONFIG_PRODUCTION which acts just like --production during install step.

But watch out! Setting NPM_CONFIG_PRODUCTION=true during install overrides NODE_ENV for all npm scripts, which is what NPM intended. So the server will behave differently if you call node ./start.js or npm start.

1
2
3
NPM_CONFIG_PRODUCTION=true
NODE_ENV=staging
node ./start.js

Server starts with process.env.NODE_ENV=staging value. But if you have NPM script start that does the same in package.json the result will be different.

package.json
1
2
3
4
5
{
"scripts": {
"start": "node ./start.js"
}
}
1
2
3
NPM_CONFIG_PRODUCTION=true
NODE_ENV=staging
npm start

Server starts with process.env.NODE_ENV=production value!

We got burnt by this once - good thing we have noticed error reports from staging being written to the production dashboard, and figured why the staging server was running against production before any production data was corrupted.

There are two solutions.

1. Override process.env.NODE_ENV in every entry point with a different variable like FORCE_NODE_ENV

1
2
process.env.NODE_ENV =
process.env.FORCE_NODE_ENV || process.env.NODE_ENV || 'development'

You have to be 100% sure that every script - start.js, knex.js, db/migrations.js goes through the same override first. Otherwise some script might still be executed against production, which is ... less than ideal.

2. Use another variable to pick the environment settings, and leave NODE_ENV alone. For example, the variable SETTINGS could be development, staging or production, and NODE_ENV will be always be undefined or production.

1
2
3
4
process.env.SETTINGS = process.env.SETTINGS || 'development'
if (process.env.NODE_ENV === 'production') {
console.log('Running production-like with settings %s', process.env.SETTINGS)
}

The second method is my preferred one - but it might not be supported by every config-loading library. So find a config library that does allow you to specify a different variable from NODE_ENV. For example config allows using NODE_CONFIG_ENV to specify environment to load.