Configuring Environment Variables for All Netlify Environments

One of the very first questions I asked when getting into the Netlify stack was around configuration options when using multiple environments (local dev, staging, prod). This ended up being my first post on The Community long before I become a Community Pilot, but the answer can still be difficult to sniff out. The idea here is that I want ENV vars configured on a per-environment basis without having to change source code per environment. I've been spoiled by Rails for local dev and Heroku's hosting, where ENV var management is a piece of cake.

The good news is that it's totally doable on Netlify right now. There are two main routes to go, each with a slight tradeoff from the other, both leveraging Netlify's Build Plugins.

Route 1: netlify-plugin-contextual-env

Pro's: Super easy to setup, simple to run

Con's: If you forget to override your prod ENV vars in your .env file, you risk accidentally running prod ENV keys locally

Route 2: netlify-plugin-inline-functions-env + CONTEXT decoding

Pro's: Always safe to run, no chance of accidentally using production keys

Con's: Takes a little more effort to setup and a bit of abstract code in every Function


Route 1

The easy one! Albeit with a little risk. Here's how it works. We start by adding any of the ENV vars we want running on Netlify to our Admin UI (under site settings, Build & Deploy, then 'Environment'). By 'running on Netlify', I mean, the configuration possibilities for when our app is running in production, in a deploy-preview, or in a branch-deploy. It looks like this:

Showing the multiple configured API keys per-environment, each environment using a different key value
Each environment uses a different value!

The idea is that our production environment will use the non-prefixed version of the ENV var, any branch-deploy environment will receive the BRANCH_DEPLOY_ prefixed value, and any deploy-preview environment will receive the DEPLOY_PREVIEW_ prefixed value. This works for both the SSG / build-time container and the serverless-functions runtime in each environment. Great!

The magic that makes this happen is the netlify-plugin-contextual-env. Before your build starts and your Functions are packaged and shipped, it determines which context the build is occurring in and re-writes the value of the naked ENV var key. Phrased differently, if your build is running in the deploy-preview context, the plugin looks up the value for DEPLOY_PREVIEW_API_KEY and overwrites the value of API_KEY with it. That way when your SSG or Function calls process.env.API_KEY, the value returned is actually the value you set for DEPLOY_PREVIEW_API_KEY. Clever!

To install netlify-plugin-contextual-env into your project, just add the following to your netlify.toml file:

[[plugins]]
  package = 'netlify-plugin-contextual-env'

Then we develop all functions and SSG code calling out to ENV vars in the usual fashion:

// output-env-var.js  /* Not for production use */
exports.handler = async _ => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      secret: process.env.API_KEY,
    }) 
  }
}

Once the code reaches a Netlify Build, the values will get swapped around to ensure that the correct values are in the correct environments thanks to netlify-plugin-contextual-env.

So what's that risk I was talking about before?

Con's: If you forget to override your prod ENV vars in your .env file, you risk accidentally running prod ENV keys locally

Ahh the local environment. Netlify Build Plugins don't run during local dev with Netlify Dev. But one of the neat tricks Netlify Dev does do is automatically pull down your site's ENV vars and inject them into your dev process when you fire it up. Can you feel the problem? If you've written all of your code to expect the 'normal' ENV var (API_KEY), but there's no build plugin swapping the value to a non-prod key, you'll be running with prod keys locally! That's a risky problem! It would be tragic if you had a few Functions that deleted some data in your local environment only to find that it actually deleted that data in your production environment! Yikes!

Possible Solution: Keep '.env*' in your .gitignore file but check in a mostly-empty .env file where the lines look like:

API_KEY=
SECURE_TOKEN=
EXAMPLE_THING=

Making sure that you cover all of your production ENV vars in this list. Netlify Dev will prioritize ENV vars defined in .env > those from your prod site, even if the values in your .env are blank. By checking in a mostly-blank list, the result is that if you ever re-clone your repository or have someone else clone it down, they won't be running prod keys from the start.

Once you've got your defaults checked in, you can go ahead and fill in the .env with your actual local keys (but don't commit these updates!):

API_KEY=sk_test_14h1o24i1141
SECURE_TOKEN=local_1212401294
EXAMPLE_THING=14214_test_1241412

That's it! That's route 1. While it feels a little bit riskier since a mistake in your .env can result in running production keys locally, this is the method I use for running multi-environment configurations on Netlify. It works great!


Route 2

Route 2 leverages the CONTEXT variable that Netlify sets for us. Depending on which environment the build and functions run in, the CONTEXT variable will resolve to either: 'deploy-preview', 'branch-deploy', or 'production'. For local development with Netlify Dev (until my PR gets merged) we'll need to add this CONTEXT variable in our .env file. If you don't have a .env file, just create the file in your project root (the directory where you kick off netlify dev) and add the line CONTEXT=dev. That way our environments all (including local) use the same environment variable key, CONTEXT, and appropriately resolve to the environment the code is currently running in.

Once that's configured, we're going to add all four environment variables to the Netlify Admin UI, prefixed with the environment name. We'll continue with the fictitious API_KEY for our example. We need to go into the Netlify Admin UI for our site, open up the "Build & Deploy" settings, and scroll down to Environment Variables. We need to add an entry for each context type we use - meaning that if our workflow for this site includes local development, PR's that generate deploy-previews, and a production app, we need to enter 3 entries. If we also use branch-deploys for a long-running branch, we need to create 4 entries. They're prefixed with the environment name, so even though the actual ENV var name we're trying to use is API_KEY, we're going to enter each entry as PRODUCTION_API_KEY, DEPLOY_PREVIEW_API_KEY, DEV_API_KEY, and BRANCH_DEPLOY_API_KEY, respectively:

Showing the multiple configured API keys per-environment, each environment using a different key value
Each environment uses a different value!

Nice! Note the difference here between Route 2 and Route 1 - in Route 1 we don't define DEV_ keys nor do we give the production keys the PRODUCTION_ prefix. These are key differences!

Once that's configured, let's open up our site code. We need to make just a couple of changes. For starters, we're going to add the netlify-plugin-inline-functions-env plugin to our site. To do that, just open up your netlify.toml and add:

[[plugins]]
  package = "netlify-plugin-inline-functions-env"

Finally, the last step is to prepare our Functions (and SSG as needed) to reach the contextually-correct ENV vars. This is a little snippet of code that will need to be added to all Functions on the site (since they're each isolated code-bases) and to the SSG code as well if your SSG pulls in ENV vars. This method essentially interpolates the CONTEXT variable into the ENV var key so that it pulls the correct key for the environment the code is built in.

const contextualEnvVar = (v) => {
  const currentContext = process.env.CONTEXT
  const formattedContext = currentContext.replace('-', '_').toUpperCase()
  return process.env[`${formattedContext}_${v}`]
}

exports.handler = async _ => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      status: contextualEnvVar('API_KEY')
    })
  }
}

For limitations I need not go into, the contextualEnvVar() function snippet does need to be pasted into each Function as shown above. It cannot be abstracted out to a package or re-usable. Once you've got that in place, just write your functions addressing contextualEnvVar('ENV_VAR') rather than using process.env. This will allow your context to always line up, be it DEV_, DEPLOY_PREVIEW_, or PRODUCTION_.

That's it!

While Route 2 has a bit more code complexity, it's safer in the sense that there's no way to accidentally run the project with production ENV values. The only way to use production ENV values is to be running in the production context, something that only happens during a production Netlify build.

With Route 2 we don't actually need to add any ENV var values to our .env file - we added them directly to the Netlify Admin UI with a DEV_ prefix instead. If you need to do some testing with a different value though, the .env file will always override what's in the Netlify UI, so just add the key to your .env like: DEV_API_KEY=testing-something-new-locally-123.

Since we aren't defining our default DEV_ ENV vars in our .env file with Route 2, the barrier to a working local environment is also lower. If my PR is merged, we won't even need to have CONTEXT=dev in our local .env, meaning that we can have a fully functional, safe local-development environment without even having a .env file. Neat 🙂


Both Routes / Gotchas

First, make sure you run netlify link in your project's root directory to connect it with your Netlify Site. Without that link being made, none of the site's ENV vars will be pulled down into your local development process and that will muck both routes up pretty hard. This is a tough one to trace down if you forget.

Second, it's paramount to understand the Netlify build process for Functions. When a Function is built, it's ultimately compiled down to an atomic .zip folder and sent off to AWS Lambda. The ENV vars to be used when running that function are included inside that package. All this to say, when you change / update / add a new ENV var in the Netlify Admin UI, nothing immediately changes about your site or Functions. You need to re-deploy your site for the new ENV vars to take root in your SSG build, and you need to re-deploy each function with a diff to the function's code. Netlify compares the .zip contents for each Function from one build to the next and skips re-building the .zip if the contents didn't change (this helps your build go faster). Unfortunately, this diff check does not take ENV vars into account. So add a console.log() or comment to our Function and re-deploy it. It should pick up new ENV vars at that point.

Okay, I think that about covers it. Let me know in the comments below if you have any issues or these things aren't working for you! Running multiple environments with their own configuration parameters is super important. Doing it right isn't the easiest, but it is worth it! Give it a try.

Photo by @maxcodes on Unsplash


Want to stay in the loop? 1 - 2 emails/month.

No spam. No data selling. Never.


Latest Blogposts

Trailing Slashes and Gatsby

The Ins and Outs of How Gatsby Does Slashes

April 16 2021

Tool Highlights: Typora

An app that allows Markdown to be my primary writing syntax

March 23 2021

Comparing JAMstack and Rails

Two different tools for two different jobs

March 20 2021