- English
- français
- Deutsche
- Contact us
- Docs
- Login

This blog post is based on a product presentation by Antonis Kalipetis, Staff Engineer at SymfonyCon 2023, about Upsun's experience with Nix. We utilized ChatGPT for transcription and to enhance the grammar and syntax.
Django deployments have evolved since the framework's early days. While Django migrations were introduced in version 1.6 to simplify our lives, modern deployment patterns, such as rolling updates and horizontal scaling, have introduced new complexities that developers need to understand and plan for. In a live workshop, Antonis walked through what actually breaks during deployments and how to prevent it.
Every Django application deployment consists of several key components working together. At the front, you have a proxy server - typically Nginx, Apache, or newer options like Caddy and Traefik. This proxy handles TLS termination, domain routing, and redirects. Behind it sits your application server, commonly Gunicorn or Uvicorn for asynchronous applications. Supporting these are a cache layer (such as Redis or Memcached) and a database (PostgreSQL, MySQL, or increasingly SQLite, for certain use cases).
This architecture works well for single-server deployments, but modern applications often run across multiple servers with rolling updates, creating scenarios where different versions of your application run simultaneously.
When you develop Django applications locally, manage.py runserver handles static file serving automatically. In production, however, this approach becomes problematic. Python application servers are designed to handle one request at a time (with some exceptions for ASGI), making them unsuitable for serving static content, such as CSS, JavaScript, and images.
The real challenge arises when multiple application servers are running different versions of your code. Consider this scenario: you update your CSS with a new brand color and deploy using Django's ManifestStaticFilesStorage. This storage backend creates hashed filenames, such as app.a1b2c3d4.css to enable permanent caching.
During a rolling deployment, you might have:
app.old123.cssapp.new456.cssIf a user's request for the new CSS file gets routed to the old server, they'll receive a 404 error, breaking the page styling.
External storage: Upload static files to services like Amazon S3 or similar cloud storage. This ensures all versions of your application can access the same static files.
Proxy-level caching: Configure your proxy server (Nginx, etc.) to cache static files for a reasonable duration. This creates a buffer during deployments where old files remain available while new ones propagate.
Shared file systems: Mount the static directory across all servers so both old and new application versions can serve files from the same location.
WhiteNoise middleware: This Python library serves static files directly from your Django application with efficient caching headers, though it does use application server resources.
Django migrations are powerful, but they require careful planning in multi-version environments. The presenter demonstrated this with a practical example using a Bigfoot sighting reporting application.
Adding a new field to a model seems straightforward, but consider what happens when you add a required field with a default value:
# New field added
gdpr_accepted = models.BooleanField(default=False)After running migrations, the new application version is aware of this field, but the old version is not. When the old version tries to save a record, it fails because the database expects the gdpr_accepted field, but the old code doesn't provide it.
The presenter highlighted Django 5's new db_default parameter as a solution:
gdpr_accepted = models.BooleanField(db_default=False)With db_default, the database itself handles the default value, not the Python code. This means old application versions can successfully save records even without knowledge of the new field.
Rolling updates create a temporary state where multiple versions of your application run simultaneously. This requires careful coordination between:
While Django makes rolling back migrations seem simple with manage.py migrate app_name 0001, production rollbacks are rarely that straightforward. The presenter emphasized from experience that rollbacks often encounter unexpected issues, making prevention through careful planning more valuable than relying on rollback capabilities.
Modern Platform-as-a-Service providers like Upsun address many of these challenges through:
However, understanding the underlying challenges remains crucial even when using managed platforms.
Django's built-in tools, such as migrations and static file management, are powerful, but they require thoughtful application in production environments. The complexity increases significantly when you move from single-server deployments to horizontally scaled, continuously deployed applications.
Success requires:
As Django applications grow and deployment patterns become more sophisticated, developers must evolve their understanding beyond local development patterns. The framework provides the tools, but successful production deployments require careful orchestration of these tools within your specific infrastructure context.
While Django continues to make our development lives easier, production deployment remains an area where careful planning and understanding of the underlying systems pays dividends in application reliability and user experience.
Join our monthly newsletter
Compliant and validated