23.5. Environment Configuration
Table of Contents
Why Environment Configuration Matters
Backend applications usually behave differently in development, testing, staging, and production. They use different databases, secrets, logging levels, and external services.
If you hardcode these values into your code, you will:
- Leak secrets when sharing code
- Make deployments fragile
- Make local development painful
Environment configuration is about keeping your code the same, and changing behavior only through configuration.
Important rule:
**Never hardcode secrets or environment specific values in your source code.
Always inject them through configuration, usually environment variables.**
Configuration Principles
Configuration as Data, Not Code
Treat configuration as data that your application reads at startup, not as logic scattered through the code.
Examples of configuration:
- Database URL
- Redis URL
- External API keys
- Debug flag
- Allowed origins for CORS
- Log level
Good: central place, usually a settings or config module, that reads from environment variables and exposes typed values to the rest of the app.
Bad: if ENV == "prod": ... checks all over the code.
Twelve-Factor App and Config
A popular guideline is the “config” factor from the Twelve-Factor App methodology:
Rule:
Store config in the environment, not in the code.
This means the same build artifact (image, package, binary) can be deployed to multiple environments by only changing environment variables.
Configuration per Environment
Typical Environments
A common set of environments:
| Environment | Purpose | Example URL |
|---|---|---|
| Local | Individual developer machines | http://localhost:8000 |
| Development (dev) | Shared testing for developers | https://api-dev.example.com |
| Staging | Pre-production, near real setup | https://api-stg.example.com |
| Production | Live system used by real users | https://api.example.com |
Each environment usually has its own:
- Database instance
- Redis instance
- Storage bucket
- Secrets
- Logging verbosity
The code should be the same, only configuration changes.
Environment-specific Variables
You might have variables like:
ENV=localorENV=productionDATABASE_URLREDIS_URLSECRET_KEYALLOWED_ORIGINSLOG_LEVELSENTRY_DSN(error tracking)
Example differences:
| Variable | Local | Production |
|---|---|---|
ENV | local | production |
DATABASE_URL | postgresql://dev:dev@localhost:5432/app_dev | postgresql://app:strong@db-prod:5432/app_prod |
SECRET_KEY | dev-secret-key | long random key from secret manager |
LOG_LEVEL | DEBUG | INFO or WARNING |
ALLOWED_ORIGINS | * or http://localhost:3000 | https://app.example.com |
Environment Variables in Practice
What Are Environment Variables?
Environment variables are key-value pairs provided by the operating system or container and read by your application process.
Examples on Linux:
export DATABASE_URL="postgresql://user:pass@localhost:5432/app"
export SECRET_KEY="super-secret"
python main.pyYour application reads them, for example in Python:
import os
DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY = os.getenv("SECRET_KEY")
Rule:
Your app should start correctly as long as the right environment variables are present, without changing the code.
Listing and Setting Environment Variables
On Linux and macOS:
# List
env
printenv
# Set only for this command
DATABASE_URL="..." SECRET_KEY="..." uvicorn app.main:app
# Set for current shell
export DATABASE_URL="..."
export SECRET_KEY="..."On Windows (PowerShell):
$Env:DATABASE_URL = "..."
$Env:SECRET_KEY = "..."
python main.pyEnvironment Variables in Docker
In containers you usually configure environment variables in:
docker runcommanddocker-compose.yml- Kubernetes manifests
Example docker-compose.yml snippet:
services:
api:
image: my-api:latest
env_file:
- .env.production
environment:
- LOG_LEVEL=INFO.env Files and Secrets
Using .env Files for Local Development
Typing many export commands is annoying. .env files help in development:
.env:
ENV=local
DATABASE_URL=postgresql://dev:dev@localhost:5432/app_dev
SECRET_KEY=dev-secret-key
LOG_LEVEL=DEBUGYou can:
- Load this file with tools like
python-dotenvin development only - Or configure Docker Compose to read it
Example with Python:
from dotenv import load_dotenv
load_dotenv() # Only in local dev, not in production
And then use os.getenv as usual.
Rule:
**Never commit real secrets to version control, even in .env files.
Use .gitignore to exclude .env files that contain sensitive values.**
Typical pattern:
- Commit a safe example file:
.env.example - Ignore real config:
.env
.gitignore:
.env
.env.example (no real secrets):
ENV=local
DATABASE_URL=postgresql://user:password@localhost:5432/app
SECRET_KEY=change-meSecrets in Production
In production, you usually do not use .env files on disk. Instead:
- Configure environment variables in your cloud provider UI
- Use a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.)
- Inject them into containers during deployment
Your application code should not care where the values come from. It just uses os.getenv(...).
Centralized Settings in Your Application
Settings Object Pattern
Create a central settings class or module that:
- Reads environment variables once at startup
- Validates and possibly converts types
- Provides defaults for non-secret values
Minimal Python example:
import os
class Settings:
def __init__(self) -> None:
self.env = os.getenv("ENV", "local")
self.debug = self.env != "production"
self.database_url = self._require("DATABASE_URL")
self.secret_key = self._require("SECRET_KEY")
self.log_level = os.getenv("LOG_LEVEL", "INFO")
def _require(self, name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
settings = Settings()Now elsewhere:
from app.config import settings
engine = create_engine(settings.database_url)
Rule:
**Fail fast on missing required configuration.
It is better for your app to refuse to start than to run with incomplete config.**
Different Config per Environment with Same Code
Avoid separate classes like DevSettings, ProdSettings that are chosen by conditionals in many places.
Instead, let environment variables carry all differences. Example:
self.debug = os.getenv("DEBUG", "false").lower() == "true"Then in each environment:
- Local:
DEBUG=true - Production:
DEBUG=false
Same code, different behavior.
Example: Configuring FastAPI in Production
Imagine you have a FastAPI app. Here is how configuration might look.
Example Environment Variables
In production, your deployment system sets:
ENV=production
DATABASE_URL=postgresql://app:strong@db:5432/app
REDIS_URL=redis://redis:6379/0
SECRET_KEY=super-long-random-string
LOG_LEVEL=INFO
ALLOWED_ORIGINS=https://app.example.comIn staging:
ENV=staging
DATABASE_URL=postgresql://app:staging@db-stg:5432/app_staging
REDIS_URL=redis://redis-stg:6379/0
SECRET_KEY=another-random
LOG_LEVEL=DEBUG
ALLOWED_ORIGINS=https://staging.example.comUsing Configuration in App Startup
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
app = FastAPI(debug=settings.debug)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Note how the same code runs in all environments, but CORS behavior, debug mode, and other settings change through configuration.
Common Configuration Pitfalls
Mixing Secrets with Non-Secrets
Not all configuration values are equally sensitive.
Examples:
| Type | Examples | Handling |
|---|---|---|
| Secrets | DB passwords, JWT secret, API keys | Strictly protected, secret store |
| Non-secrets | Port, log level, feature flags | OK in config files or .env |
| Semi-sensitive | Internal URLs, email addresses | Treat as non-public where possible |
Do not log secrets. Logging DATABASE_URL with the password in plain text is dangerous.
Inconsistent Variable Names
Decide a consistent naming convention:
- Use uppercase with underscores
- Use clear prefixes for related items
Examples:
DB_HOST,DB_PORT,DB_NAME,DB_USER,DB_PASSWORD- or a single
DATABASE_URL
Pick one style and stick with it.
Relying on Defaults in Production
Defaults are useful for local development, but production should explicitly provide all important values, especially:
- Secrets
- External service URLs
- Security-critical flags
A pattern:
self.env = os.getenv("ENV", "local")
if self.env == "production":
self.debug = False
else:
self.debug = True
So even if DEBUG is set incorrectly, production is safe.
Rule:
**Security-related configuration (debug flags, allowed hosts, CORS origins) must be explicitly safe in production.
Never rely on insecure defaults.**
Configuration Management Across Deployments
Versioning Configuration
Even though secrets are not in git, the shape of your configuration should be:
- Documented
- Versioned
- Reviewed
Keep:
.env.exampleup to date- Deployment manifests (Docker Compose, Kubernetes) in version control
- Documentation describing each variable and its meaning
Example documentation table:
| Variable | Required | Default | Description |
|---|---|---|---|
ENV | No | local | Environment name: local, staging, production |
DATABASE_URL | Yes | None | PostgreSQL connection URL |
REDIS_URL | Yes | None | Redis connection URL |
SECRET_KEY | Yes | None | Used to sign JWTs and other secrets |
LOG_LEVEL | No | INFO | Logging level |
ALLOWED_ORIGINS | Yes | None | Comma separated list of allowed CORS origins |
Migrating Configuration Safely
When adding a new required variable:
- Add support for it in code, but keep a safe default.
- Deploy this version everywhere.
- Configure the new variable in all environments.
- Make it required (remove default) in code.
- Deploy again.
This two-step process avoids breaking existing environments.
Example:
Step 1:
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")Step 4:
self.jwt_algorithm = self._require("JWT_ALGORITHM")Summary
Environment configuration is about:
- Keeping application code the same across environments
- Controlling behavior entirely through configuration, usually environment variables
- Separating secrets from code
- Centralizing and validating configuration at startup
- Documenting configuration shape and defaults
- Using different values in development, staging, and production without code changes
If you follow these practices, deployments become predictable, safer, and easier to automate.
Views: 8
KAHIBARO