How to deploy Django app on Fly.io
Deploy Django + PostgreSQL on Fly.io
In this guide we will develop a Django Todo application locally and then deploy it on Fly.io with a Postgres production database. There are a number of steps needed to convert a local Django project to be production-ready and then successfully deployed on Fly. We will cover them all here.
Initial Setup
From the command line navigate to an empty directory to store your code. On both Windows and macOS the desktop is a good default if you don't already have a preferred location. We will call create a new directory called django-fly
and then activate a new Python virtual environment within it.
# Windows
$ cd onedrive\desktop\code
$ mkdir django-fly
$ cd django-fly
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $
# macOS
$ cd ~/desktop/code
$ mkdir django-fly
$ cd django-fly
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
Next install Django, create a new project called django_project
, and run migrate
to initialize our database. Don't forget that period, .
, at the end of the startproject
command.
(.venv) $ python -m pip install django~=4.2.0
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py migrate
Start up Django's local web server with the runserver
command.
(.venv) $ python manage.py runserver
And then navigate to http://127.0.0.1:8000/
to see the Django welcome page.
Todo App
We will now build a simple Django Todo application from scratch. Stop the local web server with Control + c
and then use startapp
to create a new app called todo
.
(.venv) $ python manage.py startapp todo
Next register the app by adding it to the INSTALLED_APPS
configuration within the django_project/
settings.py
file.
# django_project/settings.puy
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"todo", # new
]
Models
We need a database model for our Todo app so create one now by updating the todo/
models.py
file with the following code. This creates a Todo
model with a single field, todo
, and also defines a human-readable __str__
method for that will be useful shortly in the Django admin.
# todo/models.py
from django.db import models
class Todo(models.Model):
todo = models.TextField()
def __str__(self):
return self.todo[:50]
Since we have created a new database model called Todo
we must make a migrations file and then migrate it.
(.venv) $ python manage.py makemigrations
(.venv) $ python manage.py migrate
Admin
One of Django's killer features is its built-in admin which we can use to manipulate and view data. To use it, first create a superuser account and respond to the prompts for a username, email, and password.
(.venv) $ python manage.py createsuperuser
Then update the existing todo/
admin.py
file so that our Todo
model is visible within the admin.
# todo/admin.py
from django.contrib import admin
from .models import Todo
admin.site.register(Todo)
Now we can start up the local web server again with the runserver
command:
(.venv) $ python manage.py runserver
In your web browser navigate to http://127.0.0.1:8000/admin/
and log in with your superuser credentials. On the admin homepage, click on the + Add
button next to Todos
and create at least one item. Click the Save
button once finished.
Views
In order to display this database content in our web app we need to wire up views, templates, and URLs. Let's start with the todo/
views.py
file. We'll use a basic Django ListView to display all available Todos from the database.
# todo/views.py
from django.views.generic import ListView
from .models import Todo
class HomePageView(ListView):
model = Todo
template_name = "todo/home.html"
Templates
In Django, template files display our data. Within the todo
app create a new directory called templates
, within it a directory called todo
, and then a file called home.html
.
Create a new directory called templates
within the todo
app. Inside that folder create a new file called home.html
. This is what the structure should look like:
├── todo
│ ├── ...
│ ├── templates
│ │ └── todo
│ │ └── home.html
The templates/home.html
file lists all the todos in our database.
<!-- templates/home.html -->
<h1>Django Todo App</h1>
<ul>
{% for text in todo_list %}
<li>{{ text.todo }}</li>
{% endfor %}
</ul>
URLs
Now for our URL routing. Create a new file called todo/
urls.py
.
# todo/urls.py
from django.urls import path
from .views import HomePageView
urlpatterns = [
path("", HomePageView.as_view(), name="home"),
]
Then in the existing django_project/
urls.py
file update the code so the Todos are displayed on the homepage:
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("todo.urls")), # new
]
Todo Homepage
Restart the local web server with the python
manage.py
runserver
command.
(.venv) $ python manage.py runserver
And navigate to our homepage at http://127.0.0.1:8000/
which now lists all Todos in our database.
Static Files
Static files such as images, JavaScript, or CSS require additional configuration. To demonstrate them we will add the Fly.io logo to our website.
Create a root-level static
folder for the Django project. The overall structure should now look like this:
├── db.sqlite3
├── django_project
│ ├── ...
├── manage.py
├── static
├── todo
│ ├── ...
Download the primary Fly.io logo and move it into the static
folder. Its default name is logo.svg
.
The django_project/
settings.py
file already has a configuration for STATIC_URL
but we need to add two more for static files to work correctly.
# django_project/settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"] # new
STATIC_ROOT = BASE_DIR / "staticfiles" # new
Now we can run the collectstatic
command to compile all static files from our project into the staticfiles
directory.
(.venv) $ python manage.py collectstatic
To add the Fly logo to our homepage update the existing template with {% load static %}
at the top and an <img>
tag.
<!-- templates/home.html -->
{% load static %}
<h1>Django Todo App</h1>
<img src="{% static 'logo.svg' %}" alt="Fly logo"
style="width:500px; height: 500px;">
<ul>
{% for text in todo_list %}
<li>{{ text.todo }}</li>
{% endfor %}
</ul>
Restart the local webserver with runserver
again.
(.venv) $ python manage.py runserver
And refresh the homepage at http://127.0.0.1:8000/
to see the new Fly logo on it.
Django Deployment Checklist
Django defaults to a local development configuration with the startproject
command. Many of the settings that are useful locally are a security risk in production. We therefore need a way to switch easily between local and production set ups. You can see the official guide here.
At a minimum we need to do the following:
install
Gunicorn
as a production web serverinstall
Psycopg
to connect with a PostgreSQL databaseinstall
environs
for environment variablesupdate
DATABASES
indjango_project/
settings.py
install
WhiteNoise
for static filesgenerate a
requirements.txt
fileupdate
DEBUG
,SECRET_KEY
,ALLOWED_HOSTS
, andCSRF_TRUSTED_ORIGINS
Gunicorn, Psycopg, and environs
Let's start by installing Gunicorn
, Psycopg
, and environs
. If you are on macOS, it is necessary to install PostgreSQL first via Homebrew--brew install postgresql
--before installing Psycogp
.
(.venv) $ python -m pip install gunicorn==20.1.0
(.venv) $ python -m pip install "psycopg[binary]"==3.1.8
(.venv) $ python -m pip install "environs[django]"==9.5.0
Then update django_project/
settings.py
with three new lines so environment variables can be loaded in.
# django_project/settings.py
from pathlib import Path
from environs import Env # new
env = Env() # new
env.read_env() # new
DATABASES
Next, update the DATABASES
setting in the django_project/
settings.py
file so that SQLite is used locally but PostgreSQL in production.
# django_project/settings.py
DATABASES = {
"default": env.dj_db_url("DATABASE_URL", default="sqlite:///db.sqlite3"),
}
WhiteNoise
WhiteNoise
is needed to serve static files in production. Install the latest version using pip
:
(.venv) $ python -m pip install whitenoise==6.4.0
Then in django_project/
settings.py
, there are three updates to make:
add
whitenoise
to theINSTALLED_APPS
above the built-instaticfiles
appunder
MIDDLEWARE
add a new line forWhiteNoiseMiddleware
afterSessionMiddleware
configure
STATICFILES_STORAGE
to useWhiteNoise
The updated file should look as follows:
{title="Code",lang="python"}
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"whitenoise.runserver_nostatic", # new
"django.contrib.staticfiles",
"todo",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # new
"django.middleware.common.CommonMiddleware",
...
]
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE =
"whitenoise.storage.CompressedManifestStaticFilesStorage" # new
We have updated the default STATICFILES_STORAGE
engine to use WhiteNoise
when running the collectstatic
management command. Run it one more time now:
(.venv) $ python manage.py collectstatic
There will be a short warning, This will overwrite existing files! Are you sure you want to do this?
Type "yes" and hit Enter
. The collected static files are now regenerated in the same staticfiles
folder using WhiteNoise.
requirements.txt
Now that all additional third-party packages are installed, we can generate a requirements.txt
file containing the contents of our virtual environment.
(.venv) $ python -m pip freeze > requirements.txt
DEBUG, SECRET_KEY, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS
DEBUG
mode helps with local development but is a massive security issue in production. We'll use an elegant approach of setting a default value so that DEBUG
is False
unless specified otherwise.
# django_project/settings.py
DEBUG = env.bool("DEBUG", default=False) # new
Create a new hidden file called .env
in the project-root directory. It will hold local environment variables. Then within it, set DEBUG
to True
so we can use it locally for debugging purposes.
# .env
DEBUG=True
Fly will generate a SECRET_KEY
environment variable for us in production and for building the Docker image in the fly launch
stage. We can again use a default value for local development.
# django_project/settings.py
SECRET_KEY = env.str(
"SECRET_KEY",
default="django-insecure-^qi19(+(oo-ere5b&$@275chw)k@7ob1)74aol5d$(k*)5kk5)",
)
For now, we can set both ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS to wildcard values. This is insecure so the production URL should be swapped in once available.
# django_project/settings.py
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = ["https://*.fly.dev"]
Deploy to Fly.io
Fly's command-line utility flyctl, will help configure our project for deployment. Use the command fly launch
and follow the wizard.
Choose an app name: this will be your dedicated
fly.dev
subdomainChoose the region for deployment: select the one closest to you or another region if you prefer
Decline overwriting our .dockerignore file: our choices are already optimized for the project
Setup a Postgres database cluster: the "Development" option is appropriate for this project. Fly Postgres is a regular app deployed to Fly.io, not a managed database.
Select "Yes" to scale a single node pg to zero after one hour: this will save money for toy projects
Decline to setup a Redis database: we don't need one for this project
(.venv) $ fly launch
? Choose an app name (leave blank to generate one): django-todo
automatically selected personal organization: Will Vincent
Some regions require a paid plan (fra, maa).
See https://fly.io/plans to set up a plan.
? Choose a region for deployment: Boston, Massachusetts (US) (bos)
App will use 'bos' region as primary
Created app 'django-todo' in organization 'personal'
Admin URL: https://fly.io/apps/django-todo
Hostname: django-todo.fly.dev
Set secrets on django-todo: SECRET_KEY
? Would you like to set up a Postgresql database now? Yes
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
? Scale single node pg to zero after one hour? Yes
Creating postgres cluster in organization personal
...
? Would you like to set up an Upstash Redis database now? No
Wrote config file fly.toml
...
Your Django app is ready to deploy!
The fly launch
command creates three new files in the project that are automatically configured: Dockerfile
, fly.toml
, and .dockerignore
.
ALLOWED_HOSTS & CSRF_TRUSTED_ORIGINS
The dedicated URL for your deployment will be <app_name>.
fly.dev
. Update the ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS configurations with your <app_name>
to include it. In this example it looks like this:
# django_project/settings.py
ALLOWED_HOSTS = ["django-todo.fly.dev", "localhost", "127.0.0.1"]
CSRF_TRUSTED_ORIGINS = ["https://django-todo.fly.dev/"]
Deploy Your Application
To deploy the application use the following command:
(.venv) $ fly deploy
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v0 deployed successfully
This will take a few seconds as it uploads your application, verifies the app configuration, builds the image, and then monitors to ensure it starts successfully. Once complete visit your app with the following command:
(.venv) $ fly open
SSH Commands
The Django web app will be live now but there are no todos visible because our PostgreSQL production database has no data. To fix this, SSH in to set a superuser
account.
(.venv) $ fly ssh console --pty -C "python /code/manage.py createsuperuser"
Then visit the /admin
page, create new Todos, and save them. Upon refresh the live production site will display them.
View Log Files
If your application didn't boot on the first deploy, run fly logs
to see what's going on.
(.venv) $ fly logs
This shows the past few log file entries and tails your production log files. Additional flags are available for filtering.
Custom Domain & SSL Certificates
After you finish deploying your application to Fly and have tested it extensively, read through the Custom Domain docs and point your domain at Fly.
In addition to supporting CNAME
DNS records, Fly also supports A
and AAAA
records for those who want to point example.com
(without the www.example.com
) directly at Fly.
Subscribe to my newsletter
Read articles from Daichi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Daichi
Daichi
Hi! I'm a japanese software engineer. I love python!