Think dotenv is the only way to load .env files in Node.js?
It used to be the default, but recent Node versions added native support and useful CLI flags.
In this post I’ll give the fastest practical steps to get environment variables into process.env, using dotenv, the node –env-file flag, and programmatic APIs like process.loadEnvFile.
You’ll see quick commands, when to skip dotenv, common pitfalls (paths, quoting, load order), and a minimal checklist so your app reads secrets correctly on startup.
Practical Steps to Load Environment Files in Node.js

Loading environment files in Node.js takes three minutes once you know where to start.
Most projects use the dotenv package. Install it with npm install dotenv in your project directory. At the top of your entry file (index.js or server.js, usually), add require('dotenv').config() for CommonJS or import 'dotenv/config' for ES modules. That call reads your .env file and dumps every variable it finds into process.env.
Node.js added native support for loading .env files in version 20.6. If your runtime’s recent enough, you can skip dotenv entirely. Create a .env file in your project root with one KEY=value pair per line. No commas, no semicolons. Launch your app with node --env-file .env index.js and Node loads those variables before your script runs. Access any variable by reading process.env.MY_KEY.
Here’s the fastest path when starting a new project:
- Create a .env file in the root with lines like
DATABASE_URL=postgres://localhost/mydb - Install dotenv:
npm install dotenv - Load it early:
require('dotenv').config() - Access variables anywhere:
const dbUrl = process.env.DATABASE_URL - Add .env to .gitignore so you don’t commit secrets
On Node 20.6 or later, replace steps 2 and 3 with one CLI flag: start your app using node --env-file .env index.js instead of plain node index.js. The flag tells Node to read and parse the .env file before executing your script, same as dotenv does.
Advanced Environment Variable Loading Workflows in Node.js

When the basic require('dotenv').config() call doesn’t match your file structure, you can pass options. Dotenv accepts a path option, like require('dotenv').config({ path: './config/.env.production' }), which tells it to read a file outside the project root or with a custom name. The encoding option lets you specify character encoding if your .env contains non-ASCII secrets or you’re troubleshooting weird characters.
Node’s native APIs give you two programmatic methods that replace dotenv. Call process.loadEnvFile('./.env') to load a file by path, or use const util = require('util'); const parsed = util.parseEnv('KEY=value\nSECRET=xyz'); to parse a string of environment variable declarations. Both are available in Node 20.12 and later. process.loadEnvFile is marked stable in Node 24. The main difference from dotenv is behavior on missing files: process.loadEnvFile() throws an exception if the file doesn’t exist, while dotenv silently continues. Wrap the call in try/catch if you want conditional loading, or use the --env-file-if-exists CLI flag to ignore missing files when launching via command line.
| Method | Example | Node Version Requirement |
|---|---|---|
| CLI flag | node –env-file .env index.js | Node 20.6+ |
| Programmatic load | process.loadEnvFile(‘./.env’) | Node 20.12+ (stable in Node 24) |
| Parse string | util.parseEnv(‘FOO=bar’) | Node 20.12+ |
Your .env file follows a simple format: KEY=value on each line, comments start with #, and quoted strings preserve whitespace like SECRET="value with spaces". Multiline values need explicit newline characters or external templating since standard .env parsers expect one variable per line. Watch out for stray spaces around the equals sign unless the value is quoted. KEY = value assigns the string ” value” with a leading space, not “value”.
Best Practices for Managing Env Files in Node.js Projects

Add .env to your .gitignore the moment you create it. That single line prevents you from accidentally committing API keys or database passwords. Create a .env.example file with placeholder values and commit that instead. New developers will know which variables the app expects. Your .env.example might look like DATABASE_URL=your_db_connection_string_here and API_KEY=your_api_key_here.
Validate required environment variables at startup rather than waiting for runtime errors deep in your application. Check for undefined values early in your entry file. Something like if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL not set'); catches missing config before the app tries to connect to a database or call an external API. Use descriptive variable names in all caps with underscores: DATABASEURL, STRIPESECRETKEY, LOGLEVEL.
Never log the actual values of secrets. If you need to confirm a variable loaded correctly, log a redacted version or just log that it exists. Don’t set fallback defaults for sensitive values like API keys. If a secret’s missing, fail fast and force the operator to provide it explicitly. For non-secret config like NODE_ENV or PORT, reasonable defaults like const port = process.env.PORT || 3000; are fine. Keep your production secrets in a dedicated secret manager like AWS Secrets Manager, HashiCorp Vault, or your cloud platform’s native secret store rather than relying on .env files on production servers.
Multiple Environment Files and Loading Order in Node.js

You can load more than one .env file by passing multiple –env-file flags when starting Node. Run node --env-file .env.local --env-file .env index.js and Node reads both files in the order you specified. If the same key appears in both files, the value from the file listed later on the command line wins. This pattern’s useful for keeping shared defaults in .env and developer overrides in .env.local, which you add to .gitignore.
Changing the flag order flips which file takes precedence. If you run node --env-file .env --env-file .env.local index.js, values in .env.local override those in .env. Reverse the flags, .env becomes the override. This ordering gives you control over layered config without editing files, which is handy when you want to test a single different value without changing your committed .env file.
When using multiple env files, follow these rules to avoid surprises:
- List the most general file first, most specific file last (like .env then .env.local)
- Document the expected load order in your README so teammates know which file wins
- Use –env-file-if-exists for optional override files that might not exist in every environment
- Remember that
process.loadEnvFile()throws if a file’s missing, so wrap it in try/catch when loading optional files programmatically
Troubleshooting Env Loading Issues in Node.js

When process.env.MY_VAR returns undefined, the first thing to check is whether you actually loaded the .env file. If you’re using dotenv, confirm require('dotenv').config() runs before any code that reads the variable. If you’re using the native –env-file flag, make sure you restarted your process with that flag. Node won’t reload the file mid-execution.
Wrong file paths cause silent failures with dotenv and loud failures with process.loadEnvFile(). Dotenv defaults to reading .env in the current working directory, not necessarily the directory where your script lives. If you run node src/index.js from the project root, dotenv looks for .env in the project root. But if you run cd src && node index.js, it looks in the src directory. Specify an absolute path or a path relative to the script location when the default behavior doesn’t match your structure.
Quoting and whitespace trip people up frequently. A line like SECRET=my secret value assigns “my” to SECRET and ignores the rest of the line in most parsers, because spaces without quotes act as delimiters. Write SECRET="my secret value" or SECRET='my secret value' to capture the full string. If variables still look wrong, check for byte-order marks (BOM) at the start of the file, especially if you created the .env on Windows. Save the file with UTF-8 encoding without BOM.
If you’re using native Node loading and getting errors about missing files:
- Confirm your Node version with
node -v. CLI –env-file requires Node 20.6+, programmatic loading requires Node 20.12+ - Use –env-file-if-exists instead of –env-file to tolerate missing files
- Wrap
process.loadEnvFile()in try/catch when the file might not exist in certain environments - Restart your Node process after editing .env, because environment variables are read at startup and won’t refresh automatically
Native Node.js Alternatives to Dotenv (Modern Approach)

Node introduced built-in .env file support to eliminate the need for dotenv in most projects. The –env-file CLI flag arrived in Node 20.6, replacing the old -r dotenv/config preload trick. process.loadEnvFile() and util.parseEnv() became available in Node 20.12, giving you programmatic alternatives to dotenv.config() and dotenv.parse(). By the time Node 24 shipped, process.loadEnvFile() was marked stable, meaning the API won’t change in future releases.
Migrating from dotenv to native Node is straightforward. Change your npm start script from node -r dotenv/config index.js to node --env-file .env index.js. Replace every require('dotenv').config() call in your code with process.loadEnvFile('./.env'), and replace dotenv.parse(buffer) with util.parseEnv(buffer.toString()). Run your tests to confirm process.env values still populate correctly. Then uninstall dotenv with npm uninstall dotenv.
| Feature | Dotenv Equivalent | Node Native Equivalent |
|---|---|---|
| CLI preload | node -r dotenv/config index.js | node –env-file .env index.js |
| Programmatic load | require(‘dotenv’).config() | process.loadEnvFile(‘./.env’) |
| Parse buffer | dotenv.parse(buffer) | util.parseEnv(buffer.toString()) |
The main behavioral difference to watch for: dotenv silently continues when the .env file’s missing, while process.loadEnvFile() throws an exception. If your app relies on optional .env files, wrap the native call in a try/catch block like try { process.loadEnvFile('./.env'); } catch {} or use the –env-file-if-exists flag at the CLI. Most production apps inject environment variables through the platform anyway, so this difference only affects local development where you can control whether the file exists.
Environment Variables in Real-World Node.js Apps

Express servers and database clients are the most common places you’ll see environment variables in action. A typical Express app pulls the server port from process.env.PORT so it can run on any port a hosting platform assigns, falling back to 3000 for local development. Database connection strings live in .env as DATABASE_URL=postgres://user:password@localhost:5432/mydb, and your ORM or client library reads that value at startup. Missing the –env-file flag when you launch the server causes connection errors because the URI variable is undefined and the MongoDB or Postgres client tries to connect to a malformed address.
ORMs like TypeORM and Sequelize expect environment variables for connection config. TypeORM reads from process.env to build a DataSource, so you’d store DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, and DB_NAME in your .env file and reference them in your ormconfig or DataSource options. Sequelize follows the same pattern. Load the .env file before initializing the ORM, otherwise the client throws errors about missing credentials.
API clients for Stripe, Twilio, SendGrid, and similar services expect secret keys stored in environment variables. Your .env might include STRIPE_SECRET_KEY=sk_test_... and SENDGRID_API_KEY=SG..... These values never belong in source code because anyone with repo access would have full access to your production services. Keep development keys in .env, production keys in your hosting platform’s secret manager, and rotate keys immediately if you accidentally commit one.
CI/CD, Containers, and Production Env Loading Strategies

In production, environment variables come from your hosting platform or orchestrator, not from .env files you deploy. Heroku, Vercel, Netlify, and AWS all provide web consoles or CLI commands to set environment variables that your app reads at runtime. GitHub Actions and GitLab CI let you define secrets in the repository settings, and your pipeline injects them as environment variables during builds and deployments. This keeps secrets out of your codebase and lets you rotate keys without redeploying code.
Docker containers treat environment variables as runtime config. Use ENV instructions in your Dockerfile to set default values, but pass sensitive values at runtime with docker run -e DATABASE_URL=... myimage or through docker-compose environment sections. The ENV instruction bakes values into the image, which is fine for non-secret config like NODE_ENV=production but unsafe for API keys. ARG instructions supply build-time variables that don’t persist in the final image, useful for passing build flags or version numbers but not for runtime secrets.
When you deploy to Kubernetes, define environment variables in your pod spec or pull them from ConfigMaps and Secrets. Secrets are base64-encoded and mounted as environment variables or files, keeping sensitive data separate from your deployment YAML. Most CI/CD platforms follow similar patterns: define secrets in the platform UI, reference them by name in your pipeline config, and let the platform inject them at runtime.
Follow these production guidelines to keep environment variables secure and manageable:
- Never commit .env files to version control, even in private repositories
- Use your platform’s secret manager (AWS Secrets Manager, Google Secret Manager, Azure Key Vault) for sensitive values
- Inject environment variables at container or process startup, not during image build
- Rotate secrets regularly and automate rotation where possible
- Audit who has access to production environment variables and restrict access to the smallest necessary group
Final Words
In the action, we walked through practical loading techniques—installing dotenv, require(‘dotenv’).config(), and the newer –env-file and process.loadEnvFile() options in Node.
We covered advanced workflows (custom paths, parsing quirks), best practices (.gitignore, .env.example), multiple-file precedence, troubleshooting checks, and production approaches for CI/CD and containers.
Try a quick test (npm i dotenv; require(‘dotenv’).config() or node –env-file .env) to see how to load env files in nodejs in minutes. You’ll be set to manage configs safely and avoid last-minute fires.
FAQ
Q: How do I load a .env file in Node.js?
A: To load a .env file in Node.js, install dotenv (npm install dotenv) then require(‘dotenv’).config() or import ‘dotenv/config’. Native Node also supports –env-file flags on Node 20.6+.
Q: How do I access environment variables in Node.js?
A: You access environment variables in Node.js via process.env.YOUR_KEY, for example process.env.PORT. Treat values as strings and provide fallbacks or validation for required variables.
Q: What native Node options replace dotenv?
A: Native Node provides –env-file and –env-file-if-exists (Node 20.6+), and process.loadEnvFile() plus util.parseEnv() (Node 20.12+), which mirror dotenv functionality without external deps.
Q: How do I load env files from a custom path or programmatically?
A: To load a custom path use dotenv.config({ path: ‘./config/.env’ }) or programmatically call process.loadEnvFile(‘./config/.env’) (Node 20.12+), and use try/catch for missing-file safety.
Q: What’s the precedence when loading multiple .env files?
A: When loading multiple files, the later file overrides earlier ones. Example: node –env-file .env.local –env-file .env index.js makes .env.local take precedence over .env.
Q: What are best practices for managing .env files?
A: Best practices: add .env to .gitignore, keep a .env.example for docs, validate required vars at startup, avoid duplicate keys, and use platform secret stores in production.
Q: How do I troubleshoot env loading issues?
A: To troubleshoot env loading, check file path and Node version, restart the process after changes, inspect encoding/BOM, verify quoting and whitespace, and try –env-file-if-exists to avoid throws.
Q: How should env variables be handled in production and CI/CD?
A: In production, inject env values via CI/CD or hosting secrets. Use Docker ENV for runtime, ARG for build-time, and Kubernetes Secrets/ConfigMaps instead of committing .env files.
Q: How do I migrate from dotenv to native Node env loading?
A: To migrate, remove dotenv.config(), use –env-file flags or process.loadEnvFile(), ensure your Node version meets requirements, and wrap native calls with try/catch where needed.
Q: What are .env quoting rules and multiline caveats?
A: .env files use KEY=value rows, support quotes for values, comments start with #, and multiline values are tricky—prefer single-line or explicit escaping to avoid parsing issues.
