Django hosting on Fly.io: a Python framework on global infrastructure
Install flyctl on GNU/Linux
$ curl -L https://fly.io/install.sh | sh
Run from Django project root
$ fly launch
Scale CPU, memory, instances & regions
$ fly scale
Quick and Easy Deployments
Fly.io makes deploying your Django app a snap. No servers to set up, no load balancer to configure and no tricky processes. Simply launch, deploy and scale your Django apps across the globe in minutes, provisioning memory, storage and CPU resources with just a few commands. Into automation magic? Deploy your changes with GitHub Actions.
Deploy Your App Where Your Users Are, Literally
Deploy your Django app closer to your users, to any of our 18 regions around the world, no matter where they are in the world. Fly.io expansive global network ensures that your applications run with lightning-fast speed and reliability.
Managed Postgres for Django Projects
Focus on features and leave database management to Fly.io. Our Managed Postgres service, gives you:
- Automatic backups and recovery
- High availability with automatic failover
- Performance monitoring and metrics
- Resource scaling (CPU, RAM, storage)
- 24/7 support and incident response
- Automatic encryption of data at rest and in transit
Seamless Scaling, Smooth Performance.
Fly.io automatically scales your Django app based on demand, handling traffic spikes effortlessly. Built-in load balancing ensures even traffic distribution, maintaining optimal performance and reliability without overloading any single instance.
Save Costs With On-Demand Scaling
No traffic? No worries. With Fly Proxy, Django instances can dynamically stop based on incoming requests, optimizing costs and resources during idle periods. Take full control with Fly Machines' REST API, enabling direct interaction with your instances.
Safe and Sound, All Around
Deploy your Django app with Fly.io's robust security features. Benefit from encrypted private networking, automated SSL/TLS certificate management, and secure data volumes. With Fly.io, you can focus on crafting exceptional Django apps, knowing that your infrastructure is fortified.
Deploy a Django app to Fly.io: the actual flow
The flow is three commands: install flyctl, run fly launch, then fly deploy for every change after that.
Install the CLI:
curl -L https://fly.io/install.sh | sh
flyctl is the only tool you need locally. Authenticate with fly auth login once.
Kick off your first deploy from your Django project root:
cd path/to/your/django/app
fly launch
fly launch detects Django by reading requirements.txt or pyproject.toml, picks a region close to you, generates a Dockerfile and a fly.toml in your repo, and auto-creates a SECRET_KEY Fly secret so the app boots on first run. The wizard offers to provision Postgres and Redis; accept whichever you need now and add the rest later.
For every change after the first deploy, run fly deploy. That rebuilds the image, pushes it to our registry, and rolls a new version onto your Machines with no downtime.
Configure ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS
Django blocks any host that isn't in ALLOWED_HOSTS, so a fresh Fly.io deploy returns 400 errors until you wire it up. Every Machine has a FLY_APP_NAME environment variable set automatically; use it to derive the hostname:
import os
ALLOWED_HOSTS = [f"{os.environ['FLY_APP_NAME']}.fly.dev"]
If you've attached a custom domain, append it to the list.
POST endpoints also need CSRF_TRUSTED_ORIGINS, or form submissions get rejected with a 403:
CSRF_TRUSTED_ORIGINS = ["https://*.fly.dev", "https://www.example.com"]
Use the HTTPS scheme prefix and exact hostnames. Subdomain wildcards are fine; the scheme has to be explicit.
Serve static files with WhiteNoise
WhiteNoise is the standard way to serve Django static files from the app process itself, and it's what we recommend on Fly.io. Install it, add the middleware in the right order, point Django at the collected directory, and run collectstatic at build time.
In settings.py:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# the rest of your middleware
]
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
WhiteNoiseMiddleware has to come right after SecurityMiddleware. CompressedManifestStaticFilesStorage gives you fingerprinted filenames plus gzip and brotli encoding, so far-future caching works correctly.
Run collectstatic during the Docker build, not at boot. Add this to your Dockerfile:
RUN python manage.py collectstatic --noinput
This belongs in the build, not in [deploy] release_command. release_command runs on a temporary Machine that gets torn down, so anything it writes to disk vanishes before your real Machines boot.
Attach Managed Postgres and run migrations
The recommended path is Fly.io Managed Postgres. MPG handles automatic backups and recovery, high availability with automatic failover, performance monitoring, resource scaling, and encryption of data at rest and in transit. We no longer support the older unmanaged fly postgres path; MPG is the supported way to run Postgres on Fly.io.
Provision a cluster and attach it:
fly mpg create
fly mpg list
fly mpg attach your-cluster-id -a your-app-name
fly mpg create walks through plan, region, and volume size. fly mpg list shows the cluster ID once it's ready. fly mpg attach writes a DATABASE_URL secret onto your app, reboots it, and uses a PGBouncer-pooled URL so connection counts stay sane under load.
Migrations don't run automatically. Wire them into [deploy] in fly.toml so every release applies pending migrations before traffic hits the new Machines:
[deploy]
release_command = "python manage.py migrate"
release_command runs on a temporary Machine with the production environment, including DATABASE_URL, before any web Machine takes traffic. If migrations fail, the deploy aborts and your previous version keeps serving. Check fly logs for the migration output if a deploy returns 500s right after rollout.
What you didn't have to set up
Look back at what actually happened. You ran fly launch, then fly mpg create and fly mpg attach. A standard Django app with a Postgres database in one region is the ordinary case Fly.io is built for, not a stretch. Here is the work that never appeared:
- A hosting service for your code.
fly launchandfly deploybuild from the directory you are standing in. A Django project with no git remote at all deploys fine. - A Dockerfile.
fly launchdetects Django and writes one, along with thefly.toml. Both land in your repository where you can read them, edit them, and see them in a diff. - A connection string.
fly mpg attachwritesDATABASE_URLonto the app as a secret. Yoursettings.pyreads it the way it always has, and the password never lands in your clipboard or your shell history. - A migration step in a deploy pipeline.
fly launchwritesrelease_commandintofly.toml, sopython manage.py migrateruns on a throwaway Machine before any Machine takes traffic. A migration that fails fails the deploy, instead of half migrating production. - TLS and a hostname. The app answers on
<app>.fly.devwith HTTPS from the first request, and the certificate is issued and renewed for you. - A load balancer or a VPC. Fly Proxy is already in front of your Machines, and your app reaches Postgres over your organization's private network as soon as the cluster is attached.
None of that work is missing. It is done, and the record of it is a fly.toml you can read in one screen, diff in a pull request, and revert with git.
Run Celery workers in their own process group
fly launch doesn't detect Celery; you wire it up by hand. Add a [processes] block to your fly.toml that defines a web group and a worker group, and pin http_service to the web group so HTTP traffic doesn't hit the worker Machines:
[processes]
web = "gunicorn myproject.wsgi"
worker = "celery -A myproject worker --loglevel=info"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
processes = ["web"]
Scale them independently:
fly scale count web=2 worker=4
fly deploy rolls out a new image to both groups simultaneously. Workers and web processes share the same Docker image and the same DATABASE_URL, but receive completely different commands at boot.
Celery needs a broker. If you accepted the Redis prompt during fly launch, the REDIS_URL secret is already set; point Celery at it in settings:
CELERY_BROKER_URL = os.environ["REDIS_URL"]
CELERY_RESULT_BACKEND = os.environ["REDIS_URL"]
For scheduled tasks, add a third process group running celery beat. Beat is a singleton (multiple beat schedulers would emit duplicate jobs), so scale it to exactly one Machine: fly scale count beat=1.
Manage secrets
Fly.io secrets are encrypted at rest, injected as environment variables when each Machine boots, and trigger a rolling restart when you change them.
fly launch sets SECRET_KEY for you on the first run, so you don't need to generate one or call fly secrets set for it. Confirm with:
fly secrets list
For everything else (Stripe keys, OAuth client secrets, custom DATABASE_URL overrides, third-party API credentials), use fly secrets set:
fly secrets set STRIPE_SECRET_KEY=sk_live_... GOOGLE_OAUTH_CLIENT_SECRET=...
Setting one or more secrets triggers a rolling restart. Your Django settings read them the same way you'd read any environment variable:
STRIPE_SECRET_KEY = os.environ["STRIPE_SECRET_KEY"]
Don't put secrets in fly.toml. The [env] block is for non-sensitive values (PORT, log levels, feature flags); anything you wouldn't put in a Git-tracked .env file belongs in fly secrets set.
Run Django across multiple regions
Django apps deploy in one region by default. To run in more, use fly scale count:
fly scale count 3 --region ord,fra,nrt
That places Machines in Chicago, Frankfurt, and Tokyo. Fly.io's proxy routes each incoming request to the nearest healthy Machine, so users in Europe hit Frankfurt, users in Asia hit Tokyo, and so on. Machines boot in a few seconds, so traffic spikes scale by adding Machines rather than queuing requests behind one overloaded process.
The piece this leaves open is the database. A Managed Postgres cluster lives in one region with a primary and a replica for in-region high availability. There are no cross-region read replicas; reads from Frankfurt back to a Chicago primary add latency you can't hide at the app layer.
The pragmatic move is to keep MPG in the region where most writes originate, run Django Machines globally in front of it, and use caching (Upstash Redis, Django's per-view cache, fragment caches, HTTP caching) to absorb hot read paths near the edge. For most Django apps, this handles the latency story without a wholesale data-layer rewrite.