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

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

image

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:

image

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:

image

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

image

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

Our simple welcome page on 127.0.0.1

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:

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 Django application from GitHub Repository

Create a Postgres Database

On the application overview page find the "Databases" block and click "Add Database".

image

Create a Postgres 17 database on the same server as the app.

image

Click on the Application name at the bottom of the screen to go to the application dashboard page.

Click on Add Processses.

image

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.

image

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.

image

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.

image

image

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.

image

image