Development vs Production Environments

The essential playbook for implementing development vs production environments in your SaaS.

Development and production must behave differently in controlled ways. Local development should optimize for speed and debugging. Production should optimize for security, reliability, performance, and repeatability.

The goal is not to make them identical everywhere. The goal is to make configuration explicit so your app runs predictably in both environments.

Quick Fix / Quick Setup

Use separate env files, separate databases, and explicit startup validation. Never share development secrets, sessions, or data stores with production.

bash
# .env.development
APP_ENV=development
DEBUG=true
LOG_LEVEL=DEBUG
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=dev-only-insecure-key
ALLOWED_HOSTS=localhost,127.0.0.1

# .env.production
APP_ENV=production
DEBUG=false
LOG_LEVEL=INFO
DATABASE_URL=postgresql://user:pass@db:5432/myapp_prod
REDIS_URL=redis://redis:6379/0
SECRET_KEY=replace-with-strong-secret
ALLOWED_HOSTS=app.example.com
python
# Python settings loader example
import os
from pydantic import BaseSettings

class Settings(BaseSettings):
    APP_ENV: str = "development"
    DEBUG: bool = False
    LOG_LEVEL: str = "INFO"
    DATABASE_URL: str
    REDIS_URL: str | None = None
    SECRET_KEY: str
    ALLOWED_HOSTS: str = "localhost"

    class Config:
        env_file = f".env.{os.getenv('APP_ENV', 'development')}"

settings = Settings()

if settings.APP_ENV == "production" and settings.DEBUG:
    raise RuntimeError("DEBUG must be false in production")

Minimum rules:

  • Set APP_ENV explicitly.
  • Keep real secrets out of git.
  • Use different DB, Redis, storage, and webhook endpoints per environment.
  • Fail startup if production config is unsafe.
  • Centralize config loading in one module.

What’s happening

Most environment problems come from implicit defaults:

  • The app silently uses local DB credentials.
  • Debug mode stays enabled after deploy.
  • Cookies were configured only for localhost.
  • Production containers write to ephemeral local disk.
  • OAuth, email, or Stripe keys point to the wrong environment.

Development usually runs on a laptop with hot reload, verbose logs, and relaxed security settings. Production usually runs behind a process manager, reverse proxy, managed database, and stricter network rules.

If the boundary is weak, you get config drift: code works locally but fails after deploy because services, domains, paths, SSL, queues, or storage are different.

A safe setup does three things:

  1. Separates config from code
  2. Uses different infrastructure per environment
  3. Validates critical settings at startup
Aspectlocal devproduction stack

side-by-side diagram of local dev vs production stack showing app config, database, Redis, storage, reverse proxy, and external services.

Step-by-step implementation

1. Define environment names once

Use a small fixed set:

  • development
  • staging
  • production

Keep the same names across:

  • app config
  • Docker Compose
  • CI/CD
  • hosting provider variables
  • worker services

Example:

bash
APP_ENV=development

Do not mix variants like prod, live, prd, local-dev unless you have a very specific reason.

2. Centralize configuration loading

Do not scatter os.getenv() calls throughout the app.

Bad:

python
db_url = os.getenv("DATABASE_URL")
debug = os.getenv("DEBUG") == "true"

Good:

python
from pydantic import BaseSettings

class Settings(BaseSettings):
    APP_ENV: str = "development"
    DEBUG: bool = False
    DATABASE_URL: str
    REDIS_URL: str | None = None
    SECRET_KEY: str
    LOG_LEVEL: str = "INFO"

Then import one config object everywhere.

For secret handling patterns, see Environment Variables and Secrets Management.

3. Use env-specific files for local development only

Commit examples, not secrets.

bash
.env.development
.env.staging.example
.env.production.example

Example .gitignore:

gitignore
.env
.env.*
!.env.example
!.env.development.example
!.env.production.example

Recommended layout:

bash
# committed
.env.development.example
.env.production.example

# uncommitted
.env.development
.env.production

For production, prefer your host’s env var system or secret store over a checked-in file.

4. Add startup validation

Fail fast before serving traffic.

python
if settings.APP_ENV == "production":
    if settings.DEBUG:
        raise RuntimeError("DEBUG must be false in production")
    if len(settings.SECRET_KEY) < 32:
        raise RuntimeError("SECRET_KEY is too short")
    if "localhost" in settings.ALLOWED_HOSTS:
        raise RuntimeError("ALLOWED_HOSTS is unsafe for production")

Validate at minimum:

  • APP_ENV
  • DEBUG
  • SECRET_KEY
  • DATABASE_URL
  • ALLOWED_HOSTS
  • storage backend config
  • required API keys for enabled features

5. Separate infrastructure by environment

Do not share these between development and production:

  • database
  • Redis
  • object storage bucket or prefix
  • queue namespace
  • webhook endpoints
  • OAuth apps
  • email provider domains/senders
  • payment credentials

Good:

bash
DATABASE_URL=postgresql://localhost:5432/myapp_dev
DATABASE_URL=postgresql://user:pass@prod-db:5432/myapp_prod

Bad:

  • local app uses production DB
  • production worker uses staging Redis
  • all environments write to the same S3 bucket root

6. Configure logging by environment

Local development:

  • human-readable logs
  • debug enabled
  • stack traces visible

Production:

  • structured logs
  • INFO/WARN/ERROR
  • no sensitive data
  • logs written to stdout/stderr or journal

Example:

bash
# development
LOG_LEVEL=DEBUG

# production
LOG_LEVEL=INFO

For runtime visibility, pair this with Logging Setup for Application Server Monitoring.

7. Define storage behavior explicitly

Local development may use disk. Production often needs object storage or mounted persistent volumes.

Example:

bash
# development
STORAGE_BACKEND=local
MEDIA_ROOT=./media

# production
STORAGE_BACKEND=s3
S3_BUCKET=myapp-production
S3_REGION=us-east-1

Common mistake: uploads work locally, then disappear in production because the container filesystem is ephemeral.

8. Enable production-safe security settings

Typical production requirements:

  • DEBUG=false
  • secure cookies
  • HTTPS redirects
  • trusted CSRF origins
  • correct cookie domain
  • explicit CORS allowlist
  • rate limits where needed

Example:

bash
SESSION_COOKIE_SECURE=true
CSRF_COOKIE_SECURE=true
CSRF_TRUSTED_ORIGINS=https://app.example.com
CORS_ALLOWED_ORIGINS=https://app.example.com

9. Reproduce production where it matters

You do not need a perfect clone locally. You do need parity for critical runtime dependencies:

  • database engine
  • Redis
  • reverse proxy behavior
  • file storage behavior
  • worker queue behavior

If production uses PostgreSQL, test against PostgreSQL before release. If production uses Docker, run your app in Docker locally at least once before shipping.

See:

10. Add deployment checks in CI/CD

Before deploy, verify required variables exist.

Example shell check:

bash
required_vars=(
  APP_ENV
  DATABASE_URL
  SECRET_KEY
  ALLOWED_HOSTS
)

for var in "${required_vars[@]}"; do
  if [ -z "${!var}" ]; then
    echo "Missing required env var: $var"
    exit 1
  fi
done

For production:

bash
if [ "$APP_ENV" = "production" ] && [ "$DEBUG" = "true" ]; then
  echo "DEBUG cannot be true in production"
  exit 1
fi

11. Split third-party integrations by environment

Use separate apps, projects, test/live keys, or webhook endpoints for:

  • auth providers
  • Stripe
  • email providers
  • webhooks
  • analytics
  • error tracking

Examples:

bash
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_test_...

# production
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_live_...

Mixing test and live credentials is a common release bug.

12. Document every environment variable

Keep a copy-paste friendly table in your repo or ops docs.

Recommended fields:

  • variable name
  • required or optional
  • example value
  • default behavior
  • environments used
  • sensitive or not

Example:

VariableRequiredExampleNotes
APP_ENVyesproductiondevelopment/staging/prod
DATABASE_URLyespostgresql://user:pass@db/appseparate per env
SECRET_KEYyeslong-random-stringnever commit
REDIS_URLnoredis://redis:6379/0required for workers
ALLOWED_HOSTSyesapp.example.comno localhost in prod
Local .env
CI Secrets
Env Injection
Startup Validation
Service Ready

Config Flow

Common causes

  • DEBUG left enabled in production
  • production app pointing to development database or Redis
  • missing or incorrect environment variables after deploy
  • secrets committed locally but not configured on server
  • local filesystem storage assumed in production container or VPS
  • OAuth, email, or payment credentials using the wrong environment
  • cookie, session, CSRF, or CORS settings built for localhost only
  • background workers using different env vars than the web app
  • different dependency versions between local and production
  • database engine differences causing migration or query failures

Debugging tips

Check the active environment first.

bash
printenv | sort
env | grep -E 'APP_ENV|DEBUG|DATABASE_URL|REDIS_URL|SECRET|HOST'
python -c "import os; print(os.getenv('APP_ENV')); print(os.getenv('DATABASE_URL'))"
python -c "from app.config import settings; print(settings.APP_ENV, settings.DEBUG, settings.LOG_LEVEL)"

Compare dependency/runtime versions:

bash
pip freeze
python --version

If using Docker:

bash
docker compose config
docker compose exec web env | sort

If using systemd on a VPS:

bash
systemctl status myapp
systemctl cat myapp
journalctl -u myapp -n 200 --no-pager
ps aux | grep gunicorn

Check health endpoints:

bash
curl -I http://127.0.0.1:8000/health
curl -I https://yourdomain.com/health
nslookup yourdomain.com

Useful debugging process:

  1. Print effective config in startup logs, excluding secrets.
  2. Verify app and worker use the same environment source.
  3. Confirm database, Redis, and storage targets are the expected ones.
  4. Verify domain, HTTPS, cookie, and CSRF settings in production.
  5. Compare package versions between local and deployed environments.

If deploy issues are involved, also review:

Checklist

  • APP_ENV is explicitly set in every environment
  • production has its own database, Redis, storage, and credentials
  • DEBUG is false in production
  • SECRET_KEY is unique and strong in production
  • ALLOWED_HOSTS or trusted domains are set correctly
  • secure cookies and HTTPS settings are enabled in production
  • static and media storage paths/backends are verified
  • background workers point to the correct broker and queue
  • webhook, OAuth, and email credentials match the environment
  • a staging environment exists for pre-production testing
  • startup validation rejects unsafe production config
  • environment variable documentation is up to date

Related guides

FAQ

Should I keep separate config files for development and production?

Yes, but keep them thin. Centralize configuration loading in one place and use env-specific files or secret scopes only for values that differ.

Is staging required for a small SaaS?

Not always on day one. It becomes important once you add payments, background jobs, OAuth, file uploads, or custom domain behavior.

Can I use the same third-party account for all environments?

Use separate projects, apps, webhook endpoints, or at minimum separate test and live credentials. Mixing environments increases risk and makes debugging harder.

What should the app validate on startup?

At minimum:

  • APP_ENV
  • DEBUG
  • SECRET_KEY presence and strength
  • database URL
  • allowed hosts
  • storage backend settings
  • required API keys for enabled features

Why does the app work locally but fail in production?

Usually because production has stricter networking, different paths, real domains, HTTPS, background workers, or a different database/storage backend than local development.

Should development match production exactly?

No. Match critical runtime dependencies and infrastructure behavior, but keep local development fast.

Should I use one .env file for everything?

No. Use separate files or secret scopes per environment.

Can I use SQLite in development and PostgreSQL in production?

Yes for early MVPs, but test against PostgreSQL before release to catch SQL and migration differences.

Where should production secrets live?

In your host, CI/CD secret store, or secret manager. Not in the repository.

Final takeaway

Development and production should share code, not assumptions.

Make environment-specific behavior explicit, validated, and documented.

Most deployment issues come from weak separation between:

  • secrets
  • services
  • domains
  • storage
  • security flags

If you define those boundaries clearly, small SaaS deployments become much more predictable.