Environment Variables Guidelines
🎯 Why Environment Variables Matter
Environment variables (“env vars”) allow us to configure applications cleanly and securely without modifying the codebase.
They are critical for:
- Security (secrets never in code)
- Reproducibility (different environments with same code)
- Reliability (fail fast if critical config is missing)
- Scalability (infrastructure can modify settings without redeploy)
🗂️ Where Environment Variables Come From
Environment | Source |
---|---|
Local Dev | .env loaded by Docker Compose or container |
Docker / Kubernetes | Passed by orchestrator (docker-compose.yml , k8s ConfigMap/Secret) |
Cloud (Prod) | Passed via Secret Manager, Env injection, or Platform (e.g., GCP Run, AWS ECS) |
✅ Applications never load .env
files themselves.
✅ Environment must set variables before the app starts.
📄 How to Handle Environment Variables
1. Pass Vars via Infrastructure
Local Dev:
-
Store
.env
file in project root (make sure it’s added to.gitignore
) -
Store a
.envrc
file in project root (and check it in), with the following contentdotenv
-
Load automatically through:
-
Using
env_file
if usingdocker-compose.yml
:services: backend: build: . env_file: - .env
-
Using direnv for shell auto-loading, which automatically loads the content of
.env
file when youcd
into the project folder, and unloads them when youcd
out of it.
-
Production:
- Inject secrets at deploy time (CI/CD pipeline, Secret Manager, GCP/AWS environment config)
2. Do NOT Load .env
Files in Code
No dotenv.load_dotenv()
calls in the application.
Instead, read environment variables directly:
import os
DATABASE_URL = os.environ["DATABASE_URL"]
3. Type Safety
Always cast env vars explicitly:
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
MAX_CONNECTIONS = int(os.environ.get("MAX_CONNECTIONS", "10"))
📦 Local Development Flow
Every project must include:
.env.example
— Committed (template only, no real secrets).envrc
— Committed with a single line:dotenv
. This instructdirenv
to load environment variables from.env
in the same director.env
— Ignored via.gitignore
(created manually from example)- Always wrap values inside double quotes (even numbers and boolean). This avoids surprises, and provide compatibility across different environments.
Typical .env
:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/db"
MAX_CONNECTIONS="20"
# Redis
REDIS_URL="redis://localhost:6379"
# API Keys
OPENAI_API_KEY="sk-..."
# App Settings
DEBUG_MODE="true"
ENVIRONMENT="local"
🔐 Secrets Management Rules
- Secrets must never be hardcoded
.env
must be in.gitignore
.env.example
must be kept up-to-date- Cloud secrets (prod) must be stored in Secret Manager or similar
- Rotate secrets regularly
⚙️ Good Practices for Using Environment Variables
- Validate critical env vars at app startup (fail fast)
- Group related vars logically (DB vars, Redis vars, LLM vars)
- Document important variables in project README or Notion
- Avoid leaking secrets in logs or errors
🧵 Environment Variables and Typer CLI
[TODO: Review]
CLI tools that require env vars should expect them to be preloaded (via Docker, local .env
, or deployment config).
You may optionally expose fallback flags via CLI, but env vars are the default:
import os
import typer
app = typer.Typer()
@app.command()
def evaluate(model: str = typer.Option(os.environ.get("DEFAULT_MODEL", "gpt-4"))):
...
📋 Checklist for Handling Env Vars
-
.env
is ignored in.gitignore
-
.env.example
is committed and updated - App does not load
.env
manually - Critical vars validated at startup
- Type-safe parsing of env vars
- No secrets printed in logs
- Safe secrets management in cloud environments
🧠 Final Note: Treat Environment Like a Contract
The environment is a first-class citizen, just like code and data.
A correct environment setup guarantees that the same code works reliably across local, staging, and production—without hacks, without surprises.