I was working on a Django template DHAT Stack and wanted to deploy it, I chose fly.io because of its awesome cli and straight forward deployment process. Though the process to deploy to fly.io is fairly easy, there were some configurations needed to make it production ready. On top of that, I wanted to use uv to manage the python environment and sqlite for the database as the project was a template and didn’t require a full fledged Postgres.
Note: This guide was tested with Django 5.1.3. While the deployment steps should work for Django 6, some configurations may differ. If you encounter issues with Django 6, please refer to the Django 6 release notes for any breaking changes.
What You’ll Learn
This guide covers:
- Configuring Django for production (whitenoise, gunicorn, environment variables)
- Building a uv-compatible Dockerfile for Fly.io
- Setting up SQLite with Fly.io volumes for data persistence
- Managing secrets and production settings
Prerequisites
Before starting, ensure you have:
- Python 3.10+ (3.12+ for Django 6)
- uv installed for dependency management
- Fly CLI installed
- A Django project with
pyproject.tomlanduv.lock
I’ll divide this blog into two sections, first we will see the configuration needed to make the Django app production ready and then we will see how to deploy it to fly.io.
Making the Django app production ready
Since I’m using uv to manage the python environment, and you’re reading this blog, I’m assuming you’re using uv as well and have a pyproject.toml and uv.lock file in the root of your project.
These files will be used in the fly.io build process to install the dependencies and that’s the advantage of using uv that we don’t need requirements.txt which is not easy to manage.
Environment variables
I’m using django-environ to manage the environment variables and have a .env file in the root of the project which is used to set the environment variables.
Create a .env file in the root of the project and add the following:
# .env
SECRET_KEY=your_secret_key
DEBUG=False Then in the settings.py file, import the environment variables:
# settings.py
import environ
env = environ.Env(
# set casting, default value
DEBUG=(bool, False),
)
# Take environment variables from .env file
environ.Env.read_env(BASE_DIR / '.env')
DEBUG = env('DEBUG')
SECRET_KEY = env('SECRET_KEY') Though here we’re reading secret key from the .env file, we will soon update it to a different method to take the secret key from fly.io secrets and use a generated secret key in development to not get an error during build process as we’re going to put .env file in the gitignore and dockerignore.
Gunicorn
Django comes with a built in minimal server which is not recommended for production, we will use gunicorn to serve the app in production. Gunicorn is a Python WSGI HTTP Server for UNIX. It’s a pre-fork worker model ported from Ruby’s Unicorn project.
Let’s install gunicorn:
uv add gunicorn==20.1.0 That’s it, we will later see how to use gunicorn in the fly.io build process.
Static files
We will use whitenoise to serve the static files. Whitenoise is a Django middleware that serves static files in production.
Let’s install whitenoise:
uv add whitenoise==6.5.0 Now we need to update the settings.py file to use whitenoise:
- Add whitenoise to the middleware after
django.middleware.security.SecurityMiddleware - Set
STATICFILES_STORAGEtowhitenoise.storage.CompressedManifestStaticFilesStorage - Set
STATIC_ROOTto the path where the static files will be collected, I’m usingBASE_DIR / 'staticfiles'
(Optional but recommended) Use whitenoise in development to keep the behavior of the static files the same in development and production, to do this we just need to add whitenoise.runserver_nostatic to INSTALLED_APPS before django.contrib.staticfiles.
#settings.py
INSTALLED_APPS = [
...
'whitenoise.runserver_nostatic',
'django.contrib.staticfiles',
...
]
MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_ROOT = BASE_DIR / 'staticfiles' Other configurations
Update ALLOWED_HOSTS to a list of domains that our django app will serve, in our case it will be the domain of the fly app.
I’m keeping the name of our fly app as dhat-stack.
#settings.py
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'dhat-stack.fly.dev'] Django also has a setting CSRF_TRUSTED_ORIGINS which is used to trust the origins of the requests, we need to add the domain of the fly app to this list.
# settings.py
CSRF_TRUSTED_ORIGINS = ['https://dhat-stack.fly.dev'] Database
For now, let’s add the following to the settings.py file:
#settings.py
DATABASES = {
'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3'),
} This will make the database use the DATABASE_URL environment variable if it’s available, otherwise it will use sqlite as default.
Deploying to fly.io
Since we’re using sqlite, there are some extra steps we need to take for it to work properly in fly.io. To avoid complexity of managing multiple sqlite databases and keeping them in sync accross multiple servers, I’ll be deloying my app to only 1 server and using fly.io volumes to persist the database. For small apps this is more than enough.
If you’re planning to scale your app to multiple servers, you should use a database that is designed to be scalable like Postgres and a managed databse service like Supabase works great with fly. If you don’t want to use a managed database service, fly also has its own semi-managed (or not) postgres database called Fly Postgres.
There might be ways to make sqlite work with multiple servers but I didn’t have the need for it, fly also has its own database service called LiteFS, I needed something simple and sqlite worked fine for my use case.
Creating a fly app
Note: You need to have the fly cli installed to be able to do this. Install flyctl
First we need to create a fly app, we can do this by running the following command:
fly launch --no-deploy --ha=false This will create a fly app without deploying it and without high availability (ha) i.e only 1 server.
Creating a volume
To store the sqlite database file even after the app is stopped, we need to create a volume.
I followed the fly.io volumes guide to launch a new fly app with a volume. For an existing fly app, a flyctl command will be needed, you can follow this guide to create a volume for an existing app.
Since I’ve already created a fly app, the next step is to create a volume.
Now we need to tell fly to mount this volume during the deployment process, we can do this by updating the fly.toml file.
# fly.toml
[mounts]
source = "dhat-stack-db"
destination = "/data" Now deploy:
fly deploy --ha=false Troubleshooting: If deployment fails with “volume not found”, wait 30 seconds and run
fly deploy --ha=falseagain. Volumes are created asynchronously in the background.
To manually create a volume, you can run the following command:
fly volumes create dhat-stack-db --region iad --size 1 This will create a volume in the iad region with a size of 1GB.
Secrets
Now create secrets for the environment variables we added to the .env file.
fly secrets set DATABASE_URL=sqlite:////data/db.sqlite3 We don’t need to set a secret key as fly will create it for us.
Dockerfile
If you’re using uv, its likely that the Dockerfile generated by flyctl contains poetry config as it detects pyproject.toml and assumes it’s a poetry project.
Since uv is new we will have to update the Dockerfile to use uv instead of poetry.
This is the complete Dockerfile I used:
# Dockerfile
ARG PYTHON_VERSION=3.12-slim
FROM python:${PYTHON_VERSION}
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN mkdir -p /code
WORKDIR /code
RUN pip install uv
COPY pyproject.toml uv.lock /code/
RUN uv venv
RUN uv sync
COPY . /code
RUN apt-get update && apt-get install -y
sqlite3
&& rm -rf /var/lib/apt/lists/*
RUN uv run manage.py collectstatic --noinput
RUN chmod +x startup.sh
EXPOSE 8000
ENTRYPOINT ["./startup.sh"] startup.sh is a simple script that is needed for migrations to work properly. Create it in the root of the project and add the following:
#!/bin/bash
uv run manage.py migrate
sqlite3 /data/db.sqlite3 'PRAGMA journal_mode=WAL;'
sqlite3 /data/db.sqlite3 'PRAGMA synchronous=1;'
uv run gunicorn --bind :8000 --workers 2 dhat_stack.wsgi Finally, deploy the app to fly.io:
fly deploy --ha=false Conclusion
This guide covered the steps I took to deploy a Django app to fly.io using uv, sqlite and a custom Dockerfile. I hope this guide will help you deploy your Django app to fly.io.
You can find the complete code for the Django app in the DHAT Stack repository.
If you found this guide helpful, please consider giving DHAT Stack repo a star and sharing this blog on bsky or twitter.
