KAHIBARO
Discord Login Register

23.5. Environment Configuration

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:

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:

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:

EnvironmentPurposeExample URL
LocalIndividual developer machineshttp://localhost:8000
Development (dev)Shared testing for developershttps://api-dev.example.com
StagingPre-production, near real setuphttps://api-stg.example.com
ProductionLive system used by real usershttps://api.example.com

Each environment usually has its own:

The code should be the same, only configuration changes.

Environment-specific Variables

You might have variables like:

Example differences:

VariableLocalProduction
ENVlocalproduction
DATABASE_URLpostgresql://dev:dev@localhost:5432/app_devpostgresql://app:strong@db-prod:5432/app_prod
SECRET_KEYdev-secret-keylong random key from secret manager
LOG_LEVELDEBUGINFO or WARNING
ALLOWED_ORIGINS* or http://localhost:3000https://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:

bash
export DATABASE_URL="postgresql://user:pass@localhost:5432/app"
export SECRET_KEY="super-secret"
python main.py

Your application reads them, for example in Python:

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:

bash
# 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):

powershell
$Env:DATABASE_URL = "..."
$Env:SECRET_KEY = "..."
python main.py

Environment Variables in Docker

In containers you usually configure environment variables in:

Example docker-compose.yml snippet:

yaml
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
ENV=local
DATABASE_URL=postgresql://dev:dev@localhost:5432/app_dev
SECRET_KEY=dev-secret-key
LOG_LEVEL=DEBUG

You can:

Example with Python:

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:

.gitignore:

text
.env

.env.example (no real secrets):

env
ENV=local
DATABASE_URL=postgresql://user:password@localhost:5432/app
SECRET_KEY=change-me

Secrets in Production

In production, you usually do not use .env files on disk. Instead:

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:

Minimal Python example:

python
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:

python
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:

python
self.debug = os.getenv("DEBUG", "false").lower() == "true"

Then in each environment:

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
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.com

In staging:

env
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.com

Using Configuration in App Startup

python
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:

TypeExamplesHandling
SecretsDB passwords, JWT secret, API keysStrictly protected, secret store
Non-secretsPort, log level, feature flagsOK in config files or .env
Semi-sensitiveInternal URLs, email addressesTreat 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:

Examples:

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:

A pattern:

python
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:

Keep:

Example documentation table:

VariableRequiredDefaultDescription
ENVNolocalEnvironment name: local, staging, production
DATABASE_URLYesNonePostgreSQL connection URL
REDIS_URLYesNoneRedis connection URL
SECRET_KEYYesNoneUsed to sign JWTs and other secrets
LOG_LEVELNoINFOLogging level
ALLOWED_ORIGINSYesNoneComma separated list of allowed CORS origins

Migrating Configuration Safely

When adding a new required variable:

  1. Add support for it in code, but keep a safe default.
  2. Deploy this version everywhere.
  3. Configure the new variable in all environments.
  4. Make it required (remove default) in code.
  5. Deploy again.

This two-step process avoids breaking existing environments.

Example:

Step 1:

python
self.jwt_algorithm = os.getenv("JWT_ALGORITHM", "HS256")

Step 4:

python
self.jwt_algorithm = self._require("JWT_ALGORITHM")

Summary

Environment configuration is about:

If you follow these practices, deployments become predictable, safer, and easier to automate.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!