.env files

4/25/2025

Managing Secrets Without the Pain

Secure storage of secrets — and the proper use of this “sensitive” data in environment variables — has been something on my mind for quite a while.

Over time I tried several approaches. Some worked… until they didn’t.

This post is about how I used to manage secrets, the problems that came with it, and how I recently simplified the whole process.


The Old Approach: Local Encryption

For years my go-to solution was local encryption.

I relied on good old openssl, which is available on almost any system. The idea was straightforward:

  1. Keep a keyfile somewhere safe (in my case, usually a hidden Google Doc).
  2. Encrypt .env files using that key.
  3. Store the encrypted files in the repository.

The resulting files would look something like this:

.env.dev.enc
.env.stage.enc
.env.prod.enc

When needed, I would decrypt them locally, update variables, and encrypt them again.

Simple enough — at least in theory.


The Problem With This Workflow

In practice, things became painful pretty quickly.

Every small change required going through the full cycle:

decrypt → edit → encrypt

And this had to be repeated for every environment.

After doing this enough times, it became tempting to take shortcuts.

More often than I’d like to admit, I ended up using a single shared .env file across multiple applications just to make life easier (though who was I really fooling?).

The consequences were predictable.

Sometimes backend environment variables would end up sitting right next to frontend ones — including things like database connection credentials 🤪.

Not exactly ideal.


Another Issue: Separate Secret Storage

There was also a structural problem.

Encrypted .env files usually lived in a separate repository, which meant:

  • secrets were updated independently
  • application changes and secret changes were not synchronized
  • mistakes became easier to make

Any extra moving part in a deployment process increases the chance that something goes wrong.

And this definitely added a few more.


Discovering Doppler

Recently I came across Doppler. It turned out to be a really nice way to manage secrets.

Instead of juggling encrypted .env files, secrets are stored and managed through a clean web interface, and injected directly when running commands.

For example:

doppler run -- npm run build

The application simply runs with the correct configuration profile.

No .env files required.

Even better, Doppler provides integrations with most modern deployment tools.


Why I Still Keep .env Files

That said, I don’t love being completely dependent on a single tool.

If the tool disappears tomorrow, I still want my projects to work.

So instead of abandoning .env files entirely, I decided to generate them automatically using Doppler.

This keeps things portable while still benefiting from centralized secret management.


Automating Setup With Taskfile

To make things smoother, I created a small Taskfile for initializing projects with the correct environment variables:

# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: "3"

tasks:
  install:
    cmds:
      - curl -Ls --tlsv1.3 --proto "=https" https://cli.doppler.com/install.sh | sh

  login:
    cmds:
      - doppler login
  
  get-env:
    vars:
      DOPPLER_PROJECT: '{{default "help" .DOPPLER_PROJECT}}'
      DOPPLER_CONFIG: '{{default "dev" .DOPPLER_CONFIG}}'
      FILEPATH: '{{default "" .FILEPATH}}'
    cmds:
      - doppler secrets download --project {{.DOPPLER_PROJECT}} --config {{.DOPPLER_CONFIG}} --no-file --format env > {{.FILEPATH}}
  
  setup-infra-secrets:
    cmds:
      - task: get-env
        vars: { DOPPLER_PROJECT: "infra", DOPPLER_CONFIG: "{{.DOPPLER_CONFIG}}", FILEPATH: ".env" }

  setup-frontend-secrets:
    cmds:
      - task: get-env
        vars: { DOPPLER_PROJECT: "frontend", DOPPLER_CONFIG: "{{.DOPPLER_CONFIG}}", FILEPATH: "apps/frontend/.env" }
  
  setup-backend-secrets:
    cmds:
      - task: get-env
        vars: { DOPPLER_PROJECT: "backend", DOPPLER_CONFIG: "{{.DOPPLER_CONFIG}}", FILEPATH: "apps/backend/.env" }

  setup-secrets:
    deps: [setup-frontend-secrets, setup-infra-secrets, setup-backend-secrets]

  initial-setup:
    cmds:
      - task: install
      - task: login
      - task: setup-secrets

With it, generating the .env file becomes part of the normal project setup.

No manual secret handling required.


Final Thoughts

Managing secrets is one of those things that seems simple until it isn't.

My old setup worked for a while, but it created too much friction and too many opportunities for mistakes.

Using Doppler — while still keeping .env files as a fallback — turned out to be a good balance between convenience and control.

And honestly, anything that removes a few steps from secret management is already a win.