From dotenv to dotenvx: Next Generation Config Management
The day after July 4th 🇺🇸, I wrote dotenv’s first commit and released version 0.0.1 on npm. It looked like this.
In the 11 years since, it’s become one of the most depended-upon packages worldwide 🌎 – adjacent ubiquitous software like TypeScript and ESLint.
It’s an example of “big things have small beginnings”. The README was short and the code was humble, but today it’s beloved by millions of developers.
It’s one of the few security tools that improve your security posture with minimal fuss.
- a single line of code -
require('dotenv').config()
- a single file -
.env
- a single gitignore append -
echo '.env' >> .gitignore
It’s aesthetic, it’s effective, it’s elegant.
But it’s not without its problems! And that’s what I want to talk about.
The problems with dotenv
In order of importance, there are three big problems with dotenv
:
- leaking your .env file
- juggling multiple environments
- inconsistency across platforms
All three pose risks to security, and the first does SIGNIFICANTLY.
But I think we have a solution to all three today - with dotenvx. In reverse problem order:
- Run Anywhere -> inconsistency across platforms
- Multiple Environments -> juggling multiple environments
- Encryption -> leaking your .env file
Let’s dig into each. I’ll do my best to show rather than tell.
Run Anywhere
dotenvx works the same across every language, framework, and platform – inject your env at runtime with dotenvx run -- your-cmd
.
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ node index.js
Hello undefined # without dotenvx
$ dotenvx run -- node index.js
Hello World # with dotenvx
> :-D
The .env parsing engine, variable expansion, command substitution, and more work exactly the same. Install dotenvx via npm, brew, curl, docker, windows, and more.
This solves the problem of inconsistency across platforms. ✅ You’ll get the exact same behavior for your python apps as your node apps as your rust apps.
Multiple Environments
Create a .env.production
file and use -f
to load it. It’s straightforward, yet flexible.
$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ dotenvx run -f .env.production -- node index.js
[dotenvx][info] loading env (1) from .env.production
Hello production
> ^^
While everything in dotenvx is inspired by community suggestions, this multi-environment feature particularly is. There were suggestions many times for something similar before I came to understand its usefulness. I’m convinvced now it cleanly solves the problem of juggling multiple environments when built into the command line. ✅
You can even compose multiple environments together with multiple -f
flags.
$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ dotenvx run -f .env.local -f .env -- node index.js
[dotenvx] injecting env (1) from .env.local, .env
Hello local
Handy! But it’s the next feature, encryption, that is the real game changer (and I think merits dotenvx as the next generation of configuration management).
Encryption
Add encryption to your .env files with a single command. Run dotenvx encrypt
.
$ dotenvx encrypt
✔ encrypted (.env)
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="03f8b376234c4f2f0445f392a12e80f3a84b4b0d1e0c3df85c494e45812653c22a"
# Database configuration
DB_HOST="encrypted:BNr24F4vW9CQ37LOXeRgOL6QlwtJfAoAVXtSdSfpicPDHtqo/Q2HekeCjAWrhxHy+VHAB3QTg4fk9VdIoncLIlu1NssFO6XQXN5fnIjXRmp5pAuw7xwqVXe/1lVukATjG0kXR4SHe45s4Tb6fEjs"
DB_PORT="encrypted:BOCHQLIOzrq42WE5zf431xIlLk4iRDn1/hjYBg5kkYLQnL9wV2zEsSyHKBfH3mQdv8w4+EhXiF4unXZi1nYqdjVp4/BbAr777ORjMzyE+3QN1ik1F2+W5DZHBF9Uwj69F4D7f8A="
DB_USER="encrypted:BP6jIRlnYo5LM6/n8GnOAeg4RJlPD6ZN/HkdMdWfgfbQBuZlo44idYzKApdy0znU3TSoF5rcppXIMkxFFuB6pS0U4HMG/jl46lPCswl3vLTQ7Gx5EMT6YwE6pfA88AM77/ebQZ6y0L5t"
DB_PASSWORD="encrypted:BMycwcycXFFJQHjbt1i1IBS7C31Fo73wFzPolFWwkla09SWGy3QU1rBvK0YwdQmbuJuztp9JhcNLuc0wUdlLZVHC4/E6q/K7oPULNPxC5K1LwW4YuX80Ngl6Oy13Twero864f2DXXTNb"
DB_NAME="encrypted:BGtVHZBbvHmX6J+J+xm+73SnUFpqd2AWOL6/mHe1SCqPgMAXqk8dbLgqmHiZSbw4D6VquaYtF9safGyucClAvGGMzgD7gdnXGB1YGGaPN7nTpJ4vE1nx8hi1bNtNCr5gEm7z+pdLq1IsH4vPSH4O7XBx"
# API Keys
API_KEY="encrypted:BD9paBaun2284WcqdFQZUlDKapPiuE/ruoLY7rINtQPXKWcfqI08vFAlCCmwBoJIvd2Nv3ACiSCA672wsKeJlFJTcRB6IRRJ+fPBuz2kvYlOiec7EzHTT8EVzSDydFun5R5ODfmN"
STRIPE_API_KEY="encrypted:BM6udWmFsPaBzlND0dFBv7R55JiaA+cZnbun8DaVNrEvO+8/k+lsXbZQ0bCPks8kUsdD2qrSp/tii0P8gVJ/gp+pdDuhdcJj91hxJ7nzSFf6h0ofRb38/2WHFhxg77XExxzui1s3w42Z"
# Logging
LOG_LEVEL="encrypted:BKmgv5E7/l1FnSaGWYWBPxxagdgN+KSEaB+va3PePjwEp7CqW6PlysrweZq49YTB5Fbc3UN/akLVn1RZ2AO4PyTVqgYYGBwerjpJiou9R2KluNV3T4j0bhsAkBochg3YpHcw3RX/"
A DOTENV_PUBLIC_KEY
(encryption key) and a DOTENV_PRIVATE_KEY
(decryption key) are generated using the same public-key cryptography as Bitcoin.
Now, even if you leak your .env file, it’s ok. An attacker needs the DOTENV_PRIVATE_KEY
to make sense of things. This effectively solves the problem of leaking your .env file ✅.
Bonus: This approach additionally makes it possible for contributors to add config while simultaneously being unable to decrypt config. I anticipate this will be useful for open source projects where you want to allow for contribution of secrets without decryption of prior secrets.
1.0.0 Release
With that, we’re pleased to announce the release of dotenvx version 1.0.0 🎉.
It is the next generation of configuration management, and I’m looking forward to what you do with it. The next decade (like the last) is bright for dotenv! 🌟
If you enjoyed this post, please share dotenvx with friends or star it on GitHub to help spread the word.