In this article I will show you how to start a new Django project from scratch and set it up to work in Docker for local development.
- Source code
- Requirements
- What is Docker and Docker Compose?
- Why dockerize your Django application?
- Creating a fresh Django Project with Docker and Postgres
- Create a folder and git init
- Create a Dockerfile for Django Channels
- Requirements for Django Project
- docker-compose.yml for Django Project
- .env file
- .gitignore file
- Pull existing Docker images and Build Django Image
- Edit Project Settings
- Start Django Project in Docker
- Apply migrations
- Start containers
- Start an application
- Deploying your app
Source code¶
The source code for this tutorial is available on GitHub: https://github.com/appliku/django-docker-tutorial
Requirements¶
You need Docker Installed on your machine.
For Mac OS and Windows use Docker Desktop.
We will not be using local Python installation for this tutorial, everything will work within the Docker container.
What is Docker and Docker Compose?¶
Docker is a way to package and run applications in a portable and isolated way.
Docker Compose is a tool for managing multiple containers as a single application.
Why dockerize your Django application?¶
Dockerizing your Django application means packaging it with all the dependencies into a container or at least Dockerfile. This makes a reproducible environment for running your app which helps running app on different systems, local or production.
Benefits of Dockerizing your Django application¶
- Makes your application portable and scalable
- Runs on any platform (local or cloud)
- Eliminates installation/compatibility concerns
-
Enables testing across different environments
-
Provides application isolation
- Separates app from host machine
-
Prevents conflicts with other applications/services
-
Streamlines development workflow
- Quick environment setup and teardown
- More efficient development and testing
- Eliminates repetitive environment configuration
- Simplifies debugging process
- Simplifies setup of auxiliary services (Postgres, Redis, RabbitMQ, etc)
Creating a fresh Django Project with Docker and Postgres¶
Create a folder and git init¶
mkdir django-docker-tutorial
cd django-docker-tutorial
git init
Create a Dockerfile for Django Channels¶
In the root of your project create Dockerfile
:
FROM python:3.12.8-bullseye
SHELL ["/bin/bash", "--login", "-c"]
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN groupadd -g $USER_ID -o app
RUN useradd -m -u $USER_ID -g $GROUP_ID -o -s /bin/bash app
ENV PIP_NO_CACHE_DIR off
ENV PIP_DISABLE_PIP_VERSION_CHECK on
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV COLUMNS 80
RUN apt-get update \
&& apt-get install -y --force-yes \
curl nano python3-pip gettext chrpath libssl-dev libxft-dev \
libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /code/
COPY requirements.txt /code/
RUN pip install wheel
RUN pip install -r requirements.txt
COPY . /code/
USER app
Requirements for Django Project¶
In the root of the project create a file requirements.txt
:
Django==5.1.4
django-environ==0.11.2
gunicorn==23.0.0
psycopg[binary]==3.2.3
whitenoise==6.8.2
Pillow==11.0.0
docker-compose.yml for Django Project¶
In the root of the project create a file docker-compose.yml
:
x-app: &app
build: .
restart: always
env_file:
- .env
volumes:
- .:/code
links:
- db
depends_on:
- db
services:
db:
image: postgres:17
environment:
- POSTGRES_USER=tutorial
- POSTGRES_PASSWORD=tutorial
- POSTGRES_DB=tutorial
ports:
- "5432:5432"
web:
<<: *app
command: python manage.py runserver 0.0.0.0:8000
ports:
- "127.0.0.1:8000:8000"
.env file¶
In the root of the project create a file .env
:
DATABASE_URL=postgresql://tutorial:tutorial@db:5432/tutorial
DEBUG=True
We need .env
file to store environment variables. They contain sensitive information like credentials for DBs and external services, can't be hardcoded and shouldn't ever be committed to version control.
.gitignore file¶
In the root of the repo create a file .gitignore
env/
.idea/
__pycache__/
*.py[cod]
*$py.class
.vscode/
.DS_Store
.AppleDouble
.LSOverride
.env
db.sqlite3
These are the files that shouldn't be added to version control. We add here some of the OS specific temporary files, python cache, IDE folders and .env
file to prevent it being committed to version control.
Pull existing Docker images and Build Django Image¶
Running this command will make Docker download images for services with defined image and not require the build.
This step is optional because they will be pulled anyway on the start of containers.
docker compose pull
Build your project's Docker image with:
docker compose build
Edit Project Settings¶
Open the file project/settings.py
and replace it with this code:
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": [BASE_DIR / "templates",],
"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/")
Why we have changed it is because the default Django settings file is not flexible, and not suitable for deployment.
We have added some libraries and settings. Let's go over them.
django-environ and environment variables¶
To avoid hardcoding credentials and other parameters we rely on environment variables.
django-environ
library helps us handle environment variables, provide default values for them, casts them to a specific type and optionally reads them from .env file.
Although, since we are using docker compose load environment variables there, it is not that important to read it from the Django code anymore.
Configurations like SECRET_KEY
, ALLOWED_HOSTS
, DATABASE_URL
are environment specific and sensitive so they should never be hardcoded. Also they change between environments and it is much easier to change environment variable than to have to change the code to set them.
Whitenoise library for serving static files with Django¶
The whitenoise
library allows Django to efficiently serve static files. You don't need to upload it to S3 or setup nginx to serve static files anymore.
To enable whitenoise
library we add it to the MIDDLEWARE
and add two settings:
WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = DEBUG
Also, we configure STATIC_*
settings:
STATIC_URL = env.str("STATIC_URL", default="/static/")
STATIC_ROOT = env.str("STATIC_ROOT", default=BASE_DIR / "staticfiles")
SECURE_PROXY_SSL_HEADER¶
This setting is needed for Django to understand if it is running behind a secure proxy.
Reverse proxy must set the header to signify it is served securely. Appliku does that in nginx configuration when the app is deployed.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Logging¶
We need Django to produce logs to be able to debug potential issues.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"": {"handlers": ["console"], "level": "DEBUG"}},
}
Media¶
We set configuration for serving media from local volume.
Read more about volumes in Appliku
MEDIA_ROOT = env("MEDIA_ROOT", default=BASE_DIR / "media")
MEDIA_URL = env("MEDIA_PATH", default="/media/")
Alternatively, you might want to use S3 for Media file uploads in Django
Databases¶
Here is how Django gets DATABASES
setting. django-environ
will read the DATABASE_URL
environment variable and set appropriate DB type and credentials from the URL string.
DATABASES = {
"default": env.db(default="sqlite:///db.sqlite3"),
}
Start Django Project in Docker¶
Now that our image is built, start a shell within a container and start our Django project (don't forget the dot at the end).
docker compose run web bash
django-admin startproject project .
Apply migrations¶
Let's apply Django migrations and make sure our project and database connection works correctly.
In the Docker bash shell run this:
python manage.py migrate
By the way if you closed the shell or you want to run one command in Docker without launching shell you can do it another way:
docker compose run web python manage.py migrate
Start containers¶
This command will start all services defined in docker-compose.yml
.
Run this command outside docker compose shell. To leave the container shell press CTRL-D.
docker compose up -d
The output of successful execution of this command should look this way:
Check logs of Django Project with docker compose logs¶
Now to make sure our services are actually working fine let's see logs.
docker compose logs -f
You will see logs for all services running. To stop following logs press CTRL-C.
To show logs for specific app you can specify process(es) to follow:
docker compose logs -f web
Here is the output for docker compose logs -f web
:
Now we can open our browser and see the Django start page.
Note, if you are on Windows, 0.0.0.0
URL will not work, replace it with http://127.0.0.1:8000
You are seeing this page only because the DEBUG
variable is on. If we set it to off, the page will be just 404 Not found.
Finally, create a root templates folder and an empty .gitkeep
to be able to add that directory to git.
mkdir -p templates
touch templates/.gitkeep
Let's start a Django application and make a simple view for the main page.
Start an application¶
If you have closed the container shell, start it again with docker compose run web bash
.
Within the container shell start out app:
python manage.py startapp mainapp
Add our app to INSTALLED_APPS
in project/settings.py
.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"mainapp",
]
Edit the file mainapp/views.py
from django.views.generic import TemplateView
class MainPage(TemplateView):
template_name = "mainapp/index.html"
This will render our template for the main page.
Create a file mainapp/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.MainPage.as_view(), name='main_page'),
]
Now edit our root URLConf in project/urls.py
to include mainapp/urls.py
in there.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('mainapp.urls')),
path('admin/', admin.site.urls),
]
ALTERNATIVELY you could import mainapp views directly from the root URLconf this way
from django.contrib import admin
from django.urls import path, include
from mainapp import views
urlpatterns = [
path('', views.MainPage.as_view(), name='main_page'),
path('admin/', admin.site.urls),
]
I am a big fan of single app projects instead of having multiple apps per project. (I will write an article about it later and will add a link from here).
Now we need to create our template.
We have our templates folder in the root of the project. Templates from it will be picked up because in the TEMPLATES
setting in project/settings.py
we have added that directory.
I like having templates in one place, just like having all URLs there to reduce the number of places where similar things can reside.
Also, having all templates in root directory directory over per-app templates folder, allows us overriding templates of 3rd party apps like the admin
one.
Create a file templates/mainapp/index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Django Docker + Postgres Tutorial</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #dedede;
}
h1 {
color: purple;
}
</style>
</head>
<body>
<h1>Welcome to Django Docker + Postgres Tutorial</h1>
</body>
</html>
Back to the browser at http://127.0.0.1:8000
Commit and push to GitHub¶
Now let's commit everything to version control
git add .
git commit -m'Initial Commit'
Go to GitHub create a new repository.
When the repository is created copy the line "git remote add" and run it
then
git push -u origin master
Deploying your app¶
Go to Appliku dashboard, create an account if you don't have one already.
Create and add a server for Django App¶
Add an Ubuntu or a Debian server from a cloud provider of your choice, if you haven't already.
Here are guides for adding servers from different cloud providers:
- Deploy Django on Hetzner
- Deploy Django on Digital Ocean Droplet
- Deploy Django on AWS EC2 Instance
- Deploy Django on Linode
- Deploy Django on Azure Virtual machine
- Deploy Django on Google Cloud Platform Virtual Machine
Create an application from GitHub repository¶
After you have created a server, go to the "Applications" page and create a new application from GitHub repository.
Create a Postgres Database¶
On the application overview page find the "Databases" block and click "Add Database".
Create a Postgres 17 database on the same server as the app.
Click on the Application name at the bottom of the screen to go to the application dashboard page.
Click on Add Processses.
Create two processes:
web: gunicorn project.wsgi --log-file -
release: python manage.py migrate
Names of the processes are important.
The web
is the process that will receive HTTP traffic
The release
is the processes that is executed after each successful deployment.
You can also edit environment variables on the "Environment variables" tab.
But now click "Save and deploy" button, then return to the application dashboard page.
If deployment fails click on the deployment number to see the logs of deployments and find the error.
When the deployment finishes, click on the Open App button and then click on the domain name.
Our application is now deployed and has TLS certificate, issued by Let's Encrypt.
You can also add your own custom domain in the application settings -> Custom domains.
Note: If the app has been deployed but you get 502 Bad Gateway error or any other error you can go to App Logs to see what can be the problem with the running app.