In this tutorial you will learn how to start a Django project, prepare it for deployment, and deploy it with Appliku on your own VPS cloud server.

Table of contents

Django Project

Starting a Django Project

To start a new Django project you will need Python 3.10 or higher.

This tutorial implies you are working in Linux or Mac terminal. If you are working on Windows - install WSL.

mkdir mydjangoproject
cd mydjangoproject
python3 -m venv env
source env/bin/activate
pip install Django==5.0.7 django-environ==0.11.2 gunicorn==22.0.0 psycopg[binary]==3.2.1 whitenoise==6.7.0 Pillow==10.4.0
pip freeze > requirements.txt
django-admin startproject project .

Open your project in your favorite code editor.

It is important to have requirements.txt in the root of your project, that's what Appliku needs to install dependencies for your project. If you happen to use other package manager, the solution would be to activate virtual environment and do pip freeze > requirements.txt

Using environment variables in Django configuration

Let's edit project/settings.py to make our Django project respect environment variables.

We need this to avoid hardcoding sensitive data like credentials.

We will be passing via environment variables the following things: - SECRET_KEY - DATABASE_URL, which will populate Django's DATABASES object that is used to connect to the database - DEBUG - ALLOWED_HOSTS - MEDIA_URL and MEDIA_ROOT for media files

Add environ import, so your imports section looks like this:

from pathlib import Path  
import environ
import os

Create env object and use it to get the rest of the key settings:

env = environ.Env(  
    # set casting, default value  
    DEBUG=(bool, False)  
)  

BASE_DIR = Path(__file__).resolve().parent.parent  
# Take environment variables from .env file  
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))  

SECRET_KEY = env("SECRET_KEY", default="change_me")  

DEBUG = env("DEBUG", default=False)  

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])

Scroll down and replace the DATABASES with this definition:

DATABASES = {
    "default": env.db(default="sqlite:///db.sqlite3"),
}

Add SECURE_PROXY_SSL_HEADER so Django can detect being behind a secure proxy.

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Add LOGGING setting to make Django produce all logs to stdout.

LOGGING = {  
    "version": 1,  
    "disable_existing_loggers": False,  
    "handlers": {"console": {"class": "logging.StreamHandler"}},  
    "loggers": {"": {"handlers": ["console"], "level": "DEBUG"}},  
}

Serving static files with the whitenoise library

Edit the MIDDLEWARE list, add "whitenoise.middleware.WhiteNoiseMiddleware", after SecurityMiddleware

MIDDLEWARE = [  
    "django.middleware.security.SecurityMiddleware",  
    "whitenoise.middleware.WhiteNoiseMiddleware",   # <-- here
    "debug_toolbar.middleware.DebugToolbarMiddleware",  
    "django.contrib.sessions.middleware.SessionMiddleware",  
    "django.middleware.common.CommonMiddleware",  
    "django.middleware.csrf.CsrfViewMiddleware",  
    "django.contrib.auth.middleware.AuthenticationMiddleware",  
    "django.contrib.messages.middleware.MessageMiddleware",  
    "django.middleware.clickjacking.XFrameOptionsMiddleware",  
]

Set STATIC settings:

STATIC_URL = env.str("STATIC_URL", default="/static/")  
STATIC_ROOT = env.str("STATIC_ROOT", default=BASE_DIR / "staticfiles")

More whitenoise configuration:

WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = DEBUG

Media files

Set these two settings to work with local files for media.

MEDIA_ROOT = env("MEDIA_ROOT", default=BASE_DIR / "media")  
MEDIA_URL = env("MEDIA_PATH", default="/media/")

In order for media files function properly you will need to add a volume later in Appliku app settings. Your settings file should look like this:

"""
Django settings for project project.

Generated by 'django-admin startproject' using Django 5.0.7.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""

from pathlib import Path
import environ
import os
env = environ.Env(
    # set casting, default value
    DEBUG=(bool, False)
)

BASE_DIR = Path(__file__).resolve().parent.parent
# Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))

SECRET_KEY = env("SECRET_KEY", default="change_me")

DEBUG = env("DEBUG", default=False)

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases

DATABASES = {
    "default": env.db(default="sqlite:///db.sqlite3"),
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {"": {"handlers": ["console"], "level": "DEBUG"}},
}

# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = env.str("STATIC_URL", default="/static/")
STATIC_ROOT = env.str("STATIC_ROOT", default=BASE_DIR / "staticfiles")

WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = DEBUG


# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


MEDIA_ROOT = env("MEDIA_ROOT", default=BASE_DIR / "media")
MEDIA_URL = env("MEDIA_PATH", default="/media/")

Create a Django app and the first view

Create your Django app:

python manage.py startapp main

add it to INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'main',
]

Create a view in main/views.py:

from django.http import HttpResponse


def main_page(request):
    return HttpResponse("It works!")

Edit project/urls.py to include this view as the root one:

from django.contrib import admin
from django.urls import path
from main import views # <-- new

urlpatterns = [
    path('', views.main_page), # <-- new
    path('admin/', admin.site.urls),
]

Management command to create a superuser

Create a folder in your new app main/management Then, main/management/commands

In both folders create empty files __init__.py .

This makes them importable python packages. Create a file main/management/commands/makesuperuser.py with the following content:

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.utils.crypto import get_random_string

User = get_user_model()


class Command(BaseCommand):
    def handle(self, *args, **kwargs):
        email = 'admin@example.com'
        new_password = get_random_string(10)
        try:
            if not User.objects.filter(is_superuser=True).exists():
                self.stdout.write("No superusers found, creating one")
                User.objects.create_superuser(username='admin', email=email, password=new_password)
                self.stdout.write("=======================")
                self.stdout.write("A superuser has been created")
                self.stdout.write("Username: admin")
                self.stdout.write(f"Email: {email}")
                self.stdout.write(f"Password: {new_password}")

                self.stdout.write("=======================")
            else:
                self.stdout.write("A superuser exists in the database. Skipping.")
        except Exception as e:
            self.stderr.write(f"There was an error {e}")

This command when executed will check if there is a superuser in the database and if not, create it and print the generated password.

Release script release.sh

It is recommended to have a release script that contains all commands to run after deployment, to avoid the need to run shell commands for tasks like creating superuser, migrations, etc.

In the root of the project create a file called release.sh. We will add this in Processes as a process called release.

#!/bin/bash
set -e
python manage.py migrate
python manage.py makesuperuser

Run script run.sh

Optional, but useful, let's create a script that runs our project when deployed. Create a file called run.sh in the root of the project

#!/bin/bash
set -e
gunicorn project.wsgi --log-file -

Please don't miss the last dash - in the gunicorn command.

Pushing code to GitHub

In the root of your project create a file .gitignore, which will prevent cluttering the git repo with files that shouldn't be there.

env/
.idea/
__pycache__/
*.py[cod]
*$py.class
.vscode/
.DS_Store  
.AppleDouble  
.LSOverride

Initialize git repository and create initial commit

git init
git add .
git commit -m'Initial commit'

Go to GitHub.com and create a new repository.

image

Copy the "git add remote" line and paste it in the terminal in the root of your project.

image

Then type git push -u origin master and this will push your code to GitHub. image

Appliku Account

If you don't already have an Appliku Account create it by going here: https://app.appliku.com/

Create a server in a cloud provider of your choice, then create an app.

Create Application

To create an application go to Applications menu, click "Add Application", select GitHub.

Give your application a name, select the repository, branch and your server, then click "Create Application".

image

image

You will see the "Application overview page".

image

Adding a Postgres Database

Scroll down and click on "Add Database"

image

Create a database Postgres 16 on the same server.

image

When it shows "Deployed", get back to the Application overview page. image

Adding processes

image

Click on the "Add Processes" button.

Add two processes: - The first one MUST be called web and the command bash run.sh. web name is important, it is the process that responds to HTTP requests. - the second process is also a special one, called release and it is a command that is executed after each successful deployment. Command for it must be bash release.sh

Hit "save and deploy" button.

You might want to set additional environment variables on "Environment variables" tab.

It is good to set "SECRET_KEY" to some random long value like "3jhrufhuifgwfg3gfgyuegfewuygfsrgilh47h23h"

The ALLOWED_HOST variable should be a comma-separated list WITHOUT WHITESPACES(!!) of domains through which your app is accessible.

When deployed with Appliku, your app receives a domain name YOUR_APP_NAME.applikuapp.com, in our example it is djangotutorial.applikuapp.com. You can disable this default subdomain on the "build settings" tab.

If you have added custom domains and you have multiple domains through which you app is accessible you should specify ALLOWED_HOSTS env var like this: djangotutorial.applikuapp.com,mysupersite.com,yetanotherchatgptwrapper.ai

Add persistent volume

In order to make persistent volumes to work, please go to the "Volumes" tab in application settings and add one with the following fields:

  • container path = /uploads
  • url = /media/
  • environment variable = MEDIA image

Read more about volumes here: Volumes

Go back to Application overview page.

You can click on "Open App" and it will show you a dropdown with all attached domains. Click on the default one and a page with your app will open saying "It works"

If you are looking for tutorial for a specific cloud provider checkout out one of these: