Task Manager API with Django and Docker

Build a containerized Django REST API with MySQL and push it to GitHub.

Introduction

30 Second Summary

Every app you use daily has a backend quietly handling your data. When you add a reminder, check off a to-do, or filter tasks by priority, a server somewhere processes that request and stores the result.

In this project, you will build a Task Manager REST API from scratch using Django, connect it to a MySQL database, run everything inside Docker containers, and push the finished project to GitHub.

What You'll Build

A fully containerized Task Manager API with CRUD endpoints, a MySQL database, and a simple frontend page to interact with your tasks visually.

By the end of this project, you'll have:

  • A Django REST Framework API with full CRUD endpoints for creating, reading, updating, and deleting tasks.
  • A Docker Compose configuration running your Django app alongside a MySQL database in connected containers.
  • A simple HTML/JavaScript frontend served by Django that lets you add, view, filter, and delete tasks through your API.
  • A complete GitHub repository with proper .gitignore and README documenting your project.
  • Secret Mission: Add a Category model with a ForeignKey relationship to Task, plus an endpoint to filter tasks by category.

Are there any prerequisites?

No prior experience with Django or Docker is required. This guide walks you through installing and setting up every tool from scratch.

Set Up the Project, Virtual Environment, and Git

Before you can build an API, you need a solid foundation. That means a project folder, the right packages installed, and version control from day one.

In this step, you'll scaffold a Django project, install the packages that power your REST API, and push everything to GitHub so your code is backed up and shareable.

In this step, get ready to:
  • Create a project folder with a Python virtual environment and install dependencies.
  • Scaffold a Django project and app using the Django CLI.
  • Initialize Git, push to GitHub, and configure Django settings.
Create the project folder and install packages

A virtual environment isolates your project's Python packages from the rest of your system. This prevents version conflicts between projects.

  • Open the terminal panel in VS Code by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir task-manager-api
cd task-manager-api
  • Create a virtual environment by running:
python -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activation command for your operating system. On macOS, use source venv/bin/activate. On Windows, use venv\Scripts\activate.

help me activate my Python virtual environment

  • Install the four packages your project needs by running:
pip install django==6.0.6 djangorestframework==3.17.1 django-cors-headers==4.9.0 mysqlclient==2.2.8

What are these packages?

  • django==6.0.6 is the web framework that powers your API.
  • djangorestframework==3.17.1 adds REST API tools like serializers and viewsets.
  • django-cors-headers==4.9.0 allows your frontend to talk to your API across different origins.
  • mysqlclient==2.2.8 lets Django connect to a MySQL database.

You should see a success message ending with Successfully installed followed by the package names.

Seeing an error during pip install?

If mysqlclient fails to install, make sure your virtual environment is active (you should see (venv) in your prompt). On Windows, mysqlclient installs from a pre-built wheel, so it should work without extra system dependencies.

help me fix my pip install error for mysqlclient

  • Scaffold the Django project and create the tasks app by running these commands:
django-admin startproject taskmanager .
python manage.py startapp tasks

What do these commands create?

django-admin startproject taskmanager . creates the Django project configuration files in your current folder. The dot at the end means "put files here" instead of creating a nested folder.

python manage.py startapp tasks creates a tasks app folder where your Task model and API code will live.

  • Verify the scaffold worked by running the Django development server:
python manage.py runserver
  • Open your browser and visit http://127.0.0.1:8000/.
  • You should see the Django welcome page with a rocket icon.
  • Stop the server by pressing Ctrl+C in the terminal.

Don't see the Django welcome page?

Make sure your virtual environment is still active (look for (venv) in the prompt). Check that you ran django-admin startproject taskmanager . with the dot at the end.

help me fix my Django runserver error

  • Now open the project folder in VS Code so you can edit files.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the task-manager-api folder on your Desktop, then click Open.

Why open the folder in VS Code?

Opening the project folder gives VS Code full context of your file tree. You'll see all your Django files in the Explorer sidebar and can edit them directly.

  • Open a new terminal in VS Code by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Reactivate your virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate
Initialize Git and push to GitHub

Version control tracks every change you make. Pushing to GitHub backs up your code and makes it shareable with others.

  • Create a .gitignore file in the root of your task-manager-api folder. In the VS Code Explorer sidebar, right-click and select New File.
  • Name it .gitignore and paste the following content:
venv/
__pycache__/
*.pyc
*.pyo
db.sqlite3
.env
*.egg-info/
dist/
build/
.DS_Store

What does .gitignore do?

This file tells Git which files and folders to ignore. You don't want to push your virtual environment (venv/), compiled Python files (__pycache__/), or sensitive files (.env) to GitHub.

  • Save .gitignore.

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

venv/
__pycache__/
*.pyc
*.pyo
db.sqlite3
.env
*.egg-info/
dist/
build/
.DS_Store
  • Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: Django project setup"

What do these commands do?

  • git init turns the folder into a Git repository.
  • git add . stages all files (except those in .gitignore) for the commit.
  • git commit saves a snapshot of your project at this point.
  • Go to github.com in your browser.
  • Click the + icon in the top right corner, then select New repository.
  • Enter task-manager-api as the repository name.
  • Leave Add a README file unchecked.
  • Leave Add .gitignore set to None.
  • Click Create repository.
  • Connect your local project to GitHub and push your code by running these commands:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/task-manager-api.git
git branch -M main
git push -u origin main

What do these commands do?

  • git remote add origin links your local repo to the GitHub repo you just created.
  • git branch -M main renames your branch to main.
  • git push -u origin main uploads your code to GitHub.
  • Refresh your GitHub repository page in the browser.
  • You should see your project files listed including manage.py, the taskmanager/ folder, and the tasks/ folder.

Push failed or asking for credentials?

If Git asks for your username and password, GitHub no longer accepts passwords for HTTPS pushes. You'll need to use a Personal Access Token or set up SSH authentication.

help me push my code to GitHub

Configure Django settings

Django's generated settings file needs a few additions to enable the REST framework, CORS headers, and your tasks app.

  • In the VS Code Explorer sidebar, expand the taskmanager folder and open settings.py.
  • Replace the entire contents of the file with the following:
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "django-insecure-change-this-in-production"

DEBUG = True

ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "tasks",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "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 = "taskmanager.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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 = "taskmanager.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

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"},
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CORS_ALLOW_ALL_ORIGINS = True

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}

What changed from the default?

  • INSTALLED_APPS now includes rest_framework, corsheaders, and tasks so Django knows about your new packages and app.
  • corsheaders.middleware.CorsMiddleware is added right after SecurityMiddleware so cross-origin requests are handled early.
  • CORS_ALLOW_ALL_ORIGINS = True lets your frontend talk to the API without origin restrictions during development.
  • REST_FRAMEWORK sets the default permission to AllowAny so you don't need authentication for this learning project.
  • ALLOWED_HOSTS = ["*"] lets Django serve requests from any hostname, which is needed when running inside Docker later.
  • Save settings.py.
  • Verify your configuration is valid by running:
python manage.py check

You should see System check identified no issues (0 silenced). in the output.

Seeing an error from manage.py check?

If you see ModuleNotFoundError, make sure your virtual environment is active (look for (venv) in your prompt). If you see a syntax error, double check that your settings.py matches the code above exactly.

help me fix my Django settings.py error

✔️ Awesome, I've got everything!

Great. Make sure settings.py is saved before moving on.

ⓧ I'd like to double check the full code

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "django-insecure-change-this-in-production"

DEBUG = True

ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "tasks",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "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 = "taskmanager.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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 = "taskmanager.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

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"},
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CORS_ALLOW_ALL_ORIGINS = True

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}

Your project foundation is solid. Next up, you'll build the Task model and create a full REST API with endpoints to create, read, update, and delete tasks.

Build the Task Model and REST API

Your Django project is scaffolded, your packages are installed, and your code is already on GitHub. Now it's time to build the actual functionality.

Every API needs a data structure to work with. In this step, you'll define a Task model that describes what a task looks like in your database, then build the REST API endpoints that let clients create, read, update, and delete those tasks.

In this step, get ready to:
  • Define the Task model with status, priority, and date fields.
  • Create a serializer and ViewSet for full CRUD operations.
  • Wire up URL routing and test the browsable API.
Create the Task model

A model in Django is a Python class that maps directly to a database table. Each field on the model becomes a column in that table.

  • Open tasks/models.py in VS Code and replace its contents with the following code:
from django.db import models


class Task(models.Model):
    # Define the allowed choices for status
    STATUS_CHOICES = [
        ("pending", "Pending"),
        ("in_progress", "In Progress"),
        ("completed", "Completed"),
    ]
    # Define the allowed choices for priority
    PRIORITY_CHOICES = [
        ("low", "Low"),
        ("medium", "Medium"),
        ("high", "High"),
    ]

    # Core task fields
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
    priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default="medium")
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-created_at"]

What does this code do?

This defines a Task class that Django will turn into a database table called tasks_task.

  • STATUS_CHOICES and PRIORITY_CHOICES restrict what values can go in those fields. Django enforces these at the form and API level.
  • auto_now_add=True on created_at automatically sets the timestamp when a task is first created.
  • The Meta class with ordering = ["-created_at"] means tasks always come back newest-first.
  • Save tasks/models.py.

Now register the model with Django's admin site so you can manage tasks through the admin panel later.

  • Open tasks/admin.py and replace its contents with this code:
from django.contrib import admin
from .models import Task

admin.site.register(Task)
  • Save tasks/admin.py.

Seeing a red underline on the import?

VS Code may show a warning on from .models import Task because the model hasn't been migrated yet. This is safe to ignore for now. The import will resolve once migrations run later in this step.

Help me understand why VS Code shows import warnings for Django models.

Create the serializer and ViewSet

A serializer converts your Django model instances to JSON (and back). A ViewSet bundles all the CRUD logic (list, create, retrieve, update, delete) into a single class.

  • Create a new file called tasks/serializers.py and add the following code:
from rest_framework import serializers
from .models import Task


# Converts Task model instances to/from JSON
class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = "__all__"

What does this code do?

ModelSerializer automatically generates serializer fields based on your model's fields. Setting fields = "__all__" means every field on the Task model gets included in the API response.

  • Save tasks/serializers.py.

Now build the ViewSet that handles incoming API requests and uses the serializer to process data.

  • Open tasks/views.py and replace its contents with this code:
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

    def get_queryset(self):
        # Start with all tasks
        queryset = Task.objects.all()
        # Check for optional query parameters
        status = self.request.query_params.get("status")
        priority = self.request.query_params.get("priority")
        # Filter by status if provided
        if status:
            queryset = queryset.filter(status=status)
        # Filter by priority if provided
        if priority:
            queryset = queryset.filter(priority=priority)
        return queryset

What does this code do?

ModelViewSet gives you all five CRUD operations (list, create, retrieve, update, destroy) with zero extra code. The get_queryset() method adds filtering on top.

When a client sends a request like /api/tasks/?status=pending, the method reads that query parameter and narrows the results to only pending tasks.

  • Save tasks/views.py.

Import error on TaskSerializer?

Make sure you saved tasks/serializers.py before opening tasks/views.py. The import won't resolve if the serializer file doesn't exist yet.

Help me fix my Django import error.

Wire up URLs and test the API

Django REST Framework's router automatically generates URL patterns for your ViewSet. You just need to register your ViewSet with a router and connect it to the main URL configuration.

  • Create a new file called tasks/urls.py and add the following code:
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

# Register the ViewSet with the router
router = DefaultRouter()
router.register(r"tasks", TaskViewSet)

urlpatterns = router.urls

What does this code do?

DefaultRouter automatically creates URL patterns for all CRUD operations. Registering TaskViewSet with the prefix "tasks" generates endpoints like /tasks/, /tasks/1/, etc.

  • Save tasks/urls.py.

Now connect the tasks URLs to the main project URL configuration under the /api/ prefix.

  • Open taskmanager/urls.py and find this line:
urlpatterns = [
    path("admin/", admin.site.urls),
]
  • Add the api/ path so the full urlpatterns list looks like this:
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
]
  • Save taskmanager/urls.py.

The model exists in code, but the database doesn't have a matching table yet. Run migrations to create it.

  • Create and apply the database migrations by running this command:
python manage.py makemigrations && python manage.py migrate

What does this command do?

makemigrations reads your model and generates a migration file describing the database changes. migrate applies those changes, creating the actual table in your SQLite database.

  • Start the development server by running:
python manage.py runserver
  • Open your browser and visit http://127.0.0.1:8000/api/tasks/.

You should see the Django REST Framework browsable API page with an empty list of tasks and a form at the bottom to create new ones.

Seeing an error page instead?

  • Check the terminal for error messages. A common issue is a typo in taskmanager/urls.py. Make sure include is imported at the top of the file.
  • Verify you ran makemigrations and migrate successfully. If either command showed errors, double-check your tasks/models.py for typos.

Help me debug my Django REST API error.

  • Stop the server by pressing Ctrl+C in the terminal.

Finally, commit your work and push to GitHub.

  • Commit and push your changes by running these commands:
git add . && git commit -m "Add Task model and REST API" && git push
  • Visit your repository on GitHub to confirm the new files appear.

✔️ Awesome, I've got everything!

Great. Double check you've saved all files before moving on.

ⓧ I'd like to double check the full code

Here's what each file should look like at this point.

from django.db import models


class Task(models.Model):
    STATUS_CHOICES = [
        ("pending", "Pending"),
        ("in_progress", "In Progress"),
        ("completed", "Completed"),
    ]
    PRIORITY_CHOICES = [
        ("low", "Low"),
        ("medium", "Medium"),
        ("high", "High"),
    ]

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
    priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default="medium")
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    class Meta:
        ordering = ["-created_at"]
from rest_framework import serializers
from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = "__all__"
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

    def get_queryset(self):
        queryset = Task.objects.all()
        status = self.request.query_params.get("status")
        priority = self.request.query_params.get("priority")
        if status:
            queryset = queryset.filter(status=status)
        if priority:
            queryset = queryset.filter(priority=priority)
        return queryset
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register(r"tasks", TaskViewSet)

urlpatterns = router.urls
from django.contrib import admin
from .models import Task

admin.site.register(Task)
from django.contrib import admin
from django.urls import path, include

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

Your API is live and testable. Next up, you'll containerize this application with Docker and swap SQLite for a proper MySQL database.

Dockerize with Docker Compose

Your API works locally with SQLite, but real-world applications need a proper database and a reproducible way to run everything.

In this step, you'll containerize your Django app and a MySQL database using Docker Compose. Both services will run together in isolated containers that anyone can spin up with a single command.

In this step, get ready to:
  • Create a Dockerfile and requirements file for your Django app.
  • Set up Docker Compose to orchestrate Django and MySQL containers.
  • Update Django settings to connect to MySQL and run everything from Docker.
Create requirements.txt and Dockerfile

Docker needs to know which Python packages to install inside the container. A requirements.txt file lists them with pinned versions so every build is identical.

  • Create a new file called requirements.txt in the root of your task-manager-api folder and paste the following:
django==6.0.6
djangorestframework==3.17.1
mysqlclient==2.2.8
django-cors-headers==4.9.0

What does this file do?

Each line pins a package to an exact version. This guarantees that everyone who builds the Docker image gets the same dependencies, avoiding "works on my machine" issues.

  • Save requirements.txt.

Next, you need a Dockerfile. This is a recipe that tells Docker how to build your application image: which base image to use, which system libraries to install, and how to start the server.

  • Create a new file called Dockerfile (no file extension) in the root of your task-manager-api folder and paste the following:
FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y \
    default-libmysqlclient-dev \
    build-essential \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

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"]

What does this Dockerfile do?

  • FROM python:3.14-slim starts from a lightweight Python image.
  • The RUN apt-get block installs system libraries that mysqlclient needs to compile against MySQL.
  • COPY requirements.txt . copies the requirements file first so Docker can cache the install layer separately from your code.
  • EXPOSE 8000 documents which port the container listens on.
  • CMD starts Django's development server, listening on all interfaces so the host machine can reach it.
  • Save Dockerfile.
  • Confirm both files exist by running:
ls

You should see requirements.txt and Dockerfile alongside your existing manage.py and taskmanager/ folder.

Don't see the files?

Make sure you created both files in the task-manager-api root folder, not inside the taskmanager/ or tasks/ subfolder. The Dockerfile must have no extension (not Dockerfile.txt).

Help me find where my Dockerfile should go in my Django project

Create docker-compose.yml

Docker Compose lets you define multiple containers in a single file and start them together. You need two services: a MySQL database and your Django web app.

  • Create a new file called docker-compose.yml in the root of your task-manager-api folder and paste the following:
services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: taskmanager
      MYSQL_USER: taskuser
      MYSQL_PASSWORD: taskpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    environment:
      - DB_NAME=taskmanager
      - DB_USER=taskuser
      - DB_PASSWORD=taskpassword
      - DB_HOST=db
      - DB_PORT=3306
    depends_on:
      db:
        condition: service_healthy

volumes:
  mysql_data:

What does this configuration do?

  • The db service pulls the official mysql:8.4 image and creates a database called taskmanager with a dedicated user.
  • The healthcheck pings MySQL every 10 seconds. The web service uses depends_on: condition: service_healthy to wait until MySQL is ready before starting Django.
  • The volumes: - .:/app mount means your local code changes appear inside the container without rebuilding.
  • The mysql_data named volume persists your database between container restarts.

Why Docker Compose instead of running containers manually?

Without Compose, you would need separate docker run commands for each container, manually creating networks so they can talk to each other. Compose handles networking, startup order, and volume management in one declarative file.

  • Save docker-compose.yml.

YAML indentation errors?

YAML is whitespace-sensitive. Make sure you use spaces (not tabs) for indentation. Each nested level should be indented exactly 2 spaces from its parent.

Help me fix YAML indentation in my docker-compose.yml

Update settings and run containers

Django currently connects to SQLite. You need to switch it to MySQL and read the connection details from environment variables that Docker Compose passes into the container.

  • In taskmanager/settings.py, find the DATABASES section that looks like this:
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}
  • Replace the entire DATABASES block with the following:
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": os.environ.get("DB_NAME", "taskmanager"),
        "USER": os.environ.get("DB_USER", "taskuser"),
        "PASSWORD": os.environ.get("DB_PASSWORD", "taskpassword"),
        "HOST": os.environ.get("DB_HOST", "db"),
        "PORT": os.environ.get("DB_PORT", "3306"),
    }
}

What does this code do?

  • os.environ.get("DB_NAME", "taskmanager") reads the database name from an environment variable. If the variable is missing, it falls back to the default value.
  • The engine switches from django.db.backends.sqlite3 to django.db.backends.mysql so Django knows to speak MySQL's protocol.
  • The HOST defaults to db, which is the service name Docker Compose uses as a hostname for the MySQL container.
  • Save taskmanager/settings.py.

The os module is already imported at the top of your settings file, so no extra import is needed.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: requirements.txt, Dockerfile, docker-compose.yml, and taskmanager/settings.py.

ⓧ I'd like to double check the full code

Here is the full content of each file you created or edited in this step:

django==6.0.6
djangorestframework==3.17.1
mysqlclient==2.2.8
django-cors-headers==4.9.0
FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y \
    default-libmysqlclient-dev \
    build-essential \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

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"]
services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: taskmanager
      MYSQL_USER: taskuser
      MYSQL_PASSWORD: taskpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    environment:
      - DB_NAME=taskmanager
      - DB_USER=taskuser
      - DB_PASSWORD=taskpassword
      - DB_HOST=db
      - DB_PORT=3306
    depends_on:
      db:
        condition: service_healthy

volumes:
  mysql_data:
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "django-insecure-change-this-in-production"

DEBUG = True

ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "tasks",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "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 = "taskmanager.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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 = "taskmanager.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": os.environ.get("DB_NAME", "taskmanager"),
        "USER": os.environ.get("DB_USER", "taskuser"),
        "PASSWORD": os.environ.get("DB_PASSWORD", "taskpassword"),
        "HOST": os.environ.get("DB_HOST", "db"),
        "PORT": os.environ.get("DB_PORT", "3306"),
    }
}

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"},
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CORS_ALLOW_ALL_ORIGINS = True

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.AllowAny",
    ]
}

Now it is time to build and run your containers. First, stop the local development server if it is still running.

  • If python manage.py runserver is still running in your terminal, press Ctrl+C to stop it.
  • Make sure Docker Desktop is running on your machine.
  • Build and start both containers by running:
docker-compose up --build

What should I see?

Docker will download the MySQL image, build your Django image, and start both containers. You will see log output from both services. Wait until you see the MySQL health check passing and Django reporting Starting development server at http://0.0.0.0:8000/.

The first build takes a few minutes because Docker needs to download base images and install system packages. Subsequent builds are much faster thanks to layer caching.

Your containers are running, but the MySQL database is empty. You need to run Django's migrations inside the container to create the database tables.

  • Open a new terminal in VS Code by pressing Ctrl+Shift+` (macOS and Windows).
  • Run migrations inside the web container by running:
docker-compose exec web python manage.py migrate

What does this command do?

docker-compose exec runs a command inside an already-running container. Here it executes python manage.py migrate in the web service, which creates all the Django and Task tables in your MySQL database.

You should see output listing each migration being applied (like Applying tasks.0001_initial... OK).

  • Create a superuser for the Django admin panel by running:
docker-compose exec web python manage.py createsuperuser
  • The command will prompt you for a username, email, and password. Enter any values you like (for example, admin for the username).
  • Open your browser and visit http://localhost:8000/api/tasks/.

You should see the DRF browsable API page, now powered by MySQL running inside a Docker container.

Seeing a connection error or blank page?

  • Check that both containers are running. In the first terminal, you should see active log output from both db and web.
  • If you see a "connection refused" error in the Django logs, MySQL may still be initializing. Wait 30 seconds and refresh.
  • Make sure you stopped the local runserver first. If port 8000 was already in use, Docker cannot bind to it.

Help me debug my Docker Compose setup for Django and MySQL

  • Commit and push your Docker configuration to GitHub by running:
git add . && git commit -m "Add Docker and MySQL configuration" && git push

Your API is now running inside containers with a real MySQL database. Next up, you'll build a frontend page so you can interact with your tasks visually through the browser.

Add the Frontend and Push to GitHub

Your API is running inside Docker and serving data from MySQL. The backend is solid, but right now the only way to interact with it is through the DRF browsable API or a tool like curl.

In this step, you'll build a simple HTML/CSS/JavaScript frontend that talks to your API. You'll also write a proper README.md so anyone visiting your GitHub repo knows what the project does and how to run it.

In this step, get ready to:
  • Create an HTML/CSS/JS frontend page that uses your Task API.
  • Wire up a Django view and URL to serve the frontend.
  • Write a README and push the final project to GitHub.
Create the frontend HTML template

Django uses a specific folder structure for templates. The convention is tasks/templates/tasks/ so Django can find templates namespaced to your app.

  • Create the template directory structure by running this command:
mkdir -p tasks/templates/tasks
  • Confirm the directory was created by running:
ls tasks/templates/tasks

You should see an empty directory (no errors).

Seeing 'No such file or directory'?

On Windows, use backslashes instead:

mkdir tasks\templates\tasks

Help me create the templates directory on my operating system.

  • Create a new file at tasks/templates/tasks/index.html and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
        h1 { text-align: center; margin-bottom: 20px; color: #333; }
        .form-container { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .form-container h2 { margin-bottom: 15px; color: #555; }
        .form-group { margin-bottom: 10px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; }
        .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        .form-group textarea { height: 80px; resize: vertical; }
        button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; }
        button:hover { background: #0056b3; }
        .task-list { list-style: none; }
        .task-item { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
        .task-info h3 { color: #333; margin-bottom: 5px; }
        .task-info p { color: #777; font-size: 14px; }
        .task-meta { text-align: right; }
        .badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
        .badge-pending { background: #fff3cd; color: #856404; }
        .badge-in_progress { background: #cce5ff; color: #004085; }
        .badge-completed { background: #d4edda; color: #155724; }
        .badge-low { background: #d1ecf1; color: #0c5460; }
        .badge-medium { background: #fff3cd; color: #856404; }
        .badge-high { background: #f8d7da; color: #721c24; }
        .delete-btn { background: #dc3545; margin-left: 10px; padding: 5px 10px; font-size: 12px; }
        .delete-btn:hover { background: #c82333; }
        .filters { margin-bottom: 20px; display: flex; gap: 10px; }
        .filters select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
    </style>
</head>
<body>
    <h1>Task Manager</h1>

    <div class="form-container">
        <h2>Add New Task</h2>
        <div class="form-group">
            <label for="title">Title</label>
            <input type="text" id="title" placeholder="Enter task title">
        </div>
        <div class="form-group">
            <label for="description">Description</label>
            <textarea id="description" placeholder="Enter task description"></textarea>
        </div>
        <div class="form-group">
            <label for="priority">Priority</label>
            <select id="priority">
                <option value="low">Low</option>
                <option value="medium" selected>Medium</option>
                <option value="high">High</option>
            </select>
        </div>
        <div class="form-group">
            <label for="due_date">Due Date</label>
            <input type="date" id="due_date">
        </div>
        <button onclick="createTask()">Add Task</button>
    </div>

    <div class="filters">
        <select id="filter-status" onchange="loadTasks()">
            <option value="">All Statuses</option>
            <option value="pending">Pending</option>
            <option value="in_progress">In Progress</option>
            <option value="completed">Completed</option>
        </select>
        <select id="filter-priority" onchange="loadTasks()">
            <option value="">All Priorities</option>
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
        </select>
    </div>

    <ul class="task-list" id="task-list"></ul>

    <script>
        const API_URL = "/api/tasks/";

        async function loadTasks() {
            const status = document.getElementById("filter-status").value;
            const priority = document.getElementById("filter-priority").value;
            let url = API_URL;
            const params = new URLSearchParams();
            if (status) params.append("status", status);
            if (priority) params.append("priority", priority);
            if (params.toString()) url += "?" + params.toString();

            const response = await fetch(url);
            const tasks = await response.json();
            const list = document.getElementById("task-list");
            list.innerHTML = "";
            tasks.forEach(function(task) {
                const li = document.createElement("li");
                li.className = "task-item";
                li.innerHTML =
                    '<div class="task-info">' +
                        '<h3>' + task.title + '</h3>' +
                        '<p>' + (task.description || "No description") + '</p>' +
                    '</div>' +
                    '<div class="task-meta">' +
                        '<span class="badge badge-' + task.status + '">' + task.status + '</span> ' +
                        '<span class="badge badge-' + task.priority + '">' + task.priority + '</span>' +
                        '<br><small>' + (task.due_date || "No due date") + '</small>' +
                        '<button class="delete-btn" onclick="deleteTask(' + task.id + ')">Delete</button>' +
                    '</div>';
                list.appendChild(li);
            });
        }

        async function createTask() {
            const title = document.getElementById("title").value;
            const description = document.getElementById("description").value;
            const priority = document.getElementById("priority").value;
            const due_date = document.getElementById("due_date").value;

            if (!title) { alert("Title is required!"); return; }

            const body = { title: title, description: description, priority: priority, status: "pending" };
            if (due_date) body.due_date = due_date;

            await fetch(API_URL, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(body)
            });

            document.getElementById("title").value = "";
            document.getElementById("description").value = "";
            document.getElementById("priority").value = "medium";
            document.getElementById("due_date").value = "";
            loadTasks();
        }

        async function deleteTask(id) {
            await fetch(API_URL + id + "/", { method: "DELETE" });
            loadTasks();
        }

        loadTasks();
    </script>
</body>
</html>

What does this code do?

This single HTML file contains three layers that work together:

  • The <style> block handles all the CSS. It creates a clean card layout for tasks and color-coded badges for status and priority.
  • The HTML body provides a form to create tasks, filter dropdowns for status and priority, and a <ul> element where tasks render dynamically.
  • The <script> block uses fetch() to call your Django API. It handles GET (load tasks), POST (create task), and DELETE (remove task) requests.

Because the frontend uses a relative URL (/api/tasks/), it avoids CORS issues entirely. Both the page and the API are served from the same Django instance.

  • Save the file.
  • You won't see a change in the browser yet. Django needs a view and URL route to serve this template, which you'll set up next.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
        h1 { text-align: center; margin-bottom: 20px; color: #333; }
        .form-container { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .form-container h2 { margin-bottom: 15px; color: #555; }
        .form-group { margin-bottom: 10px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; }
        .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        .form-group textarea { height: 80px; resize: vertical; }
        button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; }
        button:hover { background: #0056b3; }
        .task-list { list-style: none; }
        .task-item { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
        .task-info h3 { color: #333; margin-bottom: 5px; }
        .task-info p { color: #777; font-size: 14px; }
        .task-meta { text-align: right; }
        .badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
        .badge-pending { background: #fff3cd; color: #856404; }
        .badge-in_progress { background: #cce5ff; color: #004085; }
        .badge-completed { background: #d4edda; color: #155724; }
        .badge-low { background: #d1ecf1; color: #0c5460; }
        .badge-medium { background: #fff3cd; color: #856404; }
        .badge-high { background: #f8d7da; color: #721c24; }
        .delete-btn { background: #dc3545; margin-left: 10px; padding: 5px 10px; font-size: 12px; }
        .delete-btn:hover { background: #c82333; }
        .filters { margin-bottom: 20px; display: flex; gap: 10px; }
        .filters select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
    </style>
</head>
<body>
    <h1>Task Manager</h1>

    <div class="form-container">
        <h2>Add New Task</h2>
        <div class="form-group">
            <label for="title">Title</label>
            <input type="text" id="title" placeholder="Enter task title">
        </div>
        <div class="form-group">
            <label for="description">Description</label>
            <textarea id="description" placeholder="Enter task description"></textarea>
        </div>
        <div class="form-group">
            <label for="priority">Priority</label>
            <select id="priority">
                <option value="low">Low</option>
                <option value="medium" selected>Medium</option>
                <option value="high">High</option>
            </select>
        </div>
        <div class="form-group">
            <label for="due_date">Due Date</label>
            <input type="date" id="due_date">
        </div>
        <button onclick="createTask()">Add Task</button>
    </div>

    <div class="filters">
        <select id="filter-status" onchange="loadTasks()">
            <option value="">All Statuses</option>
            <option value="pending">Pending</option>
            <option value="in_progress">In Progress</option>
            <option value="completed">Completed</option>
        </select>
        <select id="filter-priority" onchange="loadTasks()">
            <option value="">All Priorities</option>
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
        </select>
    </div>

    <ul class="task-list" id="task-list"></ul>

    <script>
        const API_URL = "/api/tasks/";

        async function loadTasks() {
            const status = document.getElementById("filter-status").value;
            const priority = document.getElementById("filter-priority").value;
            let url = API_URL;
            const params = new URLSearchParams();
            if (status) params.append("status", status);
            if (priority) params.append("priority", priority);
            if (params.toString()) url += "?" + params.toString();

            const response = await fetch(url);
            const tasks = await response.json();
            const list = document.getElementById("task-list");
            list.innerHTML = "";
            tasks.forEach(function(task) {
                const li = document.createElement("li");
                li.className = "task-item";
                li.innerHTML =
                    '<div class="task-info">' +
                        '<h3>' + task.title + '</h3>' +
                        '<p>' + (task.description || "No description") + '</p>' +
                    '</div>' +
                    '<div class="task-meta">' +
                        '<span class="badge badge-' + task.status + '">' + task.status + '</span> ' +
                        '<span class="badge badge-' + task.priority + '">' + task.priority + '</span>' +
                        '<br><small>' + (task.due_date || "No due date") + '</small>' +
                        '<button class="delete-btn" onclick="deleteTask(' + task.id + ')">Delete</button>' +
                    '</div>';
                list.appendChild(li);
            });
        }

        async function createTask() {
            const title = document.getElementById("title").value;
            const description = document.getElementById("description").value;
            const priority = document.getElementById("priority").value;
            const due_date = document.getElementById("due_date").value;

            if (!title) { alert("Title is required!"); return; }

            const body = { title: title, description: description, priority: priority, status: "pending" };
            if (due_date) body.due_date = due_date;

            await fetch(API_URL, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(body)
            });

            document.getElementById("title").value = "";
            document.getElementById("description").value = "";
            document.getElementById("priority").value = "medium";
            document.getElementById("due_date").value = "";
            loadTasks();
        }

        async function deleteTask(id) {
            await fetch(API_URL + id + "/", { method: "DELETE" });
            loadTasks();
        }

        loadTasks();
    </script>
</body>
</html>
Add the view and URL to serve the frontend

Django needs two things to serve your HTML: a view function that renders the template, and a URL pattern that maps a path to that view.

  • Create a new file at tasks/template_views.py and paste the following code:
from django.shortcuts import render


# Render the frontend HTML page
def index(request):
    return render(request, "tasks/index.html")

What does this code do?

The index function receives a request and returns the rendered tasks/index.html template. Django looks for this file inside tasks/templates/ because APP_DIRS is set to True in your settings.

  • Save the file.

✔️ Awesome, I've got everything!

Great. Make sure you've saved before continuing.

ⓧ I'd like to double check the full code

from django.shortcuts import render


def index(request):
    return render(request, "tasks/index.html")
  • Create a new file at tasks/template_urls.py and paste the following code:
from django.urls import path
from .template_views import index

# URL pattern to serve the frontend at the root path
urlpatterns = [
    path("", index, name="index"),
]

What does this code do?

This maps an empty path ("") to your index view. When someone visits the root URL of your site, Django serves the frontend page.

  • Save the file.

✔️ Awesome, I've got everything!

Great. One more file to update and the frontend will be live.

ⓧ I'd like to double check the full code

from django.urls import path
from .template_views import index

urlpatterns = [
    path("", index, name="index"),
]

Now you need to tell the main project URL configuration to include your new template URLs at the root path.

  • Open taskmanager/urls.py and find the urlpatterns list:
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
]
  • Add a new path for the frontend template URLs at the end of the list, so it looks like this:
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
    path("", include("tasks.template_urls")),
]
  • Save taskmanager/urls.py.
  • Visit http://localhost:8000/ in your browser. You should see the Task Manager page with a form at the top, filter dropdowns, and an empty task list below.
  • Type a title like Buy groceries in the title field and click Add Task. You should see the new task appear in the list below.

Seeing a blank page or an error?

  • Check that your Docker containers are still running. Run docker-compose ps to verify both web and db show as running.
  • Make sure the file is saved at exactly tasks/templates/tasks/index.html. A typo in the directory name (like template instead of templates) causes a TemplateDoesNotExist error.
  • Verify that the new line in taskmanager/urls.py uses tasks.template_urls (not tasks.urls).

Help me debug why my Django frontend page isn't loading at localhost:8000.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files.

ⓧ I'd like to double check the full code

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

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
    path("", include("tasks.template_urls")),
]
Add a README and push to GitHub

A good README tells other developers what your project does and how to run it. GitHub renders README.md files automatically on the repository page.

  • Create a new file called README.md in the root of your task-manager-api folder and paste the following:
# Task Manager API

A REST API for managing tasks, built with Django REST Framework and MySQL, containerized with Docker.

## Tech Stack

- Python 3.14
- Django 6.0
- Django REST Framework 3.17
- MySQL 8.4
- Docker and Docker Compose

## Quick Start

1. Clone the repository:
   ```
   git clone https://github.com/YOUR_USERNAME/task-manager-api.git
   cd task-manager-api
   ```

2. Start the application:
   ```
   docker-compose up --build
   ```

3. Run migrations (in a new terminal):
   ```
   docker-compose exec web python manage.py migrate
   ```

4. Create a superuser (optional):
   ```
   docker-compose exec web python manage.py createsuperuser
   ```

5. Visit http://localhost:8000/ for the frontend or http://localhost:8000/api/tasks/ for the API.

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/tasks/ | List all tasks |
| POST | /api/tasks/ | Create a task |
| GET | /api/tasks/{id}/ | Get a single task |
| PUT | /api/tasks/{id}/ | Update a task |
| DELETE | /api/tasks/{id}/ | Delete a task |

## Filtering

- By status: /api/tasks/?status=pending
- By priority: /api/tasks/?priority=high
- Combined: /api/tasks/?status=pending&priority=high

## Stopping the Application

```
docker-compose down
```

To also remove the database volume:
```
docker-compose down -v
```

What does this README include?

  • A project description and tech stack summary at the top.
  • A Quick Start section so anyone can clone and run your project with Docker.
  • An API endpoints table documenting every route your API supports.
  • Filtering examples showing how to use query parameters.
  • Save README.md.

✔️ Awesome, I've got everything!

Great. Save the file and move on to the final push.

ⓧ I'd like to double check the full code

# Task Manager API

A REST API for managing tasks, built with Django REST Framework and MySQL, containerized with Docker.

## Tech Stack

- Python 3.14
- Django 6.0
- Django REST Framework 3.17
- MySQL 8.4
- Docker and Docker Compose

## Quick Start

1. Clone the repository:
   ```
   git clone https://github.com/YOUR_USERNAME/task-manager-api.git
   cd task-manager-api
   ```

2. Start the application:
   ```
   docker-compose up --build
   ```

3. Run migrations (in a new terminal):
   ```
   docker-compose exec web python manage.py migrate
   ```

4. Create a superuser (optional):
   ```
   docker-compose exec web python manage.py createsuperuser
   ```

5. Visit http://localhost:8000/ for the frontend or http://localhost:8000/api/tasks/ for the API.

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/tasks/ | List all tasks |
| POST | /api/tasks/ | Create a task |
| GET | /api/tasks/{id}/ | Get a single task |
| PUT | /api/tasks/{id}/ | Update a task |
| DELETE | /api/tasks/{id}/ | Delete a task |

## Filtering

- By status: /api/tasks/?status=pending
- By priority: /api/tasks/?priority=high
- Combined: /api/tasks/?status=pending&priority=high

## Stopping the Application

```
docker-compose down
```

To also remove the database volume:
```
docker-compose down -v
```
  • Commit and push the frontend and README to GitHub by running:
git add . && git commit -m "Add frontend and README" && git push
  • Visit your repository on GitHub. You should see all your project files listed and the README rendered nicely below them with the project description, tech stack, and API endpoints table.

Push rejected or authentication error?

  • If you see an authentication error, GitHub requires a Personal Access Token (not your password) for HTTPS pushes. Generate one from Settings > Developer settings > Personal access tokens on GitHub.
  • If the push is rejected with "non-fast-forward", you may have changes on the remote that are not in your local branch. Run git pull --rebase origin main then push again.

Help me fix my git push to GitHub.

Secret mission

Add Categories with Model Relationships

Your Task Manager works great, but real-world apps need a way to organize tasks into groups. Can you add a Category model, link it to tasks with a ForeignKey, and build a full CRUD endpoint with filtering?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally using Docker, so there are no ongoing cloud costs.

Resources you used:

  • Docker containers (web + db services) running on your local machine.
  • A MySQL data volume (mysql_data) storing your database on local disk.
  • The task-manager-api project folder on your Desktop.
  • A GitHub repository named task-manager-api.

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with the API.

  • Your Docker containers will keep running until you restart your machine or stop them manually.
  • If you stopped them earlier, start them again by running docker-compose up -d from the task-manager-api folder.
  • The containers and volume use only local disk space. There is no cost to keeping them.

Pause - I'll come back to this later

Shut down running containers to free up memory, but keep your data and files intact for next time.

  • Stop the containers by running this command from the task-manager-api folder:
docker-compose stop
  • This stops both containers without deleting any data. Your MySQL volume and project files stay on disk.
  • When you're ready to resume, run docker-compose up from the same folder to bring everything back.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes containers, the database volume, your local files, and the GitHub repository.

  • Remove containers and the MySQL data volume by running this command from the task-manager-api folder:
docker-compose down -v
  • Delete the project folder from your Desktop:

macOS

cd ~/Desktop
rm -rf task-manager-api

Windows

cd ~\Desktop
rmdir /s /q task-manager-api
  • Delete the GitHub repository by going to your repository page on GitHub.
  • Click Settings at the top of the repository.
  • Scroll to the bottom to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts.

Nice Work!

Nice Work!

Nice work! You just built a fully containerized REST API from scratch using Django REST Framework, MySQL, and Docker Compose.

You've learned how to:

  • Built a Django REST Framework API with full CRUD endpoints and query parameter filtering for status and priority.
  • Containerized Django and MySQL together using Docker Compose with health checks and persistent volumes.
  • Shipped the complete project to GitHub with a frontend page, proper .gitignore, and a README documenting the API.
  • Secret Mission: Added a Category model with a ForeignKey relationship to Task and filtering by category.

Ready to quiz yourself?