Applications are commonly required to deploy to many different environments, including staging, testing, and production, without building environment-specific artifacts.
Methodologies like the 12 Factor App specify that an application’s code and configuration are separate but combine during deployment to accommodate specific environments.
Environment variables are one of the preferred methods to define and consume environment-specific configuration values because they are supported by all major operating systems.
They can be defined in most cloud-provider Platform as a Service (PaaS) offerings, and are a common method for configuring platforms, like Docker.
However, it is sometimes convenient to define application settings in a local configuration file instead.
For example, when developing and debugging applications locally, especially when working across many feature branches, the ability to define a set of environment variables in a file streamlines a developer’s experience.
.env
files provide a popular solution for defining environment variables, particularly in Node.js. Platforms like Heroku, for instance, use .env
files as part of their recommended best practices.
In this post, we’ll look at a simple Node.js application configured via environment variables, and explore how to customize an .env
file with new environment variables that override default values.
We’ll then demonstrate how to use multiple customized .env
files to quickly switch between multiple environment-specific configurations.
Prerequisites
The sample application we’ll be working through is available on GitHub. To run the application locally, ensure Node.js is installed.
Alternatively, you can run the application in CodeSandBox. Click the link to open the GitHub
source code in a new sandbox to build, run, and expose via a random URL.
The sample Node.js application
The following code shows a simple Node.js web server configured with two environment variables: PORT
, which defines the port the web server listens on, and MYNAME
, which defines the name returned in the HTTP response:
const http = require('http');const port = parseInt(process.env.PORT, 10) || 5000;const name = process.env.MYNAME || "Matthew"http.createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.write('Hello, ' + name + '!'); response.end();}).listen(port);
Start by importing the http
module:
const http = require('http');
We can attempt to parse an integer value from the PORT
environment variable, or default to Port 5000 if the environment variable is not defined or contains something other than an integer:
const port = parseInt(process.env.PORT, 10) || 5000;
The name returned in the HTTP response can be found in the MYNAME
environment variable or defaults to the string Matthew
:
const name = process.env.MYNAME || "Matthew"
With a web server started, it listens to the port we just defined:
http.createServer((request, response) => { // ...}).listen(port);
The HTTP response is constructed in the function passed to the createServer()
function. Here, we return the name defined above:
response.writeHead(200, { 'Content-Type': 'text/plain' }); response.write('Hello, ' + name + '!'); response.end();
Now, we can run the application with the following:
node index.js
Open http://localhost:5000 to see the Hello, Matthew!
response, which means our Node.js application is running correctly.
Why do we need to override environment variables?
Why go through the trouble of overriding values, like port numbers, with environment variables in the first place?
These values can already pass in as command line arguments or load from JSON or YAML configuration files, providing more flexibility than environment variables exposing a simple name and value pairs.
However, because reading values from environment variables are often required by PaaS solutions, the PORT
environment variable became a de facto standard for defining which port an application listens to.
For example, Heroku, AWS, Azure, and Google Cloud all require Node.js applications deployed to their services to listen to the port defined by the PORT
environment variable.
It’s also often required to use environment variables explicitly defined in methodologies like the Twelve-Factor App methodology.
In the post’s introduction, we noted that this methodology requires defining an application’s configuration outside the code; the Twelve-Factor App config section goes into more detail:
The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard. – “The Twelve-Factor App,” Adam Wiggins
By configuring our application via environment variables, we can be sure our code is easy to deploy and customize across a wide range of platforms.
Setting environment variables in Node.js
A Node.js application deployed to a PaaS solution must typically assume the app is listening to a port at a random number. We can demonstrate this locally by setting the PORT
environment variable to a value other than 5000.
We’ll also set the MYNAME
environment variable to demonstrate how to define an application-specific configuration.
In Linux and macOS, we can define these environment variables like this:
PORT=5001 MYNAME=Jane node src/index.js
In Windows PowerShell, we can define the environment variables like the following:
$env:PORT="5001"$env:MYNAME="Jane"node src\index.js
This process is manageable for our two environment variables but quickly becomes tedious if we must define dozens of environment variables each time the application runs. Defining environment variables in an .env
file provides a convenient solution.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to use the React children prop with TypeScript
- Explore creating a custom mouse cursor with CSS
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Loading .env
files in Node.js
Node.js doesn’t natively load .env
files, so we must use the dotenv
package to load the file and expose the values through process.env
.
Start by adding dotenv
as a project dependency in the package.json
file under the dependencies
property:
{ "name": "node-env-file-demo", "version": "1.0.0", "description": "Node.js example loading .env files", "main": "src/index.js", "scripts": { "start": "nodemon src/index.js" }, "dependencies": { "dotenv": "10.0.0" }, "devDependencies": { "nodemon": "1.18.4" }, "keywords": [] }
Next, download the dependency with the following:
npm install
To load the .env
file, we must load the dotenv
package and call the configure()
function at the start of the index.js
file:
require('dotenv').config();const http = require('http');const port = parseInt(process.env.PORT, 10) || 5000;const name = process.env.MYNAME || "Matthew"http.createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.write('Hello, ' + name + '!'); response.end();}).listen(port);
Our application is now ready to load .env
files.
Defining custom environment variables in Node.js
We can now create the .env
file in the project’s root directory.
Customizing an .env
file involves defining the environment variable name we want to override at the start of each line, followed by =
and the variable’s value. In the example below, we defined new values for the PORT
and MYNAME
environment variables:
PORT=5001MYNAME=Jane
Now, run the application with the following command:
node src/index.js
Next, open http://localhost:5001; notice the port changed and the message Hello, Jane!
returns.
Preloading dotenv
An alternative to calling require('dotenv').config()
in our code is to use the -r
or --require
command line option to preload dotenv
:
node -r dotenv/config src/index.js
This approach injects environment variables into a Node.js application, that otherwise does not support .env
files, without editing the original source code.
Using multiple .env
files in Node.js
To allow developers to quickly swap between many customized environment files during development, we can configure dotenv
to load environment variables from a custom file via the DOTENV_CONFIG_PATH
environment variable.
To demonstrate this, create a file called .env.development
in the project’s root directory with the following contents:
PORT=5002MYNAME=Jill
Then, set the DOTENV_CONFIG_PATH
environment variable to .env.development
and run the application using the preload method we just covered.
On Windows PowerShell, run the application with these commands:
$env:DOTENV_CONFIG_PATH=".env.development"node -r dotenv/config src/index.js
On Linux or macOS, run the application with this command:
DOTENV_CONFIG_PATH=.env.development node -r dotenv/config src/index.js
Our application is now available at http://localhost:5002, and will return Hello, Jill!
.
Excluding .env
files from Git
Because .env
files often contain sensitive information, like database connection strings, we don’t want to commit those values to a Git repository. If we did, we would be sharing our passwords with anyone who can access the application source code.
Environment variables neatly avoid this problem because, by their very nature, their values are not defined in a file. However, we must specifically exclude .env
files from our Git repository to prevent sharing them.
We can do this by creating a file called .gitignore
in the project’s root directory and adding the following line:
.env*
Now, Git ignores the .env
file and any other files starting with .env
like .env.development
, meaning any sensitive information will not end up in our Git repository.
Conclusion
Including environment variables in an .env
file is a convenient way to define many related configuration values without managing them as part of your operating system or defining them on the command line each time an application runs.
200’s only
Monitor failed and slow network requests in production
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.