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.
- Deploy Django on Hetzner Cloud
- Deploy Django to Digital Ocean Droplet
- Deploy Django to AWS EC2
- Deploy Django on Linode
- Deploy Django on Azure
- Deploy Django to Google Cloud Platform
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.
Copy the "git add remote" line and paste it in the terminal in the root of your project.
Then type git push -u origin master
and this will push your code to GitHub.
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".
You will see the "Application overview page".
Adding a Postgres Database¶
Scroll down and click on "Add Database"
Create a database Postgres 16 on the same server.
When it shows "Deployed", get back to the Application overview page.
Adding processes¶
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
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: