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:
- A Django API application. Sample Django Ticketing Management API
- A PostgreSQL database
- 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:
Dockerfiledefines how to build the Django containerdocker-compose.ymldefines all services.envstores configuration valuesentrypoint.shruns 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:
dbpoints to the PostgreSQL containerredispoints 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
execinentrypoint.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
CRLFtoLF
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
dbas the hostname - Check
.envvalues carefully - Keep the wait loop in
entrypoint.sh
A Practical Mental Model
Here is a simple way to picture the full setup:
- Docker Compose starts all services
- PostgreSQL starts
- Redis starts
- Django container starts
- The entrypoint script waits for PostgreSQL
- Django migrations run
- 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
.envfiles 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.
- Start with a simple three-service setup: Django, PostgreSQL, and Redis
- Keep secrets in environment variables
- Use an entrypoint script for startup logic
- Learn how Docker networking works through service names
- Remove development-only mounts before production deployment
- Add health checks and proper monitoring as your app grows
- 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.