A better way to manage NodeJS configuration flags?

Viewed 1707

I have a NodeJS application to which I need to provide various flags to customise the environment.

I'm already handling environment files with dotenv and I'm preloading it to the app from the CLI as well:

"scripts": {
  "hello": "DOTENV_CONFIG_PATH=/my/path/dev.env node --max_semi_space_size 64 --inspect=0.0.0.0 dist/index.js"
}

My question is: I'm starting to hit a situation where I need to add more and more nodejs flags in some cases - is there a better way to do it? I can't imagine passing 20 cli args (i.e. --max_semi_space_size ... to node in the future.

Some options I've thought/heard of are:

  • moving the script logic to external file
  • aliasing the node command with node + flags
  • recompiling node with my custom settings

But it feels odd - I'm sure there's a way for handling that which isn't so hacky. How would you tackle that? I'm on docker too, if that makes any difference in your suggestions.

ta!

3 Answers

The best practice is to create a mechanism that is reusable and can be changed on multiple levels. Many docker containers that are aimed for wide use follow this approach.

Hierarchical configuration

The module needs to support hierarchical configuration. What this means is that there will be more than one way to set-up variable and there will be a strict hierarchy, based on priority, which value will be the final value. Best practice for the hierarchy is as follows:

  1. Set the value to a default value
  2. Check if there is new value in the configuration file, and if so overwrite the value
  3. Check if there is new value from the environment variables, and if so overwrite the value
  4. Check if there is there is new value in the startup flags, and if so overwrite the value.

This approach is applicable to the node CLI arguments, with the exception that the hierarchy is shallower and stops at the environment variable setup.

After this chain of checks, you will have the final value for the variable.

Application configuration variables

Node.js has a good library called nconf that you can find on github. The library is allowing you to do hierarchical configuration. Here is simple example (taken from the examples in the library, trimmed for space)

var nconf = require('nconf');

  // 1. any overrides
  nconf.overrides({
    'always': 'be this value'
  });

  // 2. `process.env`
  // 3. `process.argv`
  nconf.env().argv();

  // 4. Values in `config.json`
  nconf.file('/path/to/config.json');

  // 5. Any default values
  nconf.defaults({
    'if nothing else': 'use this value'
  });

Wrap this in your own module and add the additional logic you need for the handling of the configurations

var myConfig = require('nconf');
..
...
..
module.exports = myConfig;

then use the configuration from your code as follows

var config = require('./myConfig');
config.get('hello');

This will allow you to set the default configurations in the image and overwrite them in subsequent images. Here is an example, the first image base will have the hello flag set from the file during the build and the second image will use ENV to overwrite the setting:

FROM debian:latest as base

COPY ./hello_config.json /path/to/config.json
...
...

FROM base AS second

ENV hello="Hello world from second image!"
...
...

Now when you create a container, you can overwrite the changes either by passing new environment file (--env-file) or a singe flag (-e or --env)

docker run --env hello="Hi from container!" --env-file ./new_conf.list secod

Note that the environment file should be in a shell format FLAG=VALUE and not in json format.

Node CLI Arguments

The Node CLI arguments can also be handled with this approach (the only difference is that the hierarchy is stops at the CLI). The container should define the NODE_OPTIONS environment variable with the required flags in the Dockerfile as follows:

...
ENV NODE_OPTIONS="--max_semi_space_size 64 --inspect=0.0.0.0"
...

This can be enhanced to accept build option as well as to build the variable gradually

...
ARG NODE_OPTIONS=""
ENV NODE_OPTIONS="${NODE_OPTIONS} --max_semi_space_size 64"
ENV NODE_OPTIONS="${NODE_OPTIONS} --inspect=0.0.0.0"
...

now, during the build we can specify additional options as follow

docker build --build-arg NODE_OPTIONS="--cpu-prof --heapsnapshot-near-heap-limit=3" .

Same as the application configuration, you can use the -env or --env-file file in order to modify it for specific container.

From my point of view the package.json file should be as more clean as it possible. Usually for such cases I create scripts or shell directory, and then put all scripts into the separate bash or shell files. And finally just invoke this files from npm commands.

Something like this: Running bash scripts with npm

I'm not sure that usage of nconf for this case is good choice..

I think this could've meet your needs: https://nodejs.org/api/cli.html#cli_node_options_options

NODE_OPTIONS=options...

A space-separated list of command line options.

...

NODE_OPTIONS='--require "./a.js"' node --require "./b.js"
# is equivalent to:
node --require "./a.js" --require "./b.js"

...except that:

Node.js will exit with an error if an option that is not allowed in the environment is used

and --max_semi_space_size is not listed among allowed options... but you can try.

But anyway

"scripts": {
  "hello": "./run_node_with_options.sh"
}
# run_node_with_options.sh
DOTENV_CONFIG_PATH=/my/path/dev.env node --max_semi_space_size 64 --inspect=0.0.0.0 dist/index.js"

seems like a more robust solution.

Related