23.1. Development vs Production
Table of Contents
Why Environments Matter
When you build backend applications, you rarely run the same setup everywhere. You will usually have at least:
- A development environment, where you write and test code.
- A production environment, where real users use your application.
They serve different purposes, follow different rules, and are configured differently. Understanding this difference is critical before you deploy anything.
Key idea: Never treat your development environment as “almost production.”
Production must be safer, stricter, and more controlled than development.
In this chapter we focus on the mindset and practical differences between development and production, not on the details of any specific tool.
What Is a Development Environment?
A development environment is where you and your team write, run, and debug code. Its primary goal is to help you move fast, experiment, and find bugs early.
Typical examples of development environments:
- Your local machine running FastAPI with
uvicorn --reload. - A shared development server where new features are tested.
- A Docker Compose setup used by developers on their laptops.
Characteristics of Development Environments
Development environments are usually:
- Flexible and forgiving
- Verbose in logging and errors
- Less strict about security
- Less optimized for performance
Here are some common properties:
| Aspect | Development behavior |
|---|---|
| Code reloading | Automatic reload on file changes |
| Error visibility | Detailed error pages with stack traces |
| Logging | Very verbose, includes debug messages and sensitive details |
| Security | Relaxed, often using dummy secrets or test certificates |
| Database | Can be reset or dropped often, test data is allowed |
| Dependencies | Can change frequently; breaking changes are acceptable |
| Performance | Not a priority; correctness and visibility matter more |
Example: Python / FastAPI dev settings
In development you might:
- Run the server with auto reload and debug:
uvicorn app.main:app --reload- Use a
.envfile with simple and unsafe values:
ENV=development
DEBUG=true
SECRET_KEY=dev-secret-key-not-for-production
DATABASE_URL=postgresql://devuser:devpass@localhost/dev_db- Allow CORS from everywhere to simplify frontend work:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # fine in dev, not in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)This is comfortable in development, but it would be dangerous in production.
What Is a Production Environment?
A production environment is where your real users interact with your application. Any issue here can cost money, data, or trust.
Typical production environments:
- A cloud server (for example, AWS EC2, DigitalOcean droplet) running your app.
- A Kubernetes cluster serving thousands of requests per minute.
- A managed platform like Heroku or Render hosting your backend.
Characteristics of Production Environments
Production environments are usually:
- Stable and controlled
- Secure
- Monitored and logged
- Optimized for reliability and performance
Here are common production properties:
| Aspect | Production behavior |
|---|---|
| Code reloading | Disabled. New code only after a deployment process |
| Error visibility | No detailed stack traces to users |
| Logging | Structured, safe, and centralized |
| Security | Strict, with real secrets and TLS certificates |
| Database | Contains real data, must never be dropped or modified casually |
| Dependencies | Carefully controlled and versioned |
| Performance | Important, often tuned and tested |
Example: Python / FastAPI prod settings
In production you might:
- Run the app with a production server setup:
gunicorn -k uvicorn.workers.UvicornWorker app.main:app -w 4 -b 0.0.0.0:8000- Use environment variables set by your deployment system, not a
.envchecked into code:
ENV=production
DEBUG=false
SECRET_KEY=super-long-random-secret-from-secret-manager
DATABASE_URL=postgresql://appuser:longpassword@db-prod/app_db- Limit CORS only to trusted origins:
allowed_origins = [
"https://myapp.com",
"https://admin.myapp.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)Key Differences Between Development and Production
Both environments run the same application, but with different configuration, infrastructure, and rules.
Configuration and Environment Variables
You usually use environment variables to switch behavior.
Common example:
import os
ENV = os.getenv("ENV", "development")
DEBUG = ENV == "development"You might then use this in your code:
if DEBUG:
print("Running in development mode")
In production, ENV will be "production", so you avoid debug behaviors.
Rule: Configuration lives outside the code, usually in environment variables.
Never hard-code production secrets in your source code.
Typical variables that differ:
| Variable | Development example | Production example |
|---|---|---|
ENV | development | production |
DEBUG | true | false |
DATABASE_URL | Local DB or Docker DB | Managed database service (for example, AWS RDS) |
SECRET_KEY | Simple, short key | Long, random, stored in a secret manager |
ALLOWED_HOSTS | * or localhost | Specific domains for your app |
EMAIL_BACKEND | Console or sandbox provider | Real email provider (for example, SendGrid) |
Error Handling and Debugging
In development:
- You want maximum information.
- You accept that stack traces appear in the browser.
- You may show function names, file paths, and local variables.
In production:
- You want to hide internal details from users.
- You log errors to a file or a service, but you show a generic message.
Example of different behavior:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import os
app = FastAPI()
ENV = os.getenv("ENV", "development")
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
if ENV == "development":
# Show detailed info, only safe in development
return JSONResponse(
status_code=500,
content={"error": str(exc), "type": str(type(exc))},
)
else:
# Generic message only, safe for production
return JSONResponse(
status_code=500,
content={"error": "Internal server error"},
)Security Settings
Security in development is often simplified. In production, security rules must be much stricter.
Examples:
- CORS:
- Dev: allow
*for fast testing. - Prod: allow only your frontends’ domains.
- HTTPS:
- Dev: sometimes HTTP only, or self-signed certificates.
- Prod: always HTTPS with valid TLS certificates.
- Admin tools and docs:
- Dev: Swagger UI, admin panels, and debug endpoints are open.
- Prod: access is restricted to authenticated or internal users.
Rule: It is acceptable to relax security only in environments that have no real users and no real data.
Databases and Data
One of the biggest differences is how you treat data.
- Development data:
- Fake users.
- Test passwords.
- You can reset or drop the database.
- Often small and simple.
- Production data:
- Real users, real passwords, real money.
- You cannot casually reset or drop the database.
- You must back it up and protect it.
A typical pattern:
- Use different databases per environment:
myapp_devmyapp_testmyapp_prod- Use different DB users:
dev_userwith many privileges.app_userin production with only the permissions needed by the app.
Logging and Monitoring
In development:
- Logs are mostly for you, the developer.
- They can be messy and verbose.
- You often log to the console.
In production:
- Logs are for operations and debugging in the wild.
- Logs must be:
- Structured (usually JSON).
- Centralized (for example, ELK, Loki, CloudWatch).
- Careful about sensitive data.
Example of changing log level:
import logging
import os
ENV = os.getenv("ENV", "development")
log_level = logging.DEBUG if ENV == "development" else logging.INFO
logging.basicConfig(level=log_level)
logger = logging.getLogger(__name__)
logger.debug("Debug message") # Only appears in development
logger.info("Info message") # Appears in all environmentsPerformance and Scaling
Development:
- Usually a single instance of your app.
- Often a small local database.
- Minimal caching or none.
Production:
- Possibly multiple instances behind a reverse proxy or load balancer.
- Real database server or cluster.
- Caching (for example, Redis) and other performance tools.
Even if your application is small, production should be designed so that it can scale when needed.
Infrastructure and Deployment
In development you often start services manually:
uvicorn app.main:app --reload
docker compose upIn production:
- You have a deployment process or pipeline.
- Builds and tests run automatically.
- Containers are built and pushed to a registry.
- Servers or orchestration tools pull and run the new version.
You will learn the details in other chapters about Docker and CI/CD, but the key idea is that production deployments are repeatable, automated, and controlled.
Example: Same App, Different Environments
Assume you have this FastAPI application:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "ok"}You could run it in two different environments with different Docker Compose files.
Development docker-compose.yml
version: "3.9"
services:
api:
build: .
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
ports:
- "8000:8000"
environment:
- ENV=development
- DEBUG=true
- DATABASE_URL=postgresql://devuser:devpass@db/dev_db
volumes:
- .:/code
db:
image: postgres:16
environment:
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=devpass
- POSTGRES_DB=dev_db
ports:
- "5432:5432"Characteristics:
- Code is mounted as a volume, changes reflect immediately.
- Auto reload is enabled.
- Database is local and can be reset easily.
Production docker-compose.yml
version: "3.9"
services:
api:
image: myregistry/myapp:1.0.0
command: gunicorn -k uvicorn.workers.UvicornWorker app.main:app -w 4 -b 0.0.0.0:8000
ports:
- "8000:8000"
environment:
- ENV=production
- DEBUG=false
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
restart: alwaysCharacteristics:
- Uses a built image, no live code editing.
- Production server (gunicorn + uvicorn worker).
- Secrets provided from outside (
DATABASE_URL,SECRET_KEY). - Restart policy for reliability.
Testing and Staging Environments (Context)
Besides development and production, teams often use test or staging environments.
Common pattern:
| Environment | Purpose | Data type |
|---|---|---|
| Dev | Developer work, experiments | Fake / temporary |
| Test | Automated tests | Controlled test |
| Staging | Pre-production tests, close to production | Fake but realistic |
| Prod | Real users and workload | Real |
These environments behave more like production as you move from dev to prod. Staging is often almost identical to production infrastructure, but with fake data.
You do not need all these for small projects, but you should always have at least development and production.
Typical Pitfalls When Moving from Development to Production
Beginners often run into similar problems when they first deploy an app.
Forgetting to disable debug mode
For example, in Flask:
app.run(debug=True)If this is used in production, it can expose sensitive info.
In FastAPI, running with --reload in production has a similar risk and performance cost.
Rule: Never run your application in debug or reload mode in production.
Using the same database for dev and prod
If you accidentally point your development environment at the production database, you might:
- Insert test data into real tables.
- Drop or migrate schemas unexpectedly.
- Expose real user data in your local machine.
Always keep separate databases and verify connection strings.
Committing secrets to source control
If your .env contains production secrets and you commit it, you risk:
- Leaking database passwords.
- Exposing JWT secret keys.
- Exposing API keys for payment or email providers.
Use .gitignore and a secure method for storing secrets, such as a secret manager or environment variables configured on the server.
Mental Model: Same Code, Different Configuration
You should aim for this:
- Same application code can run in all environments.
- Configuration chooses how it behaves.
Mathematically you can think of it as:
$$
\text{Behavior} = f(\text{Code}, \text{Configuration})
$$
Where:
- Code is your Python files, FastAPI routes, database models.
- Configuration is environment variables, DB URLs, log levels, etc.
In good backend design:
Principle: Code does not change between environments, only configuration does.
You build your app once, then run it with different configuration for development, testing, and production.
Summary
- A development environment is for writing, debugging, and experimenting. It is flexible, forgiving, and can show detailed errors.
- A production environment serves real users. It must be secure, stable, monitored, and carefully configured.
- You separate environments primarily through configuration, usually environment variables.
- Security, logging, error handling, and data handling are stricter in production.
- Never share databases or secrets between development and production.
- Aim to keep the same code across environments and vary only configuration.
This foundation will help you in the next chapters, where you prepare your application specifically for production and learn to deploy it safely.
Views: 6
KAHIBARO