KAHIBARO
Discord Login Register

23.2. Preparing an Application for Production

Why “Production” Is Different

Running code on your laptop is very forgiving. You control the environment, there is no real traffic, and if something crashes you just restart it.

“Production” is different. Real users send requests at unpredictable times, your server runs for weeks or months, and any mistake can cause downtime, data loss, or security issues.

Preparing an application for production means turning “it works on my machine” into “it runs safely and reliably on real servers.”

Key production goals:

In this chapter, we will focus on what you do before deployment so that your app is ready to live in production.

Core idea: Production readiness is not about adding features. It is about making existing features safe, stable, and observable in a real environment.

Separating Configuration from Code

In development, it is tempting to put everything directly in code:

python
DATABASE_URL = "postgresql://user:password@localhost:5432/mydb"
DEBUG = True
SECRET_KEY = "super-secret-key"

In production, this is dangerous and inflexible.

Why configuration must be separate

Reasons to separate:

A common pattern is the twelve-factor app approach: store configuration in environment variables, not in code.

Typical configuration items

Common things you should never hardcode:

TypeExamples
SecretsAPI keys, database passwords, JWT secret keys
Connection infoDatabase URLs, Redis URLs, message broker URLs
Environment flagsDEBUG, ENV=development/staging/production
External servicesEmail SMTP config, payment gateway credentials
URLs / domainsBASE_URL, CDN URL, OAuth callback URLs

Your code should read these from the environment and provide safe defaults only for development.

Example in Python:

python
import os
ENV = os.getenv("ENV", "development")
DEBUG = ENV != "production"
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./dev.db")
SECRET_KEY = os.getenv("SECRET_KEY")  # no default for secrets!

Rule: Never commit real secrets to version control. Always load secrets from environment variables or a dedicated secrets manager.

Enabling Production Settings

Many frameworks and libraries have special production settings. Development defaults are often slow, verbose, and insecure.

Debug vs production mode

Most web frameworks have a debug or development flag.

Examples of typical differences:

SettingDevelopmentProduction
DEBUGTrue (full error pages)False (generic error messages)
LoggingVery verboseStructured, controlled by log level
Auto-reloadEnabled (reload on code change)Disabled (managed by process manager)
SecurityRelaxed checksStrict cookies, HTTPS, headers

In Python with a typical settings pattern:

python
import os
ENV = os.getenv("ENV", "development")
DEBUG = ENV == "development"

In FastAPI, debug mode is often configured at server level:

bash
# Dev, with auto-reload
uvicorn app.main:app --reload
# Production style, no reload, more processes
uvicorn app.main:app --host 0.0.0.0 --port 8000

You should ensure your deployment uses the correct flags and does not rely on development servers that are only meant for local use.

Security-related settings

Before production, you should verify that all relevant security settings are turned on:

These are often controlled by configuration flags so you can keep development flexible and production strict.

Handling Secrets Safely

Secrets are anything that should not be visible to users, attackers, or random developers.

Examples:

What not to do

Avoid these patterns:

python
# Bad: hard-coded secrets
SECRET_KEY = "this-is-actually-used-in-production"
DB_PASSWORD = "p@ssword123"
yaml
# Bad: committing .env.production with real values to Git
DATABASE_URL=postgres://user:real-password@db:5432/prod
SECRET_KEY=real-production-secret

Also avoid:

Safer approaches

For beginners, a simple but safer pattern is:

  1. Store secrets in environment variables on the server.
  2. Use a .env file only locally or in controlled infrastructure.
  3. Ensure .env is excluded from Git with .gitignore.

Example .env for development only:

env
ENV=development
DATABASE_URL=postgresql://devuser:devpass@localhost:5432/dev_db
SECRET_KEY=dev-only-secret

Then load it (for local development) with something like python-dotenv or your framework tools, but use real environment variables in production.

In code:

python
import os
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
    raise RuntimeError("SECRET_KEY is not set")

In production, you set SECRET_KEY at the system or container level, not in code.

For more advanced setups, you will use secrets managers (AWS Secrets Manager, Vault, etc), but the important idea is the same: application code reads secrets, it never contains secrets.

Database and Migration Readiness

Your production database is precious. You must prepare carefully before deploying.

Separate databases per environment

Never share the same database for dev and production.

Typical layout:

EnvironmentDatabase namePurpose
Devmyapp_devLocal development
Stagingmyapp_stagingPre-production tests
Productionmyapp or myapp_prodReal users and real data

Your configuration should point to the right database based on ENV.

Example:

python
ENV = os.getenv("ENV", "development")
if ENV == "production":
    DATABASE_URL = os.getenv("DATABASE_URL")  # must be set
elif ENV == "staging":
    DATABASE_URL = os.getenv("STAGING_DATABASE_URL")
else:
    DATABASE_URL = "sqlite:///./dev.db"

Migrations must be tested

If you use an ORM with migrations (for example Alembic with SQLAlchemy), you should:

  1. Generate and review migrations for your schema changes.
  2. Apply them to a test or staging database.
  3. Verify that the application works against the migrated schema.
  4. Only then apply migrations to production.

Typical production deployment sequence:

  1. Pull new code.
  2. Apply database migrations.
  3. Restart or reload the application.

The key point is: you cannot safely change tables manually in production and hope nothing breaks. Migrations are your version control for the database.

Rule: Always run migrations on a non-production environment first, then on production. Never experiment directly on the production database.

Validating Environment and Dependencies

Your app runs in a context that includes:

In production, you want this to be predictable.

Locking dependencies

In development, it is common to install packages with:

bash
pip install fastapi uvicorn[standard] sqlalchemy

In production, you want a lock file:

Example requirements.txt:

txt
fastapi==0.111.0
uvicorn[standard]==0.30.0
SQLAlchemy==2.0.31
psycopg2-binary==2.9.9
python-dotenv==1.0.1

Now production can run:

bash
pip install -r requirements.txt

This ensures you get exactly the versions you have tested.

Python version compatibility

You should know which Python version your code requires, for example:

You declare this in:

Example Dockerfile snippet:

dockerfile
FROM python:3.11-slim
# ...

If you rely on features from Python 3.11, do not run on 3.9 in production.

Health checks before deployment

Before sending users to a new version, you can run simple checks:

For example, you might add a simple script:

python
# scripts/check_env.py
import os
required = ["DATABASE_URL", "SECRET_KEY"]
missing = [name for name in required if not os.getenv(name)]
if missing:
    raise SystemExit(f"Missing required env vars: {', '.join(missing)}")
print("Environment looks OK")

Then in your deployment pipeline, run:

bash
python scripts/check_env.py

If it fails, you know configuration is invalid before switching traffic.

Logging and Observability Preparation

In development, you often log to the console and look at stack traces directly. For production, you need logs that are:

This is covered in detail in the Logging and Monitoring section, but some preparation is part of production readiness.

Log format and levels

You should decide:

Common pattern:

Example setup:

python
import logging
import os
ENV = os.getenv("ENV", "development")
log_level = logging.DEBUG if ENV == "development" else logging.INFO
logging.basicConfig(
    level=log_level,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger("myapp")

Now in your code:

python
logger.info("User logged in", extra={"user_id": user.id})
logger.error("Payment failed", extra={"order_id": order.id})

Even if you do not yet use a log aggregator, having consistent logs from the start saves you time later.

Health endpoints

A very helpful production preparation is to add a health check endpoint, for example:

This endpoint should:

Example in FastAPI:

python
from fastapi import FastAPI
from sqlalchemy.exc import SQLAlchemyError
app = FastAPI()
@app.get("/health")
async def health():
    try:
        # try a simple DB query if appropriate
        # db_session.execute("SELECT 1")
        db_ok = True
    except SQLAlchemyError:
        db_ok = False
    status = "ok" if db_ok else "degraded"
    return {"status": status, "database": db_ok}

Your load balancer or orchestration platform (Kubernetes, Docker Compose, etc) can use this endpoint to know when the app is ready to receive traffic.

Static Files and Assets

If your backend serves static files (CSS, JS, images), you must think about how this works in production.

Typical options:

Preparation steps:

  1. Choose where static files will live in production.
  2. Make sure your build process (if you have a frontend) produces files into a consistent directory, for example static/ or public/.
  3. Configure your application or server to serve from that directory.

Example FastAPI snippet:

python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")

In production, you might instead configure Nginx to serve /static by itself and let the app process only API routes.

Also think about:

Proper static file handling is important for performance and for avoiding broken frontend assets after deployment.

Performance Pre-checks

You do not need full performance optimization before you even have users, but you should at least avoid obvious performance problems.

Small load tests

Before production, you can run a simple load test:

Example ab command:

bash
ab -n 500 -c 20 http://localhost:8000/health

This sends 500 requests with concurrency 20.

If your app crashes under this small load, it is not ready for production.

CPU bound vs I/O bound

A very basic check:

If you ignore this, a single expensive request can block your server for other users.

You will learn more about performance in later chapters, but at minimum, you should know which operations are heavy and have a plan for them.

Graceful Error Handling

In development, you may let errors crash the app and show full tracebacks in the browser. In production, this is not acceptable.

You must:

Example for FastAPI:

python
from fastapi import HTTPException
@app.get("/items/{item_id}")
async def get_item(item_id: int):
    item = await repo.get_item(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

And a global exception handler (simplified):

python
from fastapi import Request
from fastapi.responses import JSONResponse
import logging
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
    logger.error("Unhandled error", exc_info=exc)
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal server error"},
    )

This way:

Pre-deployment Checklist

Before sending any backend application to production, you should walk through a checklist.

Here is a simple starting point you can adapt.

Production readiness checklist
Configuration

  • [ ] No secrets or production URLs are hardcoded in the codebase.
  • [ ] All required environment variables are documented.
  • [ ] ENV or similar flag is set to production for production.
    Security
  • [ ] Debug mode is disabled in production.
  • [ ] CORS configuration is restrictive, not * for everything.
  • [ ] Cookies and session settings are secure for production.
  • [ ] HTTPS will be used for all real user traffic.
    Database
  • [ ] Dev, staging, and production each have their own database.
  • [ ] Database migrations are generated and reviewed.
  • [ ] Migrations pass on a non-production environment.
  • [ ] Backups are configured (covered in a later chapter, but should be planned).
    Dependencies
  • [ ] Python version is defined and supported.
  • [ ] Dependencies are pinned in a lock file (requirements.txt or similar).
  • [ ] The production environment installs dependencies from the lock file.
    Logging and monitoring
  • [ ] Logging is configured with appropriate level (INFO or WARNING).
  • [ ] Logs include timestamps and severity.
  • [ ] A /health or /status endpoint exists and returns 200 when the app is ready.
    Static files
  • [ ] Strategy for serving static files is defined (app, reverse proxy, or CDN).
  • [ ] Static files build/output directory is consistent between envs.
    Performance and robustness
  • [ ] Basic load test shows the app can handle small concurrent load without crashing.
  • [ ] Long-running CPU bound tasks are identified and, if needed, offloaded to background workers.
  • [ ] Common error cases are handled gracefully with proper status codes.

You do not need a perfect system to start, but going through a checklist like this greatly reduces the chance of painful surprises after deployment.

Putting It All Together

Preparing an application for production is a mindset: you stop thinking like “a single developer running code” and start thinking like “an operator of a live system used by real people.”

In practical terms, you:

In the following chapters, you will see how to apply these ideas on a real Linux server and with Docker, and how to handle domains and HTTPS for a complete production deployment.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!