Deploy a Django app with fly.io

Oct 4 2022
8 minutes

I started a new project a few days ago built with Django. Heroku is killing their free tier, and I’ve read that Fly.io is the cool new thing, so I decided to try it out.

Overall, Fly has great documentation. Their Language and Framework Guides are pretty comprehensive, but in a list that includes popular frameworks like Rails and Laravel alongside less boring options like RedwoodJS and Remix, I couldn’t help but notice Django’s conspicuous absence.

I was able to get everything working great with Django and Fly.io after some trial and error, so I wanted to write up my process to make it easier for people going forward.


If you want to get everything set up when starting a new project, use the startproject template I made which you can find at github.com/davish/django-flyio-template. Read on if you’re interested in a more in-depth look!


As opionated as Django is, there’s still some choices you need to make when starting a project. Here’s my stack that I’ll use throughout this post:

0. Initialize the Poetry project and Django App

If you’re starting an app from scratch, then run these commands from inside an empty directory where you want to store your project:

$ poetry init
$ poetry add django dj-database-url daphne psycopg2-binary
$ poetry run django-admin startproject <project> .

1. Add the Dockerfile

Create a file called Dockerfile in the project’s base directory:

FROM python:3

ENV PYTHONUNBUFFERED 1

WORKDIR /app

COPY poetry.lock pyproject.toml /app/

RUN pip3 install poetry
RUN poetry install --no-root

COPY . .

ENV DJANGO_SETTINGS_MODULE "<project>.settings"
ENV DJANGO_SECRET_KEY "this is a secret key for building purposes"

RUN poetry run python manage.py collectstatic --noinput

CMD poetry run daphne -b 0.0.0.0 -p 8080 <project>.asgi:application

It’s also important to also add a .dockerignore file, especially if you’re storing your site in a Git repository.

.git/
__pycache__/

fly.toml
*.sqlite3

# Files that store secrets
.env

# macOS file
.DS_Store

# Editor-specific configuration
.idea/
.vscode

2. Configure the Fly.io app

  1. Run flyctl launch --no-deploy to create a project and get a name and URL. --no-deploy prevents flyctl from deploying our app right out of the gate. We have to make some more changes before we can do that.
  2. The last command will have generated a fly.toml file. Open it up and add these lines, starting with the existing empty [env]block:
[env]
  DJANGO_SETTINGS_MODULE = "<project>.settings"

[deploy]
  release_command = "poetry run python manage.py migrate"

[[statics]]
  guest_path = "/app/static"
  url_prefix = "/static"
$ flyctl postgres create
# Make a note of the db app name you choose.
$ flyctl postgres attach <db-name>
# db-name should be what you selected in the previous step.
  • The postgres attach subcommand creates a properly permissioned database user and generates a connection string that’s stored as a secret named DATABASE_URL.

4. Configure Django settings

Back in our Django project, there’s a bit of configuration that we’ll need to do in the project’s settings.py file.

The DATABASES dictionary

import os
import dj_database_url
# ...
DATABASES = {
    "default": dj_database_url.config(
        default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
    )
}

dj-database-url will pick up the connection string that flyctl postgres attach set in our environment and properly configure our database connection for us. To avoid dealing with Postgres locally, however, we’ll default to a SQLite database when we don’t have a database URL defined.

Static files

Django collects static files through the collectstatic command that we run in our Docker build process. We both need to tell Django where to store static files on the filesystem (STATIC_ROOT) and at what relative path these static files will be served from (STATIC_URL). These need to match The guest_path and url_prefix from our fly.toml file, respectively. guest_path is /app/static because we placed our app to the /app directory in our Dockerfile.

STATIC_URL = "static/"
STATIC_ROOT = "static"

With Heroku and some other hosting providers, you’d have to set up and configure WhiteNoise and serve static files through Django. I think it’s great that Fly makes it so easy to bypass the running web server and serve static files directly with so little configuration. It’s a nice bonus that it maps so cleanly on to Django’s own configuration as well.

Allowed hosts

Run flyctl status if you’ve forgotten what domain Fly.io has set aside for your app. Once you’ve got that hostname, set these two settings:

ALLOWED_HOSTS = ['hostname.fly.dev']
CSRF_TRUSTED_ORIGINS = ['https://hostname.fly.dev']

Generate a new secret key

Django generates a secret key by default and stores it in plaintext in your settings file. This is dangerous in production, since secrets should never be stored in source files where they can get caught up in verison control. As its name implies, secret keys should be kept secret, and are important for validating session cookies and other cryptographic uses throughout the framework.

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")

And then run:

flyctl secrets set DJANGO_SECRET_KEY=<generated secret key>

You can store any other sensitive values in Fly’s secrets — check out the docs for more info.

5. Prepare for deploy

Fly.io will migrate our database for us on every deploy, so now is a good time to make any changes to your app locally that you’d like to have before that first migration. Most notably, this will likely include a custom User model.

If you’re adding configuration to an existing Django app, there’s probably nothing to be done here.

6. Deploy to fly.io

We’re finally ready to deploy our app! Run flyctl deploy --local-only, and the tool will build your Dockerfile and push it up to Fly.io. I needed to use --local-only because the remote builder in my account had shut down and I had no way of turning it back on, but your mileage may vary. Local builds require Docker to be installed and running locally.

Try visiting your site, and see if you can see the default Django welcome page!

7. Create admin user

The last thing that you’ll need to do is create a superuser for accessing Django’s built-in admin site. Luckily, we can run any management commands against our production server just by sshing right into the box:

$ flyctl ssh console
# From inside the ssh session, run:
> cd app
> poetry run python manage.py createsuperuser

Next Steps

And that’s it! You should have a fully functional Django app on Fly.io with a Postgres database, and hopefully you learned some useful flyctl commands along the way.

What I’ve presented here is a “minimum viable deployment”, but to make sure your deployment is safe for users and not vulnerable to attacks, it’s important to look through Django’s deployment checklist. Some things have already been addressed throughout this article, but items like the DEBUG setting are important to be aware of for production Django.

webdev django fly.io devops