Django apps on Fly.io have a hidden performance bottleneck related to how the application server (like Gunicorn) interacts with the Fly.io proxy.
Here’s how to deploy your Django app and avoid common pitfalls.
1. Project Setup
Make sure your Django project is ready. You should have a requirements.txt with all your dependencies, including django and gunicorn.
pip freeze > requirements.txt
2. fly.toml Configuration
This file tells Fly.io how to build and run your application.
app = "your-django-app-name"
primary_region = "ord" # Or your preferred region
kill_signal = "SIGINT"
restart_seconds = 5
[build]
image = "python:3.10"
builtin = "django" # This is a shortcut for Django projects
[env]
# Set your Django SECRET_KEY here. NEVER commit this directly.
# Use `fly secrets set SECRET_KEY="your_super_secret_key"`
SECRET_KEY = "placeholder_for_local_testing"
DEBUG = "0" # Set to "1" for local development if needed
DATABASE_URL = "postgres://user:password@host:port/dbname" # Example, use Fly Postgres
[processes]
web = "gunicorn --bind 0.0.0.0:8000 --workers 4 your_project.wsgi:application"
[services]
internal_port = 8000 # The port Gunicorn listens on
protocol = "tcp"
ports = [{ port = 80, handlers = ["http"] }]
Key Points:
builtin = "django": Fly.io has a pre-built image for Django that handles many common configurations.kill_signal = "SIGINT": Ensures Gunicorn receives the interrupt signal gracefully.web = "gunicorn ...": This is your application server command.your_project.wsgi:applicationshould be replaced with your actual Django project’s WSGI application path.internal_port = 8000: This must match the--bindport in your Gunicorn command.ports = [{ port = 80, handlers = ["http"] }]: This exposes your app on port 80, the standard HTTP port.
3. wsgi.py Adjustment
Your wsgi.py file needs to be accessible. Usually, it’s in your project’s main directory (e.g., myproject/wsgi.py). The gunicorn command uses this path.
4. Database Setup (Fly Postgres)
It’s highly recommended to use Fly.io’s managed PostgreSQL.
fly postgres create --name your-db-name --region ord
Once created, you’ll get a DATABASE_URL. Set it as a secret:
fly secrets set DATABASE_URL="postgres://user:password@host:port/dbname?sslmode=disable"
Important: Replace ?sslmode=disable with ?sslmode=require if your database requires SSL.
5. Static Files
For production, you must serve static files correctly. Django’s development server shouldn’t be used.
Add django-storages and psycopg2-binary (or psycopg2 if you compile it) to your requirements.txt.
pip install django-storages psycopg2-binary
pip freeze > requirements.txt
Configure settings.py:
# settings.py
import os
# ... other settings ...
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Directory where collectstatic will gather files
# For S3 (or other compatible storage)
# You'll need to set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME
# and AWS_S3_ENDPOINT_URL (if not using AWS S3) as Fly secrets.
# Example for MinIO (often used with Fly Object Storage)
# AWS_ACCESS_KEY_ID = os.environ.get("MINIO_ACCESS_KEY")
# AWS_SECRET_ACCESS_KEY = os.environ.get("MINIO_SECRET_KEY")
# AWS_STORAGE_BUCKET_NAME = os.environ.get("MINIO_BUCKET_NAME")
# AWS_S3_ENDPOINT_URL = os.environ.get("MINIO_ENDPOINT")
# AWS_S3_USE_SSL = False # Set to True if your endpoint uses SSL
# DEFAULT_FILE_STORAGE = 'storages.backends.s3.S3Storage'
# STATICFILES_STORAGE = 'storages.backends.s3.S3Storage'
# If you're NOT using S3/Object Storage, you might need a different approach
# like a dedicated CDN or serving from a separate Nginx instance.
# For Fly.io, serving static files directly from your Django app with Gunicorn
# is generally discouraged for performance and scalability.
# If you must, ensure your `collectstatic` command is part of your build process
# and that your web server (Fly proxy) is configured to serve them.
# However, the `builtin = "django"` image often handles this by expecting
# static files to be in a `static` directory after `collectstatic`.
To collect static files during deployment:
Modify your fly.toml to include a predeploy script:
[deploy]
strategy = "rolling" # Or your preferred strategy
[processes]
web = "gunicorn --bind 0.0.0.0:8000 --workers 4 your_project.wsgi:application"
# Add a release command to run collectstatic
release_command = "python manage.py collectstatic --noinput"
[build]
image = "python:3.10"
builtin = "django"
[env]
# ... your env vars ...
[services]
# ... your services ...
This release_command will run collectstatic on a new instance before it’s switched into production traffic.
6. Deployment
Initialize Fly.io in your project directory:
fly launch
Follow the prompts. It will detect your Django project and suggest a fly.toml.
Deploy:
fly deploy
The Hidden Performance Bottleneck: Gunicorn and the Fly Proxy
The most common performance issue with Django on Fly.io isn’t Django itself, but the interaction between Gunicorn and the Fly.io edge proxy. By default, Fly.io proxies your traffic to the internal_port specified in fly.toml. Gunicorn, when listening on 0.0.0.0:8000, creates a socket. The Fly proxy then forwards requests to this socket.
The problem arises because Gunicorn, by default, doesn’t have specific optimizations for handling proxied requests, especially concerning X-Forwarded-Proto and X-Forwarded-For headers. When the Fly proxy sends a request, it sets these headers to inform the application server about the original client’s IP and protocol (HTTP vs. HTTPS).
If your Django app isn’t configured to trust these headers, it might:
- Incorrectly determine the protocol: Assume all traffic is HTTP, even if the original request was HTTPS, leading to broken redirects or mixed content warnings.
- Show internal proxy IPs: Log the IP of the Fly proxy instead of the actual client IP, hindering accurate analytics and IP-based restrictions.
The Fix:
You need to tell Gunicorn and Django to trust the proxy headers.
Gunicorn Configuration:
Modify your Gunicorn command in fly.toml to include the --forwarded-allow-ips and --proxy-protocol flags.
[processes]
# Bind to a specific IP and port that Gunicorn can manage with X-Forwarded-* headers.
# 127.0.0.1 is generally preferred for internal communication.
web = "gunicorn --bind 127.0.0.1:8000 --workers 4 --forwarded-allow-ips='*'"
--bind 127.0.0.1:8000: Binds Gunicorn to the local interface, making it explicit that it’s only listening for internal connections.--forwarded-allow-ips='*': This tells Gunicorn to trust theX-Forwarded-*headers from any IP. For greater security in more complex setups, you can restrict this to specific Fly.io proxy IP ranges if known, but*is common and usually sufficient on Fly.
Django Configuration (settings.py):
Ensure your Django settings.py is configured to trust proxy headers.
# settings.py
# ... other settings ...
USE_X_FORWARDED_HOST = True # Tells Django to trust X-Forwarded-Host
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Tells Django when to consider the connection secure
# Optional: If you want to use the actual client IP for logging and other purposes
# LOGGING = {
# 'version': 1,
# 'disable_existing_loggers': False,
# 'formatters': {
# 'verbose': {
# 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
# 'style': '{',
# },
# },
# 'handlers': {
# 'console': {
# 'class': 'logging.StreamHandler',
# 'formatter': 'verbose'
# },
# },
# 'root': {
# 'handlers': ['console'],
# 'level': 'INFO',
# },
# }
USE_X_FORWARDED_HOST = True: This is crucial. WhenTrue, Django will use theX-Forwarded-Hostheader to determine theHostheader for generated URLs.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https'): This tells Django that if theX-Forwarded-Protoheader is present and its value ishttps, it should treat the connection as secure. This is vital for generating correct HTTPS URLs and preventing mixed content issues.
By making these changes, your Django application will correctly interpret requests coming through the Fly.io proxy, ensuring proper security, redirects, and logging.
The next thing you’ll likely encounter is managing background tasks or asynchronous operations, which often involves services like Celery or RQ with a separate worker process on Fly.io.