Deploy Django App on Fly.io

Feb. 11, 2023

Deploy Django apps easily on Fly.io with a free tier for testing. Steps include app preparation, Flyctl setup, Dockerfile creation, and deployment. Fly.io also has deployment triggers via Github Action for flyctl.

Fly is a platform for running full stack apps and databases, and you can deploy your Django app. Fly.io provide a free tier so you can try to deploy in their platform.

Free allowance

Resources included for free on all plans:

Prepare Django app

For this demo, django rest framework is used to provide users' API. To manage the project and its dependency Poetry tool is needed.

Ensure you have poetry installed in your system

$ curl -sSL https://install.python-poetry.org | python3 -
$ poetry --version
Poetry (version 1.3.1)

To set up a new project, you can run:

# poetry new <project_name>
$ poetry new riemann
Created package reimann in riemann

Install required dependency

$ cd riemann
$ poetry add django djangorestframework
# after this, in the current directory has following files
$ ls
-rw-r--r--  1 sakti  staff     0 Feb 11 10:34 README.md
drwxr-xr-x  3 sakti  staff    96 Feb 11 10:34 riemann
-rw-r--r--  1 sakti  staff  3186 Feb 11 10:34 poetry.lock
-rw-r--r--  1 sakti  staff   329 Feb 11 10:34 pyproject.toml
drwxr-xr-x  3 sakti  staff    96 Feb 11 10:34 tests

Init Django project

# delete default package create by poetry, e.g riemann directory
$ rm -r riemann
# start django project in current directory
$ poetry shell
$ django-admin startproject riemann .

You can test initial django project by running ./manage.py migrate && ./manage.py runserver then open. http://127.0.0.1:8000/

django initial app

Setup RESTful API

For this demo, I am going to expose standard Django User objects on URL path /users. First, add rest_framework in INSTALLED_APP list in settings.py, and also set up STATIC_ROOT variable.

# riemann/settings.py
...
# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
]
...
STATIC_ROOT = BASE_DIR / "staticfiles"

For simplicity we put all rest_framework bootstrap in urls.py, ideally, this should be split into multiple files accordingly.

Import modules routers, serializers, and viewsets from rest_framework module.

# riemann/urls.py
...
from django.contrib.auth.models import User
from django.urls import include, path
from rest_framework import routers, serializers, viewsets
...

# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ["url", "username", "email", "is_staff"]


# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet): 
    queryset = User.objects.all()
    serializer_class = UserSerializer

# define API router
router = routers.DefaultRouter()
router.register(r"users", UserViewSet)

urlpatterns = [
    path("", include(router.urls)),
    path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
    path("admin/", admin.site.urls),
]

Now you can run ./manage.py createsuperuser to create an admin user for your app, and then run ./manage.py runserver and now we have functional RESTful API for User entity at http://localhost:8000/users/.

django simple restful api

Setup flyctl

Flyctl is command line tool provided by fly.io to interact with the service. On mac you can setup using homebrew,

$ brew install flyctl
# then login using
$ fly auth login
# those will open fly.io login page in the default web browser, then after login you can check
$ fly auth whoami

Then, run fly launch to create application configuration fly.toml, prompt will ask about the project name, account to be used, and region for deployment. We need to configure env variable (for sensitive env var use, fly.io secret, there is [env] section that needs to be updated.

# fly.toml
...
[env]
  DJANGO_ALLOWED_HOSTS="riemann.fly.dev"
  DJANGO_CSRF_TRUSTED_ORIGINS="https://riemann.fly.dev"
  DEBUG="False"
...

And on service definition we need to update the internal port, and also need to define [[statics]] for serving static files.

# fly.toml
...
[[services]]
    http_checks = []
    internal_port = 8000
    processes = ["app"]
    protocol = "tcp"
    script_checks = []
...

[[statics]]
  guest_path = "/venv/lib/python3.11/site-packages/staticfiles"
  url_prefix = "/static/"

Deployment

First execute django manage command needed during deployment e.g: collectstatic, migrate. setup.py module will be created in riemann package.

# riemann/setup.py

import argparse
import os

import django
from django.core.management import call_command

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "riemann.settings")
django.setup()

parser = argparse.ArgumentParser(description="setup script for django without using manage.py")
parser.add_argument("--static", action="store_true", default=False)
parser.add_argument("--migrate", action="store_true", default=False)
parser.add_argument("--superuser", action="store_true", default=False)


def main() -> None:
    args = parser.parse_args()
    if args.static:
        call_command("collectstatic", interactive=False)
    if args.migrate:
        call_command("migrate", interactive=False)
    if args.superuser:
        call_command("createsuperuser", interactive=True)


if __name__ == "__main__":
    main()

And then we need to make the app able to get override settings variable using environment, we need install a new dependency django-environ.

$ poetry add django-environ

Then edit settings.py

# riemann/settings.py

...
import environ

env = environ.Env()
...
SECRET_KEY = env(
    "SECRET_KEY",
    default="!!!SET DJANGO_SECRET_KEY!!!",
)
DEBUG = env.bool("DEBUG", default=True)
ALLOWED_HOSTS= env.list("DJANGO_ALLOWED_HOSTS", default=["localhost"])
CSRF_TRUSTED_ORIGINS = env.list("DJANGO_CSRF_TRUSTED_ORIGINS", default=["http://localhost"])

This will allow setting django using environment variables, for SERCET_KEY you should configure using fly secrets set SECRET_KEY=<your generated key>, and non-sensitive envvar can be defined in fly.toml.

Dockerfile

When we execute fly launch or fly deploy, fly.io will provision a free docker builder machine e.g fly-builder-damp-silence-3752 to build our app, and then push that image to registry.fly.io, and then when ready will be scheduled on selected region node run using Firecracker.

2023-02-11T04:44:51.898 runner[d6523dfa] sin [info] Unpacking image
2023-02-11T04:44:54.814 runner[d6523dfa] sin [info] Preparing kernel init
2023-02-11T04:44:55.255 runner[d6523dfa] sin [info] Configuring firecracker
2023-02-11T04:44:55.967 runner[d6523dfa] sin [info] Starting virtual machine
2023-02-11T04:44:56.251 app[d6523dfa] sin [info] Starting init (commit: c19b424)...
2023-02-11T04:44:56.285 app[d6523dfa] sin [info] Preparing to run: `./docker-entrypoint.sh` as root

First, we need to define Dockerfile, and python:3.11-slim is used for the base image.

FROM python:3.11-slim as base

ENV PYTHONFAULTHANDLER=1 \
    PYTHONHASHSEED=random \
    PYTHONUNBUFFERED=1

WORKDIR /app

FROM base as builder

ENV PIP_DEFAULT_TIMEOUT=100 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1 \
    POETRY_VERSION=1.3.2

# setup build system dependency
RUN apt-get update
RUN apt-get -y install build-essential libffi-dev libpq-dev python3-dev libjpeg-dev zlib1g-dev libc-dev make curl libfreetype6-dev

RUN pip install "poetry==$POETRY_VERSION"
RUN python -m venv /venv

COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --without-hashes | /venv/bin/pip install -r /dev/stdin

COPY . .
RUN poetry build && /venv/bin/pip install dist/*.whl

FROM base as final
# setup runtime system dependency
RUN apt-get update
RUN apt-get -y install --no-install-recommends libffi7 libpq5 libfreetype6-dev curl

EXPOSE 8000

COPY --from=builder /venv /venv
RUN /venv/bin/python -m riemann.setup --static
COPY docker-entrypoint.sh ./

CMD ["./docker-entrypoint.sh"]

The dockerfile will execute bash file docker-entrypoint.sh so we can split which ./manage.py command to be run on build or runtime. Make sure you make docker-entrypoint.sh to be executable chmod +x.

#!/bin/sh

set -e

. /venv/bin/activate

python -m riemann.setup --migrate

exec gunicorn riemann.wsgi:application --bind 0.0.0.0:8000

Deploying

After all those deployment setups, you can run fly deploy and then monitor the deployment process.

Continuous Delivery

If you host your source code in Github, Fly.io provide Github Action for flyctl for executing flyctl in Github Action. For example to define manually triggered deployment of fly.io app:

name: Fly Deploy Production
on:
  workflow_dispatch:
    inputs:
      deployRef:
        description: "Deployment refname source (tag or main branch)"
        required: true
        default: "main"
env:
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 10
          ref: ${{ github.event.inputs.deployRef }}
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --config ./fly.production.toml --remote-only

Conclusion

There are many options to deploy Django apps, on your vm, on SaaS app platform, on k8s, etc. You can try to deploy on Fly.io as an alternative, based on region and pricing maybe become your first solution to deploy your Django app.

For full source code, you can visit https://github.com/sakti/riemann.

Return to blog

footer