Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Required vs Optional Environment Variables for Safer Deploys

blog/cards/required-vs-optional-environment-variables-for-safer-deploys.jpg

It's important to differentiate these to protect your app from not starting up if a required env var is missing.

Quick Jump: Failing Early | Required vs Optional | Demo Video

If a required environment variable is missing and your app throws an exception on start up that’s really good because you can protect yourself from unwanted downtime.

For example, on deploy you might only serve live traffic to the new version once your app passes a few health checks. If your app doesn’t start it will fail those health checks and never go live. Instead, the previous version will continue to run successfully.

Often times you git ignore an .env file which may contain secrets such as API tokens and other sensitive data. The process of deploying code and setting env vars are usually done at different times in different places.

For example, you may set an environment variable before you ship the code to use it. This way it’s available when the new version of your app goes live. This introduces potential human error where you can accidentally forget to set the env var.

Failing Early

If you forget a required env var you want your app to fail as early as possible.

With a lot of web frameworks you can make this happen by only referencing env vars in your app’s config file(s). Then, throughout your application you can reference the value through its config option.

The basic idea here is when your app starts up it will read its config file(s) and if a required env var is missing an exception will be thrown. Since all of your envs exist in config files, they are all checked early in this 1 spot.

In my opinion this is much better than scattering around direct environment variable references throughout your app because this allows your app to boot with missing required env vars and you only get an exception much later at run-time when it’s being accessed on a page. That might happen days or weeks after a deploy.

Plus, I think it’s nice to have these env references in 1 area of your code. This is beneficial in 2 different ways:

  • You know for sure your app doesn’t have any hard to detect hidden dependencies lingering around
  • You’ve created a nice abstraction where your app doesn’t need to know about env vars, it only knows about config options

Practical Examples

With Flask, you can create a config file and define your config options there, and then within your app you can reference the config variables. You’d load this config in your create_app function which runs when your app starts.

With Rails, you can put your env var references in any file that exists in the config/ directory and you’re good to go. These files will be read on app startup. Then you can reference your config parameters or variables throughout your app.

I have example Docker apps using this pattern here:

There’s example apps for Django, Node and Phoenix too.

NOTE: The only exception is the example apps reference an env var to get the programming language’s run-time version on the home page. I’ve left that in these apps because the home page is meant to be changed by you. It’s throw away code and refactoring that env var into a config option will make it harder to remove it since it’ll be referenced in a few spots.

Required vs Optional

In practice I think there’s 2 ways to define environment variables:

  • Required: the env var must be defined with a value
  • Optional: the env var might be defined and if not it has a default fallback

I’ve personally found that to cover most use cases I’ve encountered.

Technically you could have an optional env var with no default fallback (defaults to None, nil, null, etc.) but in that case depending on the use case you could consider any of these:

  • Make it required
  • Default to a boolean false or a relevant data type depending on what you’re doing
  • Ask yourself if the value needs to be an environment variable
  • If the none of the above makes sense then sure default to null

Here’s a few examples using Python, Ruby and Node.

Python:
  • Required: use os.environ since it’ll fail if the env var isn’t set with a value
  • Optional: use os.getenv with a default fallback

Here’s 3 examples (required + optional with and without defining the env var):

$ docker container run --rm python:3-slim-bullseye \
    python3 -c 'import os; os.environ["NAME"]'

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<frozen os>", line 679, in __getitem__
KeyError: 'NAME'
$ docker container run --rm python:3-slim-bullseye \
    python3 -c 'import os; print(os.getenv("NAME", "nobody"))'

nobody
$ docker container run --rm -e NAME=nick python:3-slim-bullseye \
    python3 -c 'import os; print(os.getenv("NAME", "nobody"))'

nick
Ruby:
  • Required: use ENV.fetch with no default since it’ll fail if the env isn’t set with a value
  • Optional: use ENV.fetch with a default fallback

Here’s 3 examples (required + optional with and without defining the env var):

$ docker container run --rm ruby:3-slim-bullseye \
    ruby -e 'puts ENV.fetch("NAME")'

-e:1:in `fetch': key not found: "NAME" (KeyError)
        from -e:1:in `<main>'
$ docker container run --rm ruby:3-slim-bullseye \
    ruby -e 'puts ENV.fetch("NAME", "nobody")'

nobody
$ docker container run --rm -e NAME=nick ruby:3-slim-bullseye \
    ruby -e 'puts ENV.fetch("NAME", "nobody")'

nick
Node:

Node doesn’t have a built-in way to access env vars in a required vs optional way. If you reference process.env.NAME and it doesn’t exist then you’ll get undefined back but it won’t throw an exception.

The example below shows how you can make a helper function to check and then there’s an example of referencing an optional PORT env var and required SECRET_KEY variable:

function processEnvRequired(key) {
  const value = process.env[key];

  if (value === undefined || value === null || value === '') {
    throw Error(`KeyError: ${key} not found`);
  }

  return value;
}

config.express = {
  port: process.env.PORT || 8000,
  secret: processEnvRequired('SECRET_KEY'),
};

The video below covers what’s in this post but with more details going over the demo apps.

Demo Video

Timestamps

  • 0:53 – Reading all env vars when your app starts up is worth it
  • 3:39 – Demo’ing what happens when we don’t set a SECRET_KEY
  • 4:37 – Spending a few seconds looking at a Rails project
  • 5:21 – Going over required vs optional in the example Flask app
  • 7:30 – Python: Reading required vs optional env vars
  • 9:18 – Ruby: Reading required vs optional env vars
  • 12:06 – Node: Making our own required function

Are you using this pattern for managing env vars? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments