Configuring Environment Variables for All Netlify Environments
Jon Sully
8 Minutes
Correctly setup ENV vars for local, deploy-preview, branch-deploy, and production
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:
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-preview
s, and a production app, we need to enter 3 entries. If we also use branch-deploy
s 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:
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 js>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.
Comments? Thoughts?
Anonymous
To anyone reading this article in 2021, the PR got merged and now there is no need to create a .env file while using Route 2. Thanks Jon
Anonymous
Thank you.. The best explanation i have found, after spending to much time figuring how to determine my environment variable within my Netlify functions.