Operations
#django
#git
#systemd

How to Roll Back a Django Release Safely

A failed Django release can break more than page requests. It can also break background workers, scheduled jobs, static asset loading, startup commands, or database compatibility.

Problem statement

A failed Django release can break more than page requests. It can also break background workers, scheduled jobs, static asset loading, startup commands, or database compatibility. In production, a safe Django rollback means restoring service without making data integrity, schema state, or security problems worse.

The main risk is assuming that rollback only means “point the server at older code.” That works for some failures, but not all. If the bad release changed environment variables, applied migrations, rotated assets, or wrote incompatible queue payloads, code rollback alone may leave the app in a half-broken state.

A safe rollback process needs to answer three questions first:

  1. What actually failed?
  2. Can the previous release run safely against the current database and environment?
  3. Is rollback safer than a forward fix?

Quick answer

For a safe Django deployment rollback, use this order:

  1. Stop the rollout and freeze further changes.
  2. Identify whether the failure is code, config, migration, static assets, queues, or infrastructure.
  3. Put the system in a controlled state by pausing workers and scheduled jobs if needed.
  4. Switch traffic back to the last known good application release.
  5. Restart only the services that need the old release.
  6. Reverse database migrations only if you have confirmed the downgrade is safe and tested.
  7. Verify web requests, admin access, async workers, logs, static assets, and key business flows.
  8. Document the incident and stabilize before attempting another deploy.

If migrations were destructive or the bad release already changed production data in an incompatible way, a forward fix or backup restore may be safer than rolling back code.

Step-by-step solution

Identify what failed before you roll back

Determine the failure type

Classify the incident before touching production:

  • Bad application code: exceptions after startup, broken view logic, failed imports
  • Bad environment configuration: missing env vars, wrong ALLOWED_HOSTS, bad secrets, incorrect database or Redis settings
  • Broken migration: migration applied partially, app expects schema that no longer matches
  • Missing static assets: CSS/JS 404s, stale asset manifest, release points to wrong static directory
  • Process startup failure: Gunicorn or Uvicorn fails to boot, worker crash loop
  • Dependency or image issue: broken Docker image, bad Python package build, missing system libraries

Check logs before rolling back:

systemctl status gunicorn
journalctl -u gunicorn -n 100 --no-pager
systemctl status celery
journalctl -u celery -n 100 --no-pager

For Docker or Compose:

docker compose ps
docker compose logs --tail=100 web
docker compose logs --tail=100 worker

Decide whether rollback is safe

Code-only rollback is usually safe when:

  • no incompatible migration was applied
  • environment variables still match the previous release
  • queue payloads and scheduled jobs are still compatible
  • static assets for the old release are still available

Rollback is risky when:

  • columns were dropped or renamed
  • tables were renamed and old code expects the old schema
  • one-way data migrations already transformed production data
  • the failed release changed secrets or security-sensitive settings

In those cases, reverting to the previous release may mean a forward fix or backup restore, not just switching code.

Freeze the release

Stop automation first so the bad rollout does not continue.

Examples:

touch /srv/myapp/shared/DEPLOY_LOCK

Pause your CI/CD pipeline in its own UI, and disable any deployment webhook if you use one. Also make sure one operator owns the rollback so multiple people are not changing the same system at once.

Step 1: Put the system in a controlled state

If background jobs can write incompatible data, stop them before switching releases.

systemctl stop celery
systemctl stop celerybeat  # or your actual beat unit name, such as celery-beat

If using Compose:

docker compose stop worker beat

If the failed release published incompatible task payloads, do not restart old workers blindly. Inspect queued jobs first and decide whether they can be drained, discarded, rerouted, or handled by a forward fix.

If needed, enable maintenance mode for all traffic or only for admin users. Keep the health endpoint available so you can still verify recovery through the proxy.

Example Nginx health passthrough:

location /health/ {
    access_log off;
    proxy_pass http://app;
}

Step 2: Switch to the previous application release

For versioned Linux releases:

ls -1 /srv/myapp/releases
readlink -f /srv/myapp/current
ln -sfn /srv/myapp/releases/2026-04-20-120000 /srv/myapp/current

Verify the symlink:

readlink -f /srv/myapp/current

This is the cleanest safe Django rollback pattern because release artifacts stay immutable and you can move between known versions quickly.

Docker or Compose rollback

Roll back to a previously built image tag. Do not rebuild during an incident unless you are intentionally doing a forward fix.

docker compose ps
docker image ls
export IMAGE_TAG=2026-04-20-120000
docker compose up -d web worker

This assumes your compose.yaml uses ${IMAGE_TAG} in the image reference; if not, update the image tag in the Compose configuration or deploy using your normal image-pinning method.

Then inspect logs:

docker compose logs --tail=100 web

Make sure the previous image tag still matches current mounted volumes and environment variables.

Git-based rollback

For simpler setups:

cd /srv/myapp/app
git log --oneline -n 5
git checkout <previous-good-commit>

This is workable, but less reliable than immutable release directories or image tags because dependencies and build artifacts may drift. If you use this method, also verify that the virtualenv, Python dependencies, and built assets still match that commit before restarting services.

Step 3: Restart only the required services

Restart the app server and related services tied to the release.

systemctl restart gunicorn
systemctl restart celery
systemctl restart celerybeat  # or your actual beat unit name

If you use Uvicorn under systemd, restart that service instead. Reload Nginx only if its configuration changed:

nginx -t && systemctl reload nginx

Verification:

systemctl status gunicorn
systemctl status celery

Step 4: Handle database migrations carefully

Check current migration state first:

cd /srv/myapp/current
python manage.py showmigrations
python manage.py migrate --plan

Do not reverse migrations automatically as part of every rollback.

A downgrade may be acceptable only when:

  • the migration is reversible
  • no destructive schema change occurred
  • old code is compatible with the downgraded schema
  • you have tested the downgrade path in staging

Example, only if confirmed safe:

python manage.py migrate app_name 0012_previous

Avoid rollback migrations in production when:

  • columns were dropped
  • data was transformed in a one-way migration
  • new production writes depend on the new schema

In those cases, reversing migrations in production may cause more damage than the original release. If schema or data is corrupted, restoring from backup may be the only safe recovery path.

Step 5: Restore static assets consistency

If each release has its own static build, make sure the old release points to matching assets. If static files are shared, verify the manifest is still valid for the previous code.

If needed:

cd /srv/myapp/current
python manage.py collectstatic --noinput

Use this carefully. Running collectstatic during an incident is fine if your storage design supports it, but it changes state. If your deployment already versions static assets correctly, simply repointing the release is often enough.

Check asset responses in the browser and network logs, not just HTML status codes.

Step 6: Verify the rollback

Run application and infrastructure checks immediately.

curl -I https://example.com/health/
curl -I https://example.com/
cd /srv/myapp/current
python manage.py check --deploy

Then verify:

  • admin login page loads
  • one key read and write flow works
  • worker queue starts processing normally
  • scheduled jobs are restored only after web compatibility is confirmed
  • static assets load correctly
  • error rate and latency return to baseline

For logs:

journalctl -u gunicorn -n 100 --no-pager
journalctl -u celery -n 100 --no-pager

For Compose:

docker compose logs --tail=100 web worker

Explanation

A good Django release rollback process works because it separates code recovery from schema recovery. Most failed deploys are fixed by returning web and worker processes to the last known good release, but production incidents become dangerous when teams also reverse migrations, rotate services, and clear caches without deciding whether those changes are actually safe.

The safest default is:

  • freeze changes
  • roll back code
  • keep the database as-is unless a tested downgrade is clearly safe
  • verify web and async paths separately

This also explains why immutable releases matter. Symlinked release directories, tagged Docker images, and stored build artifacts make deployment recovery predictable. Git checkout on a live server is harder to reason about because code, virtualenv contents, and generated assets may not match.

Rollback patterns by deployment style

  • Symlinked releases on Linux: fastest manual rollback; current points to a previous directory while shared media and env files remain stable
  • Docker or Compose: switch to the prior image tag and restart services without rebuilding
  • systemd non-container deploys: revert the active code or virtualenv path and inspect journald after restart

Security and operational checks during rollback

During a rollback, check for drift:

  • confirm the previous release still supports current environment variables
  • verify SECRET_KEY expectations, because a mismatch can invalidate sessions or break signed data
  • confirm host and request settings such as ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, and DEBUG
  • confirm proxy and TLS-related settings still match production, including SECURE_PROXY_SSL_HEADER where applicable
  • revoke secrets if the failed release exposed credentials or unsafe config
  • make sure old workers can handle current queue payloads before consuming them
  • disable feature flags tied to the failed release
  • clear or invalidate caches if cached structures changed
  • confirm session compatibility if serializers or auth settings changed

When to automate this

Manual rollback becomes repetitive once you have multiple services, release directories, and health checks. That is usually the point to standardize deploy locks, previous-release detection, ordered restarts, queue checks, and post-rollback smoke tests in a reusable script or runbook template. The manual sequence should stay the reference path even if you automate it later.

Edge cases / notes

Reversible vs destructive migrations

Safe examples:

  • adding a nullable column
  • adding an index
  • adding a table not yet required by old code

Dangerous examples:

  • dropping columns
  • renaming tables used by old code
  • changing data formats with one-way migrations

Background jobs

A common rollback failure is restoring web code but leaving Celery workers on the bad release, or letting old workers consume messages created by new code. Pause workers first, then restart them on the same release as the web app.

If the bad release enqueued incompatible jobs, rolling back web code is not enough. Inspect the queue and decide whether jobs should be drained, purged, rerouted, or replayed after a forward fix.

Sessions, cache, and feature flags

If the failed deploy changed cache keys, serializers, session signing expectations, or feature-flag behavior, stale state can make rollback appear unsuccessful. Clear only the affected cache layer if needed, disable release-specific flags before reopening traffic, and verify that login and CSRF-protected forms still work.

Backups and restore

If the application wrote incompatible production data or an irreversible migration already ran, rollback may require database restore plus media reconciliation. That is a recovery event, not a normal deploy step, and should follow your backup validation runbook.

If you are building your rollback process, start with Django Deployment Checklist for Production. For deployment baselines, see Deploy Django with Gunicorn and Nginx on Ubuntu, Deploy Django ASGI with Uvicorn and Nginx, and Deploy Django with Caddy and Automatic HTTPS. For release safety around schema changes, read how to run Django migrations safely in production. If the release is still failing after rollback, use how to troubleshoot a failed Django deployment.

FAQ

Can I roll back a Django deployment without rolling back the database?

Yes. That is often the safest first move. Roll back the application release, then verify whether the old code can run against the current schema. Only reverse migrations if you have confirmed the downgrade path is safe.

When should I avoid reversing Django migrations in production?

Avoid it when migrations are destructive, one-way, or already changed live data in a way the old code cannot safely use. In those cases, a forward fix or backup restore is usually safer.

How do I roll back Celery workers after a bad Django release?

Stop workers and beat first, switch them to the same previous release or image tag as the web app, then restart them after web health is confirmed. Also check whether queued messages from the failed release are still compatible before letting old workers consume them.

What should I check immediately after a Django deployment rollback?

Check the health endpoint, homepage, admin login, one critical database write path, worker processing, service status, recent logs, and static asset loading. Also confirm that error rates drop back to normal.

When is a forward fix safer than a rollback?

A forward fix is safer when the bad release applied irreversible schema changes, transformed production data, changed queue contracts, or changed runtime assumptions that older code cannot understand. In those cases, forcing an older release back into production can extend the outage or damage data further.

2026 · django-deployment.com - Django Deployment knowledge base