Skip to content

ericbusboom/dotconfig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dotconfig

Environment configuration cascade manager for .env files.

dotconfig assembles a single .env file from multiple layered source files — public config, SOPS-encrypted secrets, and per-developer local overrides — and can round-trip it back. It is designed for teams where:

  • Different developers have different local settings (Docker context, domain names, key paths).
  • Secrets live in SOPS-encrypted files alongside public config.
  • Multiple named environments (dev, prod, test, staging, CI, …) share the same layout.
  • Every tool (Docker, dotenv, IDEs) still reads a single .env file.

Table of Contents


Installation

Install with pipx so the tool is globally available without polluting any project's virtual environment:

pipx install dotconfig

Or into a project's own virtual environment:

pip install dotconfig

Verify the install:

dotconfig --version

Quick start

your-project/
  config/
    sops.yaml        ← SOPS encryption rules (generated by dotconfig init)
    dev/
      public.env     ← create this (public dev vars)
      secrets.env    ← SOPS-encrypted dev secrets
    prod/
      public.env     ← create this (public prod vars)
      secrets.env    ← SOPS-encrypted prod secrets
    local/
      yourname/
        public.env   ← create this (your personal overrides)
        secrets.env  ← optional personal encrypted secrets
  .env               ← generated; do not edit directly (add to .gitignore)

Load config into .env:

dotconfig load -d dev -l yourname   # dev deployment + your local overrides
dotconfig load -d prod              # prod deployment, no local overrides

Edit .env directly if you need to tweak a value, then save it back to the source files:

dotconfig save

Load/save specific files (YAML, JSON, etc.):

dotconfig load -d dev --file app.yaml --stdout   # print to stdout
dotconfig save -d dev --file app.yaml            # store into config/dev/

Directory structure

config/
  sops.yaml                      # SOPS encryption rules (non-dotfile, in config/)
  dev/
    public.env                   # Public config for the "dev" deployment
    secrets.env                  # SOPS-encrypted secrets for "dev"
  prod/
    public.env                   # Public config for the "prod" deployment
    secrets.env                  # SOPS-encrypted secrets for "prod"
  local/
    alice/
      public.env                 # Public local overrides for Alice
      secrets.env                # SOPS-encrypted local secrets (optional)
    bob/
      public.env                 # Another developer's overrides

Deployment names are open-ended — use any string that works as a directory name (dev, prod, test, staging, ci, …).


Generated .env format

dotconfig load produces a .env file with marked sections that map back to the source files:

# CONFIG_DEPLOY=dev
# CONFIG_LOCAL=ericbusboom

#@dotconfig: public (dev)
APP_DOMAIN=inventory.example.com
NODE_ENV=development
PORT=3000
DEPLOYMENT=dev
DATABASE_URL=postgresql://app:devpassword@localhost:5433/app
DO_SPACES_ENDPOINT=https://sfo3.digitaloceanspaces.com
DO_SPACES_BUCKET=my-bucket
DO_SPACES_REGION=sfo3

#@dotconfig: secrets (dev)
SESSION_SECRET=abc123...
GITHUB_CLIENT_ID=...
GOOGLE_CLIENT_ID=...

#@dotconfig: public-local (ericbusboom)
DEV_DOCKER_CONTEXT=orbstack
PROD_DOCKER_CONTEXT=swarm1
QR_DOMAIN=http://192.168.1.40:5173/
SOPS_AGE_KEY_FILE=/Users/ericbusboom/.config/sops/age/keys.txt

#@dotconfig: secrets-local (ericbusboom)

#@dotconfig: files
CERT=LS0tLS1CRUdJTi...                      # optional — only when --embed is used (CERT_FILE → CERT)

Last-write-wins: when the file is shell-sourced (set -a; . .env; set +a), later sections override earlier ones. Local overrides deployment; secrets override public.

The #@dotconfig: markers are unique to dotconfig — do not use this prefix in your own comments. The two metadata comments (CONFIG_DEPLOY, CONFIG_LOCAL) tell dotconfig save where to write each section back.

The #@dotconfig: files section is present only when --embed / -e was passed to load. It holds base64-encoded file contents (for Docker / env-var consumers that need PEM / cert files as variables). It is regenerated on each load and is ignored by dotconfig save — it is never written back to any source file.


Commands

dotconfig load

Usage: dotconfig load [OPTIONS]

  Assemble config files into .env, or load a specific file.

Options:
  -d, --deploy TEXT     Deployment / environment name (e.g. dev, prod).
  -l, --local TEXT      Local / developer name for personal overrides.
  -f, --file TEXT       Load a specific file instead of assembling .env.
  -e, --embed [VAR_FILE]
                        Base64-embed a config file into the 'files' section
                        of the .env.  Pass a *_FILE variable name to embed
                        only that one (e.g. -e CERT_FILE → CERT=<base64>);
                        pass -e with no value to embed every *_FILE
                        variable in the loaded config.  Repeatable.
  --no-export           Strip the leading 'export ' prefix from assignment
                        lines.  Implied by -S/--stdout.  Use for parsers
                        that reject shell-style exports (e.g. docker
                        stack deploy).
  --add-export          Ensure every assignment line has an 'export '
                        prefix.  Use to override -S/--stdout's implicit
                        --no-export.  Mutually exclusive with --no-export.
  -o, --output TEXT     Destination file.  [default: .env or the --file name]
  -S, --stdout          Print to stdout instead of writing to a file.
                        Implies --no-export unless --add-export is set.
  --config-dir TEXT     Root config directory.  [default: config]
  --help                Show this message and exit.

Examples:

# Load dev deployment with Eric's local overrides
dotconfig load -d dev -l ericbusboom

# Load prod deployment, no local overrides
dotconfig load -d prod

# Write to a file other than .env
dotconfig load -d dev -o .env.dev

# Load a specific YAML file from the dev deployment
dotconfig load -d dev --file app.yaml

# Print a file to stdout (useful for agents / piping)
dotconfig load -d dev --file app.yaml --stdout

# Embed config files as base64 env vars (Docker / env-var consumers).
# Declare *_FILE variables in your config (e.g. CERT_FILE=server.pem),
# then pass the *_FILE name to -e (suffix is stripped → CERT=<base64>).
dotconfig load -d dev -e CERT_FILE                  # one file
dotconfig load -d dev -e CERT_FILE -e SSL_KEY_FILE  # several
dotconfig load -d dev -e                            # every *_FILE var

# Plain KEY=value output (no `export` prefix) for docker stack deploy
dotconfig load -d prod --no-export -o .env

# -S/--stdout is pipe-friendly by default (implicit --no-export)
dotconfig load -d prod -S | jq .

# Keep the export prefix on stdout when you really want to source it
dotconfig load -d prod -S --add-export

When using --file, specify either -d or -l (not both) — the file lives in one location only.

When using --embed / -e, the pairing of destination variable to source filename lives in the config itself via the _FILE suffix convention. Declare a <NAME>_FILE=<filename> variable in your config, then pass -e <NAME>_FILE (or -e alone to expand every *_FILE variable). For each expanded variable, dotconfig reads the referenced file from config/<deploy>/<filename> first, then config/local/<user>/<filename> (first-match wins; auto-decrypted if SOPS-encrypted), base64-encodes it, and emits <NAME>=<base64> in the #@dotconfig: files section. The original <NAME>_FILE variable is preserved in its source section, so consumers that read the path-style variable still work. The files section is regenerated on each load and is not written back to any source file by dotconfig save. With --split, embedded files are written to the .env.secret companion (they are treated as secrets). Incompatible with --file, --json, and --yaml.

When using --no-export, leading export prefixes are stripped from assignment lines in the .env output. This is required by docker stack deploy (Swarm) and some other env-file parsers that reject shell-style export prefixes; docker compose up accepts both forms. Metadata comments and section markers are preserved. To round-trip source files that use export KEY=value, pair with save --add-export so the prefix is restored when writing back. Incompatible with --file.

-S/--stdout implies --no-export by default — piped consumers like jq, docker stack deploy, and most env-file parsers reject the shell export prefix, so the pipe-friendly form is the right default. Pass --add-export alongside -S to keep the prefix when you do want a shell-sourceable stdout dump. --no-export and --add-export are mutually exclusive.

What it reads (without --file):

Source file Section in .env
config/{deploy}/public.env #@dotconfig: public ({deploy})
config/{deploy}/secrets.env (SOPS-encrypted) #@dotconfig: secrets ({deploy})
config/local/{local}/public.env #@dotconfig: public-local ({local})
config/local/{local}/secrets.env (SOPS-encrypted) #@dotconfig: secrets-local ({local})

If a secrets file is absent or SOPS is unavailable, the section is written as empty with a warning — the command does not abort.

If a local file is absent, a warning is printed and the section is written as empty — useful when a new developer clones the repo before creating their own local overrides.


dotconfig save

Usage: dotconfig save [OPTIONS]

  Save .env sections back to config/ source files, or store a file.

Options:
  -d, --deploy TEXT     Target deployment (overrides .env metadata).
  -l, --local TEXT      Target local / developer name (overrides .env metadata).
  -f, --file TEXT       Save a specific file into the config directory.
  --add-export          Prepend 'export ' to assignment lines when writing
                        back to source .env files.  Pairs with load's
                        --no-export for round-trip style preservation.
  --env-file TEXT       .env file to read and save.  [default: .env]
  --config-dir TEXT     Root config directory.  [default: config]
  --help                Show this message and exit.

Examples:

# Save all sections from .env back to config/
dotconfig save

# Save to a different deployment
dotconfig save -d staging

# Re-add `export` prefix on write (pairs with load --no-export)
dotconfig save --add-export

# Save a YAML file into the dev deployment
dotconfig save -d dev --file app.yaml

# Save a JSON file into a local config directory
dotconfig save -l alice --file settings.json

When using --file, specify either -d or -l (not both) — the file lives in one location only.

When using --add-export, every assignment line written back to a source .env file is prefixed with export unless it already has one. Comments, blank lines, and section markers are left alone. Incompatible with --file, --json, and --yaml.

What it writes (without --file):

Section in .env Destination file
#@dotconfig: public ({deploy}) config/{deploy}/public.env (plaintext)
#@dotconfig: secrets ({deploy}) config/{deploy}/secrets.env (SOPS-encrypted)
#@dotconfig: public-local ({local}) config/local/{local}/public.env (plaintext)
#@dotconfig: secrets-local ({local}) config/local/{local}/secrets.env (SOPS-encrypted, only if non-empty)

dotconfig save requires a dotconfig-managed .env (one that was produced by dotconfig load) because it relies on the CONFIG_DEPLOY metadata comment to know where to write the files back.


SOPS integration

dotconfig delegates all encryption and decryption to sops. You must install sops separately.

SOPS is optional for loading public config. If sops is not installed, or a secrets file is missing, the secrets section is left empty and a warning is printed.

Key discovery follows the standard sops precedence:

  1. SOPS_AGE_KEY_FILE environment variable (path to an age private key file)
  2. SOPS_AGE_KEY environment variable (inline age private key)
  3. sops.yaml specified via --config flag or SOPS_CONFIG environment variable

If SOPS_AGE_KEY_FILE is defined inside .env itself (e.g. in the public-local section), dotconfig save reads it from the file before invoking sops, so you do not need to export it manually.

Recommended config/sops.yaml:

creation_rules:
  # Secrets companion files (app.secrets.yaml, etc.)
  - path_regex: '.+\.secrets\.(?:env|json|yaml|yml|txt|conf)$'
    age: >-
      age1v3f2rn...,age1h02a69...
  # Legacy secrets files (secrets.env, etc.)
  - path_regex: '.+/secrets\.(?:env|json|yaml|yml|txt|conf)$'
    age: >-
      age1v3f2rn...,age1h02a69...
  # Catch-all for any file dotconfig encrypts (private keys, etc.)
  - path_regex: '.+'
    age: >-
      age1v3f2rn...,age1h02a69...

Because sops.yaml is not a dotfile, SOPS will not auto-discover it. Specify it explicitly when invoking sops directly:

SOPS_CONFIG=config/sops.yaml sops --encrypt --in-place config/dev/secrets.env
# or
sops --config config/sops.yaml --encrypt --in-place config/dev/secrets.env

Workflow

First-time setup (new developer)

git clone <repo>
# Create your personal local overrides
cp -r config/local/ericbusboom config/local/yourname
# Edit it with your values
$EDITOR config/local/yourname/public.env
# Load dev config
dotconfig load -d dev -l yourname

Daily development

# Reload if someone changed config files
dotconfig load -d dev -l yourname

# Make an ad-hoc change in .env directly, then save it back
$EDITOR .env
dotconfig save

Keeping .env out of version control

Add .env to .gitignore — it is a generated file:

# Generated by dotconfig load — do not commit
.env

The source files in config/ are committed. Encrypted secrets files (config/secrets/) are safe to commit because they are SOPS-encrypted.


Adding a new deployment

  1. Create the deployment directory and its public config:
    mkdir -p config/{name}
    $EDITOR config/{name}/public.env      # add public variables
  2. Load the new deployment to generate .env:
    dotconfig load -d {name}
  3. Add any secrets to the secrets section in .env, then save back:
    $EDITOR .env                          # add values under the secrets section
    dotconfig save                        # encrypts secrets via SOPS automatically

Adding a new developer

  1. Ask the developer to generate an age key pair:
    age-keygen -o ~/.config/sops/age/keys.txt
    # Share the PUBLIC key (age1...) with the team
  2. Add their public key to config/sops.yaml and re-encrypt secrets:
    SOPS_CONFIG=config/sops.yaml sops updatekeys config/dev/secrets.env
    SOPS_CONFIG=config/sops.yaml sops updatekeys config/prod/secrets.env
  3. The developer creates their local overrides:
    cp -r config/local/ericbusboom config/local/theirname
    $EDITOR config/local/theirname/public.env
    dotconfig load -d dev -l theirname

Design decisions

Decision Rationale
Single .env file Tools (dotenv, Docker, IDEs) read one file — no cascade-compatibility issues.
Marked sections Enable round-tripping between .env and config/ source files without extra metadata files.
Open deployment names Not limited to dev/prod; supports test, ci, staging, or any custom name.
Last-write-wins ordering Later sections override earlier when shell-sourced; local overrides deployment.
SOPS optional at load time Developers without SOPS access can still load public config; secrets are skipped with a warning.
No shell variable expansion Values are literal strings — no $VAR interpolation. Use a local override to change a value for a specific machine.
Local secrets are optional config/local/{user}/secrets.env is supported but most developers won't need it.

About

Program for managing .env files in development.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages