A Beginner-Friendly Guide to Dockerizing a Django API with PostgreSQL and Redis

Building a Django API on your laptop is one thing. Running it the same way on another machine, on a teammate's computer, or in the cloud is another challenge entirely.

This tutorial walks you through a cleaner and more beginner-friendly version of the original draft you shared.

By the end, you will understand how to package a Django API with Docker, connect it to PostgreSQL and Redis, and avoid several common setup problems.

What You Will Build

In this tutorial, you will run three services together:

  1. A Django API application. Sample Django Ticketing Management API
  2. A PostgreSQL database
  3. A Redis service for caching or background jobs

We will use:

  • Docker to package the app
  • Docker Compose to run multiple services together
  • Gunicorn to serve the Django app inside the container

Why Docker Is Useful

Before Docker, developers often had problems like:

  • One machine had a different Python version
  • Another machine was missing PostgreSQL
  • Someone forgot to install Redis
  • The app worked locally but failed in production

Docker helps solve this by putting your app and its dependencies into containers.

In simple terms

Think of a container as a small box that includes:

  • Your code
  • Your runtime
  • Your dependencies
  • Your system setup

This means the app behaves more consistently across environments.

Understanding the Services

Before writing code, it helps to understand what each part does.

Django

Django is your web application. In this case, it powers an API.

PostgreSQL

PostgreSQL stores your data, such as users, products, orders, or anything your API manages.

Redis

Redis is an in-memory data store. It is often used for:

  • Caching
  • Session storage
  • Message brokering
  • Background job queues

Docker Compose

Docker Compose lets you define all your services in one file and start them together.

Project Structure

A simple project structure can look like this:

.
├── manage.py
├── .env
├── Dockerfile
├── docker-compose.yml
├── entrypoint.sh
├── requirements.txt
├── project_name/
└── app_name/

Each file has a role:

  • Dockerfile defines how to build the Django container
  • docker-compose.yml defines all services
  • .env stores configuration values
  • entrypoint.sh runs startup tasks before the app launches

Step 1: Add Environment Variables

Environment variables keep configuration outside your code.

Create a .env file:

POSTGRES_DB=mydb
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword

DB_NAME=mydb
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=db
DB_PORT=5432

REDIS_URL=redis://redis:6379/0

Why this matters

This is useful because:

  • You avoid hardcoding secrets in Python files
  • You can change settings per environment
  • Your project becomes easier to deploy later

Step 2: Create the Dockerfile

The Dockerfile defines how your Django image is built.

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    netcat-openbsd \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

COPY . /app/

RUN chmod +x /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]

Step 3: Understand the Dockerfile Line by Line

Let us break it into smaller pieces.

FROM python:3.14-slim

This tells Docker to start from a lightweight Python image.

ENV PYTHONDONTWRITEBYTECODE=1

This stops Python from creating .pyc files.

ENV PYTHONUNBUFFERED=1

This makes logs print immediately, which is helpful inside containers.

WORKDIR /app

This sets /app as the working folder inside the container.

RUN apt-get update ...

This installs system packages needed by Python packages like psycopg.

COPY requirements.txt .

This copies your dependency file first, which helps Docker cache the install step.

RUN pip install ...

This installs Python dependencies.

COPY . /app/

This copies your full project into the container.

ENTRYPOINT ["/app/entrypoint.sh"]

This tells Docker what script to run when the container starts.

Step 4: Create the Startup Script

Create entrypoint.sh:

#!/bin/sh
set -e

echo "Waiting for postgres..."
while ! nc -z "$DB_HOST" "$DB_PORT"; do
  sleep 1
done

python manage.py check
python manage.py migrate --noinput

exec gunicorn project_name.wsgi:application --bind 0.0.0.0:8000 --workers 3

Step 5: Understand What the Startup Script Does

This script handles startup in the correct order.

set -e

This makes the script stop immediately if a command fails.

Waiting for PostgreSQL

while ! nc -z "$DB_HOST" "$DB_PORT"; do
  sleep 1
done

Django should not start before the database is ready. This loop checks until PostgreSQL is reachable.

Django checks

python manage.py check

This validates your Django project settings and app configuration.

Migrations

python manage.py migrate --noinput

This applies database migrations automatically.

Starting Gunicorn

exec gunicorn project_name.wsgi:application --bind 0.0.0.0:8000 --workers 3

exec is important because it replaces the shell process with Gunicorn. That helps Docker manage signals correctly.

Step 6: Create the Docker Compose File

Now define all services in docker-compose.yml.

services:
  db:
    image: postgres:16
    env_file:
      - .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

  web:
    build: .
    env_file:
      - .env
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    depends_on:
      - db
      - redis

volumes:
  postgres_data:

Step 7: Understand the Compose File

This file creates a multi-container setup.

db

This is the PostgreSQL service.

  • Uses the official Postgres image
  • Reads settings from .env
  • Stores data in a named volume

redis

This runs Redis using a lightweight Alpine image.

web

This is your Django app.

  • Builds from the current project
  • Loads environment variables
  • Maps port 8000
  • Mounts your code into the container

How Services Talk to Each Other

A very important Docker concept is networking.

In Docker Compose, services can reach each other by service name.

That is why this works:

DB_HOST=db
REDIS_URL=redis://redis:6379/0

Here:

  • db points to the PostgreSQL container
  • redis points to the Redis container

You do not need IP addresses.

Step 8: Update Django Settings

Your Django settings need to read environment variables.

Here is a simple example:

import os

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("DB_NAME"),
        "USER": os.getenv("DB_USER"),
        "PASSWORD": os.getenv("DB_PASSWORD"),
        "HOST": os.getenv("DB_HOST", "db"),
        "PORT": os.getenv("DB_PORT", "5432"),
    }
}

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.getenv("REDIS_URL", "redis://redis:6379/0"),
    }
}

This makes your application configurable and container-friendly.

Step 9: Build and Run the Project

Use these commands:

docker compose build
docker compose up

Once the containers are running, open:

http://localhost:8000

If your Django app is set up correctly, your API should now be reachable.

Common Problems and How to Fix Them

These issues are very common when working with Docker for the first time.

Problem 1: The container exits immediately

Possible cause:

  • The startup command finished and nothing kept the container running

Fix:

  • Make sure Gunicorn is the final foreground process
  • Use exec in entrypoint.sh

Problem 2: entrypoint.sh: no such file or directory

Possible causes:

  • The file does not exist
  • The path is wrong
  • The volume mount replaced the file
  • The file uses Windows line endings

Fixes:

  • Confirm the file exists in your project root
  • Confirm the Dockerfile copies it
  • Make sure the file is executable
  • Convert line endings from CRLF to LF

Problem 3: PostgreSQL says the database does not exist

Possible cause:

  • A previous Postgres volume was already initialized with older values

Fix:

docker compose down -v

This removes containers and named volumes so Postgres can initialize again.

Problem 4: Django cannot connect to the database

Possible causes:

  • Wrong host name
  • Wrong credentials
  • Database not ready yet

Fixes:

  • Use db as the hostname
  • Check .env values carefully
  • Keep the wait loop in entrypoint.sh

A Practical Mental Model

Here is a simple way to picture the full setup:

  1. Docker Compose starts all services
  2. PostgreSQL starts
  3. Redis starts
  4. Django container starts
  5. The entrypoint script waits for PostgreSQL
  6. Django migrations run
  7. Gunicorn serves the API

This sequence is easier to debug when you understand who depends on whom.

Development vs Production

A setup that works well for local development is not always production-ready.

For example, this line is helpful in development:

volumes:
  - .:/app

It lets code changes appear immediately inside the container.

But in production, you usually remove that volume mount and run the image as built.

Key Considerations

When moving toward production, keep these points in mind.

Security

  • Do not commit .env files to version control
  • Use a secrets manager in cloud environments
  • Set a proper SECRET_KEY
  • Restrict ALLOWED_HOSTS

Reliability

  • Add health checks
  • Do not rely only on depends_on
  • Handle service startup timing carefully

Performance

  • Tune Gunicorn worker count
  • Add a reverse proxy such as Nginx if needed
  • Use caching where it makes sense

Data persistence

  • Keep PostgreSQL data in volumes
  • Plan for backups in production

Background tasks

  • Redis becomes more useful when paired with Celery or another task queue
  • Separate workers from the web container

Logging and monitoring

  • Send logs to standard output
  • Use monitoring and alerting tools in production

Recommendations

Here are some practical recommendations for beginners.

  1. Start with a simple three-service setup: Django, PostgreSQL, and Redis
  2. Keep secrets in environment variables
  3. Use an entrypoint script for startup logic
  4. Learn how Docker networking works through service names
  5. Remove development-only mounts before production deployment
  6. Add health checks and proper monitoring as your app grows
  7. Treat Docker as a way to standardize environments, not just as a deployment trick

Example: A Better Production Direction

A more production-oriented architecture may look like this:

  • Django + Gunicorn for the app
  • Nginx as a reverse proxy
  • PostgreSQL as the main database
  • Redis for cache and tasks
  • Celery worker for background jobs
  • Cloud storage for static and media files
  • CI/CD pipeline for automated builds and deployments

This is not required on day one, but it is helpful to know where you are heading.

Final Thoughts

Dockerizing a Django API is more than putting code in a container.

It teaches you how applications are packaged, how services communicate, and how different parts of a system depend on each other.

Once you understand this setup, you are much better prepared for:

  • Local development
  • Team collaboration
  • Cloud deployment
  • Production thinking

Start small, understand each moving part, and improve the setup step by step. That is how beginner projects grow into reliable systems.