runserver. Here’s what I’ve learned building platforms that handle real traffic, real payments, and real users who don’t read documentation.The Gap Between Tutorial and Production#
Every Django developer knows the feeling: you follow the tutorial, everything works locally, and then you deploy. Suddenly you’re debugging CORS errors at 2 AM, your static files are 404ing, and your database migrations are timing out.
After shipping multiple Django platforms — from a consultancy CMS to a fitness booking system — I’ve developed a set of principles that close the gap between “it works on my machine” and “it works in production.”
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .When your development environment matches production from day one, entire categories of bugs disappear. “It works on my machine” becomes a meaningful statement because your machine is the production environment.
Settings Architecture Matters#
Cookiecutter Django gets this right: separate settings for base, local, and production. But the key insight is what goes where:
base.py: Everything that doesn’t change between environmentslocal.py: Debug toolbar, console email backend, relaxed securityproduction.py: Gunicorn, real email, strict CORS, database connection pooling
# base.py — the boring, correct defaults
DATABASES = {
"default": env.db("DATABASE_URL")
}
# Never hardcode. Always env vars. Always.
SECRET_KEY = env("DJANGO_SECRET_KEY")Database Migrations in Production#
My workflow for every migration:
- Write the migration locally
- Test it against a copy of production data
- Check if it requires a table lock (adding columns, creating indexes)
- If it locks: schedule downtime or use a two-step migration (add nullable → backfill → add constraint)
WhiteNoise for Static Files#
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # Right after security
# ...
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"This eliminates an entire nginx configuration file and simplifies your Docker setup to a single container.
The Monitoring Blindspot#
The one thing I wish I’d set up earlier in every project: structured logging. Not print() statements. Not even basic Django logging. Structured JSON logs that can be searched, filtered, and alerted on.
LOGGING = {
"version": 1,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json",
},
},
"formatters": {
"json": {
"class": "pythonjsonlogger.jsonlogger.JsonFormatter",
},
},
}What I’d Tell My Past Self#
- Use environment variables from the start. Not “when you deploy.” From the first
django-admin startproject. - Write the Dockerfile before the first model. The discipline pays for itself within a week.
- Separate read and write concerns early. Even if you’re not doing CQRS, having different serializers for list vs. detail views saves enormous refactoring later.
- Test the deployment, not just the code. A passing test suite means nothing if your Docker image doesn’t build.
Have questions or want to discuss Django architecture? Reach out