Week 3 — Django Setup & Docker Containerization

Goal: By the end of this handout, you will have a small Django app running locally, and then running inside a Docker container. Type every command yourself — don't copy-paste. Typing builds muscle memory.

Prerequisites

  • Python 3.11+ installed (python3 --version)
  • Docker Desktop (or Docker Engine) installed and running (docker --version)
  • A terminal and a code editor

Part 1 — Set up your environment

Create a project folder and an isolated Python environment (a "virtual environment") so this project's packages don't collide with anything else on your machine.

mkdir mini-crm && cd mini-crm
python3 -m venv venv

# Activate it:
source venv/bin/activate        # macOS/Linux
venv\Scripts\activate           # Windows

Your terminal prompt should now show (venv) at the start of the line. That means it's active.

Install Django:

pip install django
pip freeze > requirements.txt

Checkpoint: Open requirements.txt. You should see a line like Django==5.x.x. This file is what Docker will use later to install the same packages inside the container.


Part 2 — Create the Django project and app

django-admin startproject config .
python manage.py startapp contacts

Notice the . at the end of the first command — it creates the project files in the current folder instead of nesting them one level deeper.

Open config/settings.py and register the app you just created. Find INSTALLED_APPS and add "contacts":

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "contacts",
]

Checkpoint: Run python manage.py runserver and visit http://127.0.0.1:8000/. You should see Django's welcome page. Stop the server with Ctrl+C when you've confirmed it.


Part 3 — Build a minimal model, view, and template

We'll build a tiny contact list — one model, one view, one template.

contacts/models.py

from django.db import models


class Contact(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    company = models.CharField(max_length=100, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

contacts/admin.py

from django.contrib import admin
from .models import Contact

admin.site.register(Contact)

contacts/views.py

from django.shortcuts import render
from .models import Contact


def contact_list(request):
    contacts = Contact.objects.all().order_by("name")
    return render(request, "contacts/contact_list.html", {"contacts": contacts})

contacts/urls.py (create this file)

from django.urls import path
from . import views

urlpatterns = [
    path("", views.contact_list, name="contact_list"),
]

config/urls.py — wire the app in

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("contacts/", include("contacts.urls")),
]

contacts/templates/contacts/contact_list.html (create the folders too)

<!DOCTYPE html>
<html>
<head><title>Contacts</title></head>
<body>
    <h1>Contacts</h1>
    <ul>
        {% for contact in contacts %}
            <li>{{ contact.name }} — {{ contact.email }} ({{ contact.company }})</li>
        {% empty %}
            <li>No contacts yet.</li>
        {% endfor %}
    </ul>
</body>
</html>

Now make and apply migrations, and create an admin login so you can add test data:

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser

Checkpoint: Run python manage.py runserver, log into /admin/, add a contact or two, then visit /contacts/ and confirm they show up.


Part 4 — Containerize it with Docker

Stop the local server (Ctrl+C) — from here on, Docker will run it for you.

Dockerfile (in the project root, next to manage.py)

FROM python:3.11-slim

# Prevents Python from buffering stdout/stderr — logs show up immediately
ENV PYTHONUNBUFFERED=1

WORKDIR /app

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

COPY . .

EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

.dockerignore — keeps junk out of the image

venv/
__pycache__/
*.pyc
db.sqlite3
.git/

Checkpoint — build the image:

docker build -t mini-crm .

Watch the output. Each RUN/COPY line becomes a "layer." If this fails, the error is almost always a missing or misspelled package in requirements.txt.

Run the container:

docker run -p 8000:8000 mini-crm

Visit http://127.0.0.1:8000/contacts/ again — it should look identical, but it's now being served from inside the container, not your local Python install.

Why the -p 8000:8000? The left number is the port on your machine; the right is the port inside the container (matching EXPOSE 8000). Docker forwards traffic between them.


Typing that docker run command with all its flags gets old fast. docker-compose lets you define it once.

docker-compose.yml

services:
  web:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    command: python manage.py runserver 0.0.0.0:8000

The volumes line mounts your local folder into the container, so code changes show up without rebuilding the image.

docker compose up --build

Stop it with Ctrl+C, then docker compose down to clean up.


Exercises (do these on your own before wk4)

  1. Add a phone field to the Contact model, make a migration, and confirm it shows up in the admin.
  2. Add a second model (Company) with a ForeignKey from Contact to Company, and update the template to show the company name.
  3. Break something on purpose — misspell a package name in requirements.txt and rebuild the image. Read the actual Docker error output. Fix it.

Troubleshooting cheat sheet

| Symptom | Likely cause | |---|---| | docker build fails on pip install | Typo or version conflict in requirements.txt | | Page loads locally but not in container | Forgot 0.0.0.0:8000 (container defaults to localhost, which isn't reachable from outside) | | Changes to code don't show up | No volume mount (Part 5), or you edited before rebuilding the image | | port is already allocated | Something else is already using port 8000 — stop it, or map to a different host port, e.g. -p 8001:8000 |

What's next

Week 4 covers Git/GitHub fundamentals and a deeper Docker admin guide. Keep this mini-crm project — you'll push it to GitHub as your first repo.