Deploy a Django app with fly.io
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:
- Poetry for package management.
- Daphne for the production webserver.
- Docker to describe the deployment.
- Fly also has support for heroku-style buildpacks and Procfiles.
- Postgres for the production database, since Fly has built-in support for Postgres.
- Local development still uses SQLite.
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
- Run
flyctl launch --no-deploy
to create a project and get a name and URL.--no-deploy
preventsflyctl
from deploying our app right out of the gate. We have to make some more changes before we can do that. - 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"
3. Link a Postgres Database
$ 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 namedDATABASE_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 ssh
ing 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.