Set Up a Staging Environment for Django
Build a Django staging environment that catches real release problems: production parity, isolated data and secrets, and safe side effects.
Problem statement
A staging environment is where a release runs on production-like infrastructure before real users see it. Done well, it catches failed migrations, missing environment variables, broken static files, and integration errors. Done poorly, it becomes a second source of incidents:
- staging differs from production (SQLite instead of PostgreSQL,
DEBUG=True, no Celery), so it passes releases that then fail - staging sends real emails or charges real cards because it shares credentials with production
- a copy of production data with customer information sits on a less protected server
- search engines index the staging site
- nobody deploys to staging consistently, so it drifts and is ignored
This guide sets up a Django staging environment that mirrors production's architecture, is isolated from production's data and side effects, and sits in the normal release path.
Quick answer
- Run staging on the same stack as production (same Python, database engine and major version, web server, task queue), at smaller size.
- Use the same settings module with environment variables; set a
DJANGO_ENV=stagingvalue for environment-specific behavior. - Give staging its own database, Redis, storage buckets, and secrets; share nothing with production.
- Route email to a test inbox, use payment provider test keys, and point webhooks at staging.
- Protect access (SSO, VPN, or basic auth at the proxy) and send
X-Robots-Tag: noindex. - Seed with generated or anonymized data, never raw production copies.
- Deploy the same artifact to staging first, run checks, then promote that artifact to production.
Step-by-step solution
1. Define what "production-like" means
Match the things that cause release failures:
| Match exactly | May differ |
|---|---|
| Python version and dependency lockfile | server size and count |
| Database engine and major version | database size |
| Web server, app server, and proxy chain | autoscaling rules |
| Task queue and broker | retention periods |
| Storage backend type (S3-compatible, local) | domain names |
DEBUG=False and security settings | third-party accounts (test mode) |
A staging environment on SQLite with runserver will not catch a PostgreSQL-specific migration failure or a Nginx static file mistake, which are exactly the failures you want to find.
2. One settings module, environment-driven
Avoid a separate staging.py settings file that slowly diverges. Use production settings with environment variables, plus a small switch for behavior that must differ:
import os
DJANGO_ENV = os.environ.get("DJANGO_ENV", "production")
IS_STAGING = DJANGO_ENV == "staging"
DEBUG = False
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
if IS_STAGING:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Point EMAIL_HOST at a test inbox service such as Mailpit or a provider sandbox
Keep IS_STAGING branches rare and obvious. Every difference is something staging cannot test.
3. Isolate infrastructure and secrets
Staging must not be able to reach production data:
- separate database server or at least separate database and role, with no production credentials present
- separate Redis instance, so a staging
FLUSHALLcannot wipe production caches or queues - separate storage buckets with separate access keys
- separate
SECRET_KEY, so signed tokens from staging are useless in production - separate cloud account, project, or at least IAM roles when possible
A quick test: from a staging shell, try connecting to production's database host and Redis. It should fail at the network level.
4. Neutralize side effects
External effects are the most dangerous part of staging:
- Email: route all mail to a test inbox (for example Mailpit or your provider's sandbox), or add a safety net that rewrites recipients:
# core/mail.py
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
class StagingRedirectBackend(EmailBackend):
def send_messages(self, email_messages):
for message in email_messages:
message.to = [settings.STAGING_EMAIL_RECIPIENT]
message.cc = []
message.bcc = []
return super().send_messages(email_messages)
if IS_STAGING:
EMAIL_BACKEND = "core.mail.StagingRedirectBackend"
STAGING_EMAIL_RECIPIENT = "staging-inbox@example.com"
- Payments: use test mode API keys; production keys should not exist in staging's secret store.
- Webhooks: register staging endpoints separately with each provider's test mode.
- SMS, push notifications, analytics: use sandbox credentials or disable them with the environment switch.
- Scheduled jobs: decide which Celery Beat tasks should run in staging; disable ones that contact real customers.
5. Control access and indexing
Staging often has weaker authentication for convenience and less monitoring. Put it behind an access layer:
server {
listen 443 ssl;
server_name staging.example.com;
auth_basic "Staging";
auth_basic_user_file /etc/nginx/staging.htpasswd;
add_header X-Robots-Tag "noindex, nofollow" always;
location / {
proxy_pass http://unix:/run/myapp/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Create the password file with htpasswd -c /etc/nginx/staging.htpasswd teammate (from the apache2-utils package). Basic auth at the proxy blocks crawlers and casual visitors without changing Django. Exempt webhook and health check paths with separate location blocks using auth_basic off; if providers must reach them. SSO through an identity-aware proxy is stronger for larger teams.
6. Realistic data without customer data
Options, from safest to riskiest:
- Generated fixtures: factories (for example factory_boy) that create realistic volumes and edge cases.
- Anonymized copies: restore a production backup into an isolated environment, run a script that replaces names, emails, addresses, and free-text fields, then dump that anonymized database for staging.
- Raw production copies: avoid; staging then needs production-level security, access logging, and data protection compliance.
A minimal anonymization step in SQL, run before the data ever reaches staging:
UPDATE auth_user
SET email = 'user' || id || '@example.test',
first_name = 'User',
last_name = id::text,
password = '!';
A password value of ! is an unusable password for Django's hashers, so nobody can log in as copied users. Extend the script for every model with personal data, and review it when models change.
7. Put staging in the release pipeline
The value of staging comes from deploying the same build there first:
- CI builds one artifact (image tag or release archive) per commit.
- Deploy that artifact to staging automatically on merge to the main branch, including migrations.
- Run smoke tests against staging.
- Promote the same artifact to production (manual approval or automatic after checks).
Never rebuild for production: a rebuild can pick up different dependency or base image versions than the ones you tested.
A basic smoke test script:
#!/bin/bash
set -euo pipefail
BASE=https://staging.example.com
AUTH="-u smoke:${STAGING_BASIC_AUTH_PASSWORD}"
curl -fsS $AUTH -o /dev/null "$BASE/healthz/"
curl -fsS $AUTH -o /dev/null "$BASE/"
curl -fsS $AUTH -o /dev/null "$BASE/static/admin/css/base.css"
curl -fsS $AUTH "$BASE/api/status/" | grep -q '"ok"'
echo "staging smoke tests passed"
8. Test migrations on production-sized data
Schema changes that are instant on a small staging database can lock a large production table for minutes. Periodically restore an anonymized production-sized snapshot into staging and time the pending migrations there before releasing them.
9. Verify
Verification check:
DEBUGisFalseandcheck --deploypasses on staging- staging cannot connect to production database or Redis hosts
- an email triggered on staging arrives only in the test inbox
- a payment flow on staging appears only in the provider's test dashboard
curl -I https://staging.example.com/without credentials returns401- the deployed image tag on staging matches the tag promoted to production
Explanation
Staging is a rehearsal. It is only useful if the rehearsal uses the same script (artifact), the same stage (infrastructure shape), and fake props (data and external services). Parity catches configuration and migration failures; isolation keeps rehearsal mistakes from reaching customers; promotion of a single artifact guarantees that what passed staging is exactly what reaches production.
Edge cases / notes
Preview environments per pull request
Short-lived environments per branch are useful for review, but they rarely match production closely. Keep a long-lived staging environment as the final gate.
Cost control
Staging can use smaller instances and a single server as long as the software stack matches. Scale down or stop it outside working hours if your platform supports it.
Feature flags
Flags let you ship code to production dark and test it in staging with the flag on. Keep staging and production flag states documented so differences are intentional.
Keeping staging trustworthy over time
A staging environment decays unless someone owns it. Warning signs:
- releases are often "tested in staging" days before they go to production, with other changes merged in between
- staging has manual configuration changes nobody recorded
- the team routinely ignores failures in staging because "staging is always broken"
- staging's database schema differs from production because a migration was faked or reverted by hand
Practices that keep it useful:
- Deploy staging from automation only. No manual
git pullon staging servers. If a fix is needed, it goes through the pipeline. - Recreate rather than repair. Periodically rebuild staging from infrastructure code and a fresh anonymized dataset, which removes accumulated drift.
- Treat staging failures as release blockers. A failing smoke test on staging stops promotion to production.
- Compare configuration. Script a check that lists environment variable names (not values) in staging and production and reports differences.
- Assign ownership. One team or person is responsible for staging's health, the same as for production.
Comparing configuration safely
A simple approach lists only the keys:
ssh staging 'sudo cut -d= -f1 /etc/myapp/env | sort' > /tmp/staging-keys
ssh prod 'sudo cut -d= -f1 /etc/myapp/env | sort' > /tmp/prod-keys
diff /tmp/staging-keys /tmp/prod-keys
Expected differences (for example STAGING_EMAIL_RECIPIENT) should be documented; unexpected ones are drift to fix.
Load and performance testing
Staging is a natural place for load tests, as long as it is sized proportionally and isolated. Results on smaller hardware are not directly comparable to production, but relative changes between releases (a new endpoint that is five times slower) are meaningful. Run load tests against endpoints with realistic data volumes, and never point them at third-party services in test mode that have strict rate limits.
Common mistakes
- Giving staging production API keys "temporarily" to debug an integration.
- Letting staging's Celery Beat run customer-facing jobs such as billing or reminders.
- Exposing staging's admin with the same superuser passwords as production.
- Forgetting to renew staging's TLS certificate, which trains the team to click through certificate warnings.
Next steps
- Automate the promotion flow with Deploy Django with GitHub Actions CI/CD.
- Plan schema changes with Django Migration Strategy for Safe Deployments.
- Standardize smoke tests with the Post-Deploy Verification Checklist for Django.
FAQ
Should Django staging use DEBUG=True?
No. Staging should run with DEBUG=False like production, otherwise errors in static file handling, error pages, and security settings stay hidden until production.
Can staging share a database with production?
No. A shared database lets staging migrations, test data, and bugs affect real users. Use separate database servers or at least separate databases and roles with no cross-access.
How do I stop a Django staging site from sending real emails?
Point staging at a test inbox or sandbox SMTP service, and add a backend that rewrites all recipients to a staging address as a safety net.
Is it safe to copy production data into staging?
Only after anonymizing personal data in an isolated environment. Raw copies require staging to meet the same security and compliance standards as production.