Django hosting on Fly.io: a Python framework on global infrastructure

Build and scale Django apps on a developer-focused public cloud.

Launch Now

Ready, Set, Go!

Speedrun Your Django App Onto Fly.io

Deploy a Django app to Fly.io in a few minutes with the Fly.io command line tool, flyctl.

Get Started
> 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

Not on GNU/Linux? Install flyctl for your platform.

Quick and Easy Deployments

From Local to Live, in No Time!

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.

Global Reach, Instant Speed

Deploy Your App Where Your Users Are — Literally

Deploy your Django app closer to your users, to any of our 30+ data centers 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.

NEW!

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
Learn More

Load Balancing Wizardry

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.

Idle-Free Infrastructure

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.

Rock-Solid Security

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.

  • Globally Distributed Storage

    Tigris is a globally distributed, S3-compatible object storage service built on Fly.io infrastructure. It minimizes latency by keeping files close to where they're needed most, intelligently routing and caching data based on global traffic patterns. This ensures fast, CDN-like performance with zero configuration.

  • Managed Databases & Services

    Using Postgres? Opt for Supabase. Is SQLite enough? Choose LiteFS. Seek a popular in-memory caching? Provision it on Upstash. With Fly.io, you can integrate fully managed services right next to your app, ensuring low latency and high performance.

  • Built-in Observability

    Monitor, troubleshoot, and fine-tuned your Django apps with real-time metrics and logging. Gain insights into your app's performance with Fly.io's built-in observability tools accessible directly from the dashboard.

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.

The generated Dockerfile installs your Python dependencies, runs collectstatic at build time, and starts gunicorn on the PORT your fly.toml exposes. The generated fly.toml has an http_service block that maps that PORT to Fly.io's edge proxy. Most apps still need to edit settings.py for ALLOWED_HOSTS and database config before the first request lands cleanly. Both are covered below.

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.

Two failure modes show up on first deploys. First, if DATABASE_URL is missing, the app crashes on import. Run fly secrets list to confirm it's set; if not, the attach either didn't complete or pointed at the wrong app. Second, if your settings.py opens a database connection at module load time (a third-party app that calls connection.cursor() at import, a startup check at the top of a file), the build's collectstatic step will fail because the builder can't reach Postgres. Move connection-dependent code into a function or an AppConfig.ready hook.

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.