Django hosting on Fly.io: migrations, workers and Postgres from one fly.toml

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 in minutes, provisioning memory, storage and CPU resources with just a few commands. Into automation magic? Deploy your changes with GitHub Actions.

Safe Releases

Migrations Run Before Any Machine Updates

Put release_command = "python manage.py migrate" under [deploy] in fly.toml, and every fly deploy runs your migrations once, in a temporary Machine booted from the new image, before any Machine is updated. If a migration fails, the deploy stops and the current version keeps serving.

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)
  • Automatic encryption of data at rest and in transit
Learn More

Load Balancing Wizardry

Fly Proxy Balances the Load for You.

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.

  • S3-Compatible Object Storage

    Tigris is S3-compatible object storage run on Fly.io. Run fly storage create and the bucket credentials are set as secrets on your app, so your Django storage backend reads them from the environment and no keys get copied into settings.py.

  • Managed Databases & Services

    Using Postgres? Opt for Supabase. Is SQLite enough? Choose LiteFS. Seek a popular in-memory caching? Provision it on Upstash. Fly.io integrates with these managed services, so you don't run them yourself.

  • 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, suggests a region, 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 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.

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 launch and fly deploy build from the directory you are standing in. A Django project with no git remote at all deploys fine.

A Dockerfile. fly launch detects Django and writes one, along with the fly.toml. Both land in your repository where you can read them, edit them, and see them in a diff.

A connection string. fly mpg attach writes DATABASE_URL onto the app as a secret. Your settings.py reads 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 launch writes release_command into fly.toml, so python manage.py migrate runs 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.dev with 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.