As I’ve gotten more involved in various open source projects and helping out newbies, I’ve started to realize that many
frameworks rely on a .env
file to hold configuration. However, as soon as a developer moves to a framework or language where this is not the case, they get very confused.
History
For many developers around today, .env
files have been a standard configuration format for their entire career.
However, this has not always been the case. Before we had .env
files, we had configuration files. When you started on a new job, you’d have to drop some configuration files at specific places on your disk. These files were often XML, JSON, or INI files.
.env
files appear to be first introduced in December 2011, by Ruby/Heroku in PR #103 of Foreman. This was eventually implemented by bkeepers/dotenv
as a standalone ruby gem, which became widely used across Ruby projects (eventually even replacing the implementation in Foreman).
Later that March 2012, a Python fork of Foreman implemented .env
files, and then it was implemented in a JavaScript port later that October.
Finally, in January 2013, nearly one year later, we saw our first usage of .env
files outside of Foreman, in vlucas/phpdotenv
, which was quickly adopted by the Laravel community.
From there, it spread quickly to other languages and frameworks, becoming available as a package in most languages
by 2016.
Today, .env
files are basically the de facto configuration format for applications.
What is an environment anyway?
Behind the scenes, a program inherits a set of environment variables from the operating system.
For example, what kind of terminal is running, where to find other programs (PATH
), the hostname of the computer, the logged-in user, location of servers (such as on Linux, how to send sound to the sound card or display graphics), etc.
Environment variables are simple key-value pairs that your program can access at runtime. They’re not part of your code, and they don’t live in a file. If you spawn another program, it will inherit all the environment variables you’ve inherited plus any changes you’ve made.
You can try this out by skipping the .env
file and manually setting the environment:
DB_PASSWORD=password123 DB_USER=app
and typing:
export DB_PASSWORD=password123 DB_USER=app
Then when you run your app; it will just work!
So, what about .env files?
If environment variables already exist in the operating system, why do we need .env
files anyway?
Well, nobody wants to type export BLAH=BLAH FOO=BAR
before starting the application. And with the advent of Docker, nobody wants to manually manage complex configuration files and risk baking them into the image.
If no special loader (like a dotenv
library) is used, the .env
file is ignored. It’s not magic. Frameworks execute this very early in the boot process so that it feels magical and invisible.
In the end, a .env
file does nothing; it is just used by a loader to inject variables into your environment.
What to watch for
You need to be especially careful with .env
files and/or storing secrets in your environment. These values are stored as plain text, so if your users can execute arbitrary code, they can extract API keys, database passwords, encryption keys, signing keys, etc.
It is usually better to use proper secrets management instead of environment variables. If you’re using something like docker compose
, you can use secrets to share sensitive data with containers securely, without exposing them as plain-text environment variables.