---
title: "Django hosting on Fly.io: migrations, workers and Postgres from one fly.toml"
description: "Django hosting on Fly.io. fly launch detects Django and writes the Dockerfile and fly.toml; migrations run before each deploy, with Managed Postgres a command away."
---

# 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](https://fly.io/speedrun)

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](https://docs.fly.io/django/getting-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.](https://docs.fly.io/flyctl/install)

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](https://fly.io/mpg/)

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.

## Trusted by teams at

- **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.

## Launch Your Django App into Orbit

Fly.io is tailored for Django developers who want deploys to be one command. fly launch gets your Django app running in minutes, and fly scale count adds Machines when it needs more. Say goodbye to infrastructure headaches and focus on building outstanding applications.

[Get Started](https://fly.io/app/sign-up)

## 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`.
