Back to blog

Alternatives to .env files: dotenvx, SOPS, git-crypt, hosted managers and Valt0

Vlad Vidican 8 min read

A .env file keeps your project's passwords, tokens and API keys in plain text. It works until it leaks: committed by mistake, pasted into a chat, or read out of a hosting platform's variable store. There are four common ways out: encrypt the .env file with dotenvx, encrypt files in git with SOPS or git-crypt, move secrets to a hosted secrets manager such as Doppler, Infisical or AWS Secrets Manager, or keep them in a Valt0 vault.

We make Valt0, and for most projects it is what we recommend: it is free, there is no account to make, and your secrets stay encrypted in the repository, next to the code they belong to. Below is how each option works, where each one fits, and what Valt0 does differently.

Why a .env file falls short

A .env file is simple, and that is its appeal. It also means:

  • Every copy is readable. Each laptop, CI runner and server that has the file has every secret in it, in plain text.
  • It has to stay out of git. One missing line in .gitignore and the secrets are in the repository's history, where removing them is harder than adding them.
  • It travels by hand. A new teammate gets the values in a chat message or an email, and those copies stay where they were sent.
  • In production it becomes a host's variable store. The same values move into your platform's environment variables. In October 2026 two platforms reported attacks on exactly that store: Zeabur, which says an attacker read 3,218,965 environment variables, and Railway, which says an attacker may have accessed customers' encrypted environment variables and, for some, the decrypted contents.

Encrypt the .env file: dotenvx

dotenvx is "a secure dotenv", from the creator of dotenv. It encrypts the values in your .env file with a public key, DOTENV_PUBLIC_KEY, and decrypts them with a private key, DOTENV_PRIVATE_KEY. Its site says to commit the encrypted file with your code. The private key stays out of git: by default in your operating system's secret store, and in production "where your app runs", such as your hosting platform's environment variables or CI secrets.

Your app reads the values the way it did with dotenv: require('@dotenvx/dotenvx').config() in code, or dotenvx run -- node index.js in front of any command.

The better choice when you want to keep the .env format and the way your app already loads it, or run in a language Valt0 has no client for, since dotenvx run works in front of any command.

What to weigh: in production the private key usually lives in the host's environment variables, beside the app. Anyone who reads that store and the repository can decrypt every secret, which is the case the two breaches above are about.

Encrypt files in git: SOPS and git-crypt

SOPS

SOPS is "an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats". It encrypts with keys from AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, age or PGP. By default it encrypts all the values and leaves the keys in cleartext, so a diff shows which setting changed without showing its value. A checksum over the file catches edits made outside SOPS. Your deploy step or app gets the values with sops decrypt.

The better choice when your team already has a cloud key service or age keys, or the secrets sit in configuration files for your infrastructure rather than in one app's .env.

git-crypt

With git-crypt, the files you choose are "encrypted when committed, and decrypted when checked out". You share the repository with people's GPG keys, or export one symmetric key and hand it over yourself. Its README is clear about the limits: it "does not encrypt file names, commit messages, symlink targets, gitlinks, or other metadata", it "does not support revoking access", and it "is not the best tool for encrypting most or all of the files in a repository".

The better choice when a small team that already uses GPG needs a few files kept private in a repository, and wants nothing new in how the app reads them.

What to weigh, for both: what is visible without the key (SOPS keeps the names readable, git-crypt the file names), and where the decrypted copy ends up. With git-crypt, the files in your working folder are plain text.

Use a hosted secrets manager: Doppler, Infisical, AWS Secrets Manager

A hosted manager keeps your secrets in a service, and your app or a command line tool fetches them from it.

  • Doppler offers access controls, logs, versioning and a command line tool. doppler run -- npm start injects the secrets as environment variables, or mounts them as a file that is cleaned up when the process exits. You sign up for an account.
  • Infisical is a platform for secrets that you use through its managed cloud or host yourself. It has SDKs for Node, Python, Go, Ruby, Java and .NET, a command line tool, secret rotation, audit logs and versioning.
  • AWS Secrets Manager replaces hard-coded credentials with "a runtime call to the Secrets Manager service", and can rotate secrets on a schedule. Google Cloud and Azure have their own.

The better choice when you need what only a service can do: rotating credentials on a schedule, a log of who read what, access that differs per person and per environment, or one place for many services.

What to weigh: an account and a service sit between your app and its secrets, and the secrets live with the provider, or on the server you run for a self-hosted one. If your app fetches its secrets when it starts, the service has to be reachable then, unless you keep a fallback copy (Doppler makes one) or sync the secrets into your host's variable store. Either way, the service is the store an attacker would aim for.

Keep secrets in a vault you commit: Valt0

Valt0 keeps a project's secrets in an encrypted file, a vault, that you commit next to your code. On your machine the vault opens with its key, which you keep; a deployment uses two keys made for it. There is no account and no hosted service: a service on the same machine as your app hands it its secrets, and the tool makes no network requests of its own. It is free to use.

Start from the .env you have. One command turns it into a vault:

Terminal
# make a vault called dev.valt0 from the .env you have
valt0 convert .env --output dev
git add dev.valt0

It prints the vault's key once: save it in your password manager before you close the terminal. valt0 convert only reads the .env, so take the secrets out of it once the vault holds them. Your app then asks for each secret by name, with a Valt0 client library. It needs the vault's id, which valt0 info dev prints, and never the key:

app.mjs
import { openVault } from '@valt0/client';
import Stripe from 'stripe';

const vault = await openVault({
  id: 'VLT0-ID-9254C02EE8685310C76B',
  name: 'dev',
});

const secret = await vault.secret('STRIPE_KEY');
const stripe = new Stripe(secret.reveal());
secret.destroy();

What Valt0 does differently

  • Compared with a .env file: the secrets are encrypted, so the vault is committed and versioned with the code instead of kept out of git and passed around by hand. A teammate pulls the repository and imports the vault with valt0 import; the key is the only thing you hand over.
  • Compared with an encrypted .env: in production no key sits in the host's environment variables. A deployment opens the vault with a prime key, which goes into the build, and an unlock key file, which the app is given when it starts. Neither opens the vault on its own; both together do, so they are kept apart. Your app asks for the secrets it needs by name, rather than finding all of them in its environment.
  • Compared with SOPS and git-crypt: there is no GPG, age or key service to set up, and unlike git-crypt, nothing is decrypted into your working folder. A secret prints as [REDACTED] in a log line until your code asks for its value.
  • Compared with a hosted manager: no account, no plan to outgrow, and no other company's service between your app and its secrets. The only service runs on the same machine as your app, and the tool makes no network requests of its own.

Side by side

Where the secrets live What opens them How your app reads them
A .env file A plain-text file on each machine Nothing Environment variables
dotenvx An encrypted .env in the repository A private key, kept where the app runs Environment variables, as with dotenv
SOPS Encrypted files in the repository A cloud key service, age or PGP keys Decrypted with sops
git-crypt Encrypted files in the repository GPG keys or a shared key The decrypted files in your working folder
Doppler Doppler's service A Doppler account or service token Environment variables or a mounted file
Infisical Infisical's cloud, or your own server Your Infisical account SDKs or a command line tool
AWS Secrets Manager AWS Your AWS credentials A call to the service at run time
Valt0 An encrypted vault file in the repository The vault key; in a deployment, a prime key and an unlock key file A client asks the Valt0 service by name

Which to choose

For most projects, a solo one included, start with Valt0. It is free, there is no account to make, and the .env you already have becomes a vault with one command. Installation has the command for each system, and the FAQ answers the questions people ask first.

Choose a hosted manager such as Doppler, Infisical or your cloud's own when you need rotation on a schedule, audit logs or per-person access across many services.

Create your first vault.

$ valt0 init prod

Install