Deploy Django App on Fly.io
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:
- Up to 3 shared-cpu-1x 256mb VMs†
- 3GB persistent volume storage (total)
- 160GB outbound data transfer
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/
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/.
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.