Backend Development 9 min read

Mastering .env Files: Secure Node.js Config with dotenv and dotenvx

This guide explains why .env files are used, how to install and configure the dotenv package, handle multiple environments, expand variables, and secure secrets with dotenvx, providing clear code examples and best‑practice recommendations for Node.js developers.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Mastering .env Files: Secure Node.js Config with dotenv and dotenvx

When protecting API keys or any secrets you don’t want to expose in an open‑source project, developers typically rely on a .env file parsed by the dotenv package, a library downloaded by over 31k developers weekly and designed around the Twelve‑Factor App principle of separating configuration from code.

The filename does not have to be exactly .env ; any name works, but a leading dot makes the file hidden on most operating systems (e.g., .ssh , .github , .vscode ).

Getting Started

First, install the package in your Node.js project:

<code>npm install dotenv</code>

Create a .env file at the project root. Each line defines an environment variable in KEY=VALUE format, for example:

<code># .env file
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
SECRET_KEY=mysecretkey</code>

Note: The .env file should usually be excluded from version control (e.g., add /.env to .gitignore ).

In your entry file ( app.js or index.js ), load and configure dotenv as one of the first operations:

<code>require('dotenv').config();
// Now you can access variables via process.env
console.log(process.env.PORT); // 3000
console.log(process.env.DB_HOST); // localhost</code>

If you need separate files for development, testing, or production, specify the path with the config options:

<code>require('dotenv').config({ path: './config/.env.dev' });</code>

For variable substitution, use dotenv-expand :

<code># .env file
HOST=localhost
PORT=3000
FULL_URL=http://${HOST}:${PORT}</code>
<code>const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const myEnv = dotenv.config();
dotenvExpand.expand(myEnv);
console.log(process.env.FULL_URL); // http://localhost:3000</code>

Source Implementation

The core of dotenv resides in a single file: main.js . It parses the .env file into key‑value pairs and populates process.env using a regular expression that handles quotes, line breaks, and comments.

Key‑Value Parsing

<code>const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg</code>

Encryption / Decryption

Since version 16.1.x, dotenv added decryption support for higher‑security projects. The following function demonstrates AES‑256‑GCM decryption:

<code>function decrypt(encrypted, keyStr) {
  const key = Buffer.from(keyStr.slice(-64), 'hex');
  let ciphertext = Buffer.from(encrypted, 'base64');
  const nonce = ciphertext.subarray(0, 12);
  const authTag = ciphertext.subarray(-16);
  ciphertext = ciphertext.subarray(12, -16);
  try {
    const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce);
    aesgcm.setAuthTag(authTag);
    return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
  } catch (error) {
    // handle decryption error
    throw error;
  }
}</code>

The decryption uses AES‑256‑GCM, which provides both confidentiality and integrity verification. Encryption itself is not built into dotenv ; it relies on the external tool dotenvx ( dotenvx docs ).

dotenvx is an extension of dotenv that adds professional encryption and other advanced features for higher‑security scenarios.

You can generate an example encrypted env file with:

<code>dotenvx ext genexample</code>

Flexible Configuration Entry

The configDotenv function lets you set custom config file paths and includes a built‑in debug mode that outputs detailed information via the _debug helper.

<code>function configDotenv(options) {
  const dotenvPath = path.resolve(process.cwd(), '.env');
  let encoding = 'utf8';
  const debug = Boolean(options && options.debug);
  if (options && options.encoding) {
    encoding = options.encoding;
  }
  // Load and parse .env files
  let optionPaths = [dotenvPath];
  if (options && options.path) {
    // custom path handling logic
  }
  let lastError;
  const parsedAll = {};
  for (const path of optionPaths) {
    try {
      const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }));
      DotenvModule.populate(parsedAll, parsed, options);
    } catch (e) {
      if (debug) {
        _debug(`Failed to load ${path} ${e.message}`);
      }
      lastError = e;
    }
  }
  // Populate process.env
  DotenvModule.populate(processEnv, parsedAll, options);
  return { parsed: parsedAll, error: lastError };
}
</code>

Conclusion

Beyond basic loading, dotenvx offers encryption, example generation, and the ability to run a Node.js script with a specific env file without manually invoking dotenv :

<code>dotenvx run -f .env.production -- node index.js</code>

Key takeaways:

Never commit unencrypted .env files to a repository unless you are certain they contain no sensitive data.

Maintain separate files for different environments (e.g., .env.development , .env.production ).

Set appropriate file permissions to prevent unauthorized access.

backendconfigurationNode.jssecurityEnvironment Variablesdotenv
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.