Django-Admin.ru

Как создать сайт на Django: пошаговое руководство с примером

Создание сайта на Django с нуля на примере каталога: проект и приложение, модели, админка, шаблоны, форма заявки, статика, тесты и подготовка к деплою.

Редакция Django-Admin.ru
· 20 мин чтения

Содержание · 16 разделов
  1. Что получится в итоге
  2. Что нужно перед началом
  3. Установите Django в виртуальное окружение
  4. Создайте проект и приложение
  5. Настройте settings.py
  6. Опишите модели данных
  7. Наполните сайт через админку
  8. Напишите представления и маршруты
  9. Соберите шаблоны страниц
  10. Подключите статику: CSS и картинки оформления
  11. Добавьте форму заявки
  12. Проверьте сайт тестами
  13. Подготовьте сайт к деплою
  14. Вынесите настройки в переменные окружения
  15. Зафиксируйте зависимости и соберите статику
  16. Проверьте настройки безопасности
  17. Выберите, где разместить сайт
  18. Что добавить на сайт дальше
  19. Частые ошибки при создании сайта
  20. TemplateDoesNotExist: base.html
  21. CSS и фото не загружаются, в логе 404
  22. The STATICFILES_DIRS setting should not contain the STATIC_ROOT setting
  23. Cannot use ImageField because Pillow is not installed
  24. 403 Forbidden: CSRF verification failed
  25. NoReverseMatch: ‘catalog’ is not a registered namespace
  26. Частые вопросы
  27. Сколько времени нужно, чтобы сделать сайт на Django?
  28. Нужно ли знать HTML, CSS и JavaScript?
  29. Почему slug из русского названия получается пустым?
  30. Django или WordPress (конструктор) для сайта?
  31. Можно ли сделать сайт на Django без базы данных?
  32. Как перенести сайт на другой компьютер или сервер?
  33. Итог

Чтобы создать сайт на Django, установите Django в виртуальное окружение, создайте проект командой django-admin startproject и приложение командой startapp, опишите модели и примените миграции, наполните базу через админку, а страницы соберите из view, маршрутов и шаблонов. Затем подключите CSS, добавьте форму, проверьте всё тестами и подготовьте настройки к деплою.

В этом руководстве мы пройдём весь путь создания сайта на Django — от пустой папки до работающего каталога небольшой мастерской: категории и карточки товаров с фото, постраничный вывод, форма заявки, которая сохраняется в базу и видна в админке. Все примеры кода проверены на Django 6.1 и Python 3.12; на Django 5.2 LTS они работают без изменений, отличие в почтовых настройках отмечено в тексте. Если вы впервые слышите о Django, сначала прочитайте обзор Django для начинающих: там объяснено, что такое фреймворк и как устроен путь запроса.

Что получится в итоге

Сайт из трёх типов страниц и админки:

АдресЧто показывает
/все опубликованные товары, по 9 на странице
/category/tables/товары одной категории
/product/oak-table/карточка товара с фото, описанием и формой заявки
/admin/админка: товары, категории и полученные заявки

Структура готового проекта:

mysite/
├── manage.py
├── config/              # пакет настроек проекта
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py, asgi.py
├── catalog/             # приложение каталога
│   ├── admin.py
│   ├── forms.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   ├── views.py
│   ├── migrations/
│   └── templates/catalog/
│       ├── product_list.html
│       └── product_detail.html
├── templates/
│   └── base.html        # общий макет всех страниц
├── static/
│   └── css/style.css
└── media/               # фото, загруженные через админку

Тот же порядок работы подходит для блога, портфолио, сайта услуг или небольшого веб-приложения: меняются модели и шаблоны, шаги остаются прежними.

Что нужно перед началом

  • Python 3.12 или новее — Django 6.x не работает на более старых версиях. Для Python 3.10–3.11 берите Django 5.2 LTS.
  • Терминал: PowerShell на Windows, Terminal на macOS и Linux.
  • Редактор кода, например VS Code или PyCharm.
  • Базовый Python (функции, классы, импорты) и основы HTML.

Отдельный сервер базы данных не нужен: Django по умолчанию использует SQLite, которая встроена в Python и хранит данные в одном файле db.sqlite3. PostgreSQL понадобится позже, для продакшна.

Установите Django в виртуальное окружение

Создайте папку проекта, виртуальное окружение и установите Django вместе с Pillow — библиотекой для работы с изображениями. Без неё Django не позволит использовать поле ImageField для фото товаров.

Windows (PowerShell):

mkdir mysite
cd mysite
py -m venv .venv
.venv\Scripts\activate
python -m pip install django pillow

macOS и Linux:

mkdir mysite
cd mysite
python3 -m venv .venv
source .venv/bin/activate
python -m pip install django pillow

Проверьте версию:

python -m django --version
# → 6.1.2

Если команда не находится или PowerShell запрещает запуск скрипта активации, решения собраны в статье «Как установить Django».

Создайте проект и приложение

Проект — это сайт целиком: настройки, корневые маршруты, точки входа для веб-серверов. Приложение — модуль с одной областью ответственности, у нас это каталог. Точка в конце startproject создаёт проект в текущей папке без лишней вложенности:

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

Подробный разбор файлов проекта и вариантов запуска — в статье «Как создать проект Django», а устройство приложения и типичные ошибки при его создании — в статье «Как создать приложение в Django».

Настройте settings.py

Откройте config/settings.py и внесите пять изменений: подключите приложение, укажите папку общих шаблонов, язык и часовой пояс, пути к статике и загружаемым файлам.

# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'catalog',  # наше приложение
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # общие шаблоны проекта
        'APP_DIRS': True,
        # OPTIONS оставьте как есть
    },
]

LANGUAGE_CODE = 'ru'
TIME_ZONE = 'Europe/Moscow'

STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']  # ваши CSS, JS, картинки оформления
STATIC_ROOT = BASE_DIR / 'staticfiles'    # сюда collectstatic соберёт всё для продакшна

MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'           # файлы, загруженные пользователями

LANGUAGE_CODE = 'ru' переводит на русский админку, сообщения об ошибках форм и названия месяцев. Разница между статикой и медиа важна: статика — файлы оформления, которые вы пишете сами и храните в Git; медиа — файлы, которые загружают пользователи и администраторы через сайт. Их хранят и раздают раздельно.

Опишите модели данных

Модель — класс Python, которому соответствует таблица в базе. Для каталога нужны три модели: категория, товар и заявка.

# catalog/models.py
from django.db import models
from django.urls import reverse


class Category(models.Model):
    name = models.CharField('Название', max_length=100)
    slug = models.SlugField('URL', unique=True)

    class Meta:
        ordering = ['name']
        verbose_name = 'категория'
        verbose_name_plural = 'категории'

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('catalog:category', args=[self.slug])


class Product(models.Model):
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT, related_name='products',
        verbose_name='Категория',
    )
    name = models.CharField('Название', max_length=200)
    slug = models.SlugField('URL', unique=True)
    description = models.TextField('Описание', blank=True)
    price = models.DecimalField('Цена, ₽', max_digits=10, decimal_places=2)
    image = models.ImageField('Фото', upload_to='products/', blank=True)
    is_published = models.BooleanField('Опубликован', default=True)
    created_at = models.DateTimeField('Создан', auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'товар'
        verbose_name_plural = 'товары'

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('catalog:product', args=[self.slug])


class Inquiry(models.Model):
    product = models.ForeignKey(
        Product, on_delete=models.SET_NULL, null=True, related_name='inquiries',
        verbose_name='Товар',
    )
    name = models.CharField('Имя', max_length=100)
    phone = models.CharField('Телефон', max_length=30)
    message = models.TextField('Комментарий', blank=True)
    created_at = models.DateTimeField('Получена', auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'заявка'
        verbose_name_plural = 'заявки'

    def __str__(self):
        return f'{self.name}, {self.phone}'

Несколько решений, которые стоит понять:

  • SlugField хранит человекочитаемую часть адреса: /product/oak-table/ вместо /product/17/. Такие адреса понятнее посетителям и поисковым системам.
  • on_delete=models.PROTECT не даст удалить категорию, пока в ней есть товары. У заявки SET_NULL: если товар удалят, заявка останется в базе.
  • DecimalField хранит цену без ошибок округления, которые бывают у float.
  • get_absolute_url() возвращает адрес объекта. Шаблоны, админка (кнопка «Смотреть на сайте») и redirect() используют его, поэтому адреса не придётся собирать вручную.
  • verbose_name задаёт русские подписи полей в админке и формах.

Типы полей, параметры Meta и связи подробно разобраны в статье «Модели в Django».

Создайте и примените миграции — файлы, которые описывают изменения структуры базы:

python manage.py makemigrations catalog
python manage.py migrate
Migrations for 'catalog':
  catalog/migrations/0001_initial.py
    + Create model Category
    + Create model Product
    + Create model Inquiry

После каждого изменения моделей повторяйте эти две команды.

Наполните сайт через админку

Админка Django — готовый интерфейс для управления данными. Зарегистрируйте в ней модели:

# catalog/admin.py
from django.contrib import admin

from .models import Category, Inquiry, Product


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug']
    prepopulated_fields = {'slug': ['name']}


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'category', 'price', 'is_published']
    list_editable = ['price', 'is_published']
    list_filter = ['category', 'is_published']
    search_fields = ['name', 'description']
    prepopulated_fields = {'slug': ['name']}


@admin.register(Inquiry)
class InquiryAdmin(admin.ModelAdmin):
    list_display = ['name', 'phone', 'product', 'created_at']
    readonly_fields = ['product', 'name', 'phone', 'message', 'created_at']
  • prepopulated_fields заполняет slug автоматически, пока вы печатаете название, и транслитерирует кириллицу: «Стол из дуба» превратится в stol-iz-duba.
  • list_editable позволяет менять цену и публикацию прямо в списке товаров.
  • readonly_fields у заявок защищает их от случайной правки: менеджер только читает.

Создайте администратора и запустите сервер разработки:

python manage.py createsuperuser
python manage.py runserver

Откройте http://127.0.0.1:8000/admin/, войдите и добавьте 2–3 категории и несколько товаров с фото. Загруженные изображения Django сохранит в media/products/.

Напишите представления и маршруты

Представление (view) принимает HTTP-запрос и возвращает ответ. Одно представление покажет список товаров — весь или по категории, второе — карточку товара с формой заявки. Форму создадим чуть ниже.

# catalog/views.py
from django.contrib import messages
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render

from .forms import InquiryForm
from .models import Category, Product


def product_list(request, category_slug=None):
    category = None
    products = Product.objects.filter(is_published=True).select_related('category')
    if category_slug:
        category = get_object_or_404(Category, slug=category_slug)
        products = products.filter(category=category)

    page = Paginator(products, 9).get_page(request.GET.get('page'))
    return render(request, 'catalog/product_list.html', {
        'category': category,
        'categories': Category.objects.all(),
        'page': page,
    })


def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug, is_published=True)

    if request.method == 'POST':
        form = InquiryForm(request.POST)
        if form.is_valid():
            inquiry = form.save(commit=False)
            inquiry.product = product
            inquiry.save()
            messages.success(request, 'Спасибо! Мы перезвоним вам в течение дня.')
            return redirect(product)
    else:
        form = InquiryForm()

    return render(request, 'catalog/product_detail.html', {
        'product': product,
        'form': form,
    })

Что здесь происходит:

  • filter(is_published=True) скрывает черновики, а get_object_or_404 отвечает кодом 404 на несуществующий или снятый с публикации товар — вместо ошибки сервера.
  • select_related('category') загружает категории одним запросом вместе с товарами. Без него каждая карточка делала бы к базе отдельный запрос.
  • Paginator(...).get_page() режет список на страницы и сам обрабатывает некорректный номер в ?page=.
  • После успешной отправки формы view делает редирект. Без него обновление страницы в браузере отправило бы заявку повторно.

Маршруты приложения связывают адреса с представлениями:

# catalog/urls.py
from django.urls import path

from . import views

app_name = 'catalog'

urlpatterns = [
    path('', views.product_list, name='product_list'),
    path('category/<slug:category_slug>/', views.product_list, name='category'),
    path('product/<slug:slug>/', views.product_detail, name='product'),
]

Подключите их в корневом config/urls.py и разрешите серверу разработки отдавать загруженные фото:

# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('catalog.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

app_name = 'catalog' задаёт пространство имён: в шаблонах адрес строится как {% url 'catalog:product_list' %}, и имена маршрутов разных приложений не конфликтуют. Строка со static() работает только при DEBUG = True — в продакшне медиафайлы отдаёт веб-сервер.

Соберите шаблоны страниц

Общий макет — шапку, подвал, подключение CSS и вывод сообщений — вынесите в базовый шаблон. Страницы будут наследовать его и заполнять только блок контента.

{# templates/base.html #}
{% load static %}
<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}Мастерская{% endblock %}</title>
  <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
  <header class="header">
    <a class="logo" href="{% url 'catalog:product_list' %}">Мастерская</a>
  </header>

  <main class="container">
    {% for message in messages %}
      <p class="message">{{ message }}</p>
    {% endfor %}

    {% block content %}{% endblock %}
  </main>

  <footer class="footer">© {% now "Y" %} Мастерская</footer>
</body>
</html>

Список товаров с фильтром по категориям и пагинацией:

{# catalog/templates/catalog/product_list.html #}
{% extends 'base.html' %}

{% block title %}{{ category.name|default:'Каталог' }} — Мастерская{% endblock %}

{% block content %}
  <h1>{{ category.name|default:'Каталог' }}</h1>

  <nav class="categories">
    <a href="{% url 'catalog:product_list' %}">Все</a>
    {% for cat in categories %}
      <a href="{{ cat.get_absolute_url }}">{{ cat.name }}</a>
    {% endfor %}
  </nav>

  <div class="grid">
    {% for product in page %}
      <article class="card">
        {% if product.image %}
          <img src="{{ product.image.url }}" alt="{{ product.name }}">
        {% endif %}
        <h2><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></h2>
        <p class="price">{{ product.price|floatformat:"0g" }} ₽</p>
      </article>
    {% empty %}
      <p>Товаров пока нет.</p>
    {% endfor %}
  </div>

  {% if page.has_other_pages %}
    <nav class="pagination">
      {% if page.has_previous %}
        <a href="?page={{ page.previous_page_number }}">← Назад</a>
      {% endif %}
      <span>Страница {{ page.number }} из {{ page.paginator.num_pages }}</span>
      {% if page.has_next %}
        <a href="?page={{ page.next_page_number }}">Вперёд →</a>
      {% endif %}
    </nav>
  {% endif %}
{% endblock %}

Карточка товара с формой:

{# catalog/templates/catalog/product_detail.html #}
{% extends 'base.html' %}

{% block title %}{{ product.name }} — Мастерская{% endblock %}

{% block content %}
  <a href="{{ product.category.get_absolute_url }}">← {{ product.category.name }}</a>
  <h1>{{ product.name }}</h1>

  {% if product.image %}
    <img class="photo" src="{{ product.image.url }}" alt="{{ product.name }}">
  {% endif %}
  <p class="price">{{ product.price|floatformat:"0g" }} ₽</p>
  {{ product.description|linebreaks }}

  <h2>Оставить заявку</h2>
  <form method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button type="submit">Отправить</button>
  </form>
{% endblock %}

Пояснения к шаблонам:

  • Шаблоны приложения лежат в catalog/templates/catalog/ — второй уровень catalog защищает от конфликта имён, если в другом приложении тоже окажется product_list.html.
  • Фильтр floatformat:"0g" округляет цену до рублей и добавляет разделитель разрядов по правилам языка сайта: 25000 выводится как «25 000 ₽».
  • {% csrf_token %} обязателен в каждой POST-форме: без него Django отклонит отправку с ошибкой 403.
  • {% if product.image %} нужен, потому что фото необязательно: обращение к .url у пустого поля вызывает ValueError.

Наследование, фильтры и теги подробно описаны в статье «Шаблоны Django».

Подключите статику: CSS и картинки оформления

Файлы оформления положите в папку static/ в корне проекта, которую мы указали в STATICFILES_DIRS:

/* static/css/style.css */
body {
  margin: 0;
  font-family: system-ui, sans-serif;
  color: #222;
}
.header,
.footer {
  padding: 16px 24px;
  background: #0c4b33;
  color: #fff;
}
.header a {
  color: #fff;
  text-decoration: none;
  font-weight: 700;
}
.container {
  max-width: 960px;
  margin: 0 auto;
  padding: 24px;
}
.categories a {
  margin-right: 12px;
}
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
}
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}
.card img,
.photo {
  max-width: 100%;
  border-radius: 6px;
}
.price {
  font-weight: 700;
}
.message {
  padding: 12px;
  background: #e6f4ea;
  border-radius: 6px;
}
.errorlist {
  color: #b00020;
}

В шаблоне файл подключается тегом {% static 'css/style.css' %} после {% load static %} в начале шаблона. Тег строит адрес из STATIC_URL, поэтому при переносе статики на CDN шаблоны менять не придётся. Логотип и иконки кладите туда же, например в static/img/, и подключайте так же.

Для разработки этого достаточно: при DEBUG = True сервер runserver сам находит статику в STATICFILES_DIRS и в папках static/ приложений. Готовые CSS-фреймворки вроде Bootstrap подключаются через CDN-ссылку в base.html или файлом в static/.

Добавьте форму заявки

Форма проверяет данные посетителя и сохраняет заявку в базу. ModelForm строит поля по модели, поэтому описывать их второй раз не нужно:

# catalog/forms.py
from django import forms

from .models import Inquiry


class InquiryForm(forms.ModelForm):
    class Meta:
        model = Inquiry
        fields = ['name', 'phone', 'message']
        widgets = {'message': forms.Textarea(attrs={'rows': 3})}

    def clean_phone(self):
        phone = self.cleaned_data['phone']
        digits = ''.join(ch for ch in phone if ch.isdigit())
        if len(digits) < 10:
            raise forms.ValidationError('Укажите телефон полностью, с кодом.')
        return phone
  • fields перечисляет только поля, которые заполняет посетитель. Товар view подставляет сам через form.save(commit=False), поэтому подменить его из браузера нельзя.
  • Метод clean_phone() — своя проверка поля. Если она не прошла, форма вернётся на страницу с текстом ошибки под полем, а введённые данные сохранятся.
  • Сообщение «Спасибо!» выводит фреймворк сообщений (django.contrib.messages): view кладёт его перед редиректом, а цикл в base.html показывает на следующей странице.

Запустите сервер, откройте карточку товара и отправьте заявку — она появится в админке в разделе «Заявки». Чтобы менеджер узнавал о заявках сразу, добавьте отправку письма или сообщения в мессенджер после inquiry.save(). Виджеты, валидацию нескольких полей и оформление форм разбирает статья «Формы в Django».

Проверьте сайт тестами

Пара тестов страхует от поломок при следующих изменениях. Django запускает их во временной базе, ваши данные не пострадают:

# catalog/tests.py
from django.test import TestCase
from django.urls import reverse

from .models import Category, Inquiry, Product


class CatalogTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.category = Category.objects.create(name='Столы', slug='tables')
        cls.product = Product.objects.create(
            category=cls.category, name='Стол из дуба', slug='oak-table', price=25000,
        )

    def test_product_list(self):
        response = self.client.get(reverse('catalog:product_list'))
        self.assertContains(response, 'Стол из дуба')

    def test_hidden_product_returns_404(self):
        self.product.is_published = False
        self.product.save()
        response = self.client.get(self.product.get_absolute_url())
        self.assertEqual(response.status_code, 404)

    def test_inquiry_is_saved(self):
        response = self.client.post(
            self.product.get_absolute_url(),
            {'name': 'Анна', 'phone': '+7 900 123-45-67', 'message': ''},
        )
        self.assertRedirects(response, self.product.get_absolute_url())
        self.assertEqual(Inquiry.objects.get().product, self.product)

    def test_short_phone_is_rejected(self):
        response = self.client.post(
            self.product.get_absolute_url(), {'name': 'Анна', 'phone': '123'},
        )
        self.assertContains(response, 'Укажите телефон полностью')
        self.assertFalse(Inquiry.objects.exists())
python manage.py test
Found 4 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 4 tests in 0.083s

OK
Destroying test database for alias 'default'...

Подготовьте сайт к деплою

runserver предназначен только для разработки. Перед публикацией сайта в интернете нужно вынести секреты из кода, выключить режим отладки и собрать статику.

Вынесите настройки в переменные окружения

Ключ шифрования, режим отладки и список доменов отличаются на вашем компьютере и на сервере, поэтому их читают из переменных окружения:

# config/settings.py
import os
from pathlib import Path

SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-dev-only')
DEBUG = os.environ.get('DJANGO_DEBUG') == '1'
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '127.0.0.1,localhost').split(',')

Теперь режим отладки по умолчанию выключен — случайно оставить его включённым на сервере не получится. Если забыть задать DJANGO_SECRET_KEY, check --deploy предупредит о небезопасном ключе (security.W009). Локально включайте его перед запуском сервера:

# Windows (PowerShell)
$env:DJANGO_DEBUG = "1"
python manage.py runserver
# macOS и Linux
export DJANGO_DEBUG=1
python manage.py runserver

На сервере задайте DJANGO_SECRET_KEY (длинная случайная строка, например из python -c "import secrets; print(secrets.token_urlsafe(50))") и DJANGO_ALLOWED_HOSTS с вашим доменом.

Зафиксируйте зависимости и соберите статику

python -m pip freeze > requirements.txt
python manage.py collectstatic --noinput
131 static files copied to '/home/user/mysite/staticfiles'.

collectstatic копирует в STATIC_ROOT ваш CSS и стили админки. Если после деплоя админка открылась без оформления, обычно забыт именно этот шаг. В .gitignore добавьте .venv/, db.sqlite3, media/ и staticfiles/: окружение, база и загруженные файлы в Git не хранятся.

Проверьте настройки безопасности

python manage.py check --deploy

Команда перечислит, что ещё нужно включить для работы по HTTPS: SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, SECURE_HSTS_SECONDS. В проекте на Django 6.1 она также выдаёт ошибку mail.E001: шаблон settings.py настраивает консольный почтовый бэкенд, который печатает письма в терминал вместо отправки.

Выберите, где разместить сайт

ВариантЧто делаете выКому подходит
VPS с Gunicorn и Nginxнастраиваете сервер, базу, HTTPS и обновлениянужен полный контроль, несколько сайтов
Docker на VPS или PaaSпишете Dockerfile, платформа запускает контейнерхотите одинаковое окружение везде
Хостинг с поддержкой Pythonзагружаете код и указываете WSGI-файл в панелинебольшой сайт, минимум администрирования

На Linux-сервере сайт запускает сервер приложений Gunicorn, а Nginx принимает запросы из интернета, раздаёт staticfiles/ и media/ и обеспечивает HTTPS:

python -m pip install gunicorn
gunicorn config.wsgi:application --bind 127.0.0.1:8000

Gunicorn работает только на Linux и macOS; на Windows для проверки используйте runserver. Настройку systemd, Nginx, HTTPS и PostgreSQL по шагам описывает статья «Деплой Django: Gunicorn, Nginx и чек-лист продакшна».

Что добавить на сайт дальше

  • Пользователи и личный кабинет — регистрация, вход, @login_required для закрытых страниц. Приложение django.contrib.auth уже подключено.
  • Поиск — поле ?q= и фильтр name__icontains в product_list.
  • Статические страницы «О нас» и «Контакты» — TemplateView с отдельным шаблоном или модель Page, которую редактируют в админке.
  • SEO — уникальные <title> и description через блоки шаблона, sitemap.xml встроенным приложением django.contrib.sitemaps.
  • API для мобильного приложения или фронтенда на JavaScript — Django REST Framework.
  • PostgreSQL вместо SQLite для продакшна с заметной нагрузкой и одновременной записью.

Частые ошибки при создании сайта

TemplateDoesNotExist: base.html

Django не нашёл шаблон. Проверьте, что в TEMPLATES указано 'DIRS': [BASE_DIR / 'templates'], папка templates лежит рядом с manage.py, а шаблоны приложения находятся в catalog/templates/catalog/ и приложение есть в INSTALLED_APPS.

CSS и фото не загружаются, в логе 404

При DEBUG = False Django не отдаёт ни статику, ни медиа. Локально включите DJANGO_DEBUG=1; на сервере раздавайте staticfiles/ и media/ через Nginx. Если стилей нет даже в режиме отладки — проверьте {% load static %} в начале шаблона и путь static/css/style.css.

The STATICFILES_DIRS setting should not contain the STATIC_ROOT setting

Ошибка staticfiles.E002 появляется, если STATIC_ROOT и STATICFILES_DIRS указывают на одну папку, например обе на static. Папка с исходниками и папка для collectstatic должны быть разными: static и staticfiles.

Cannot use ImageField because Pillow is not installed

Ошибка fields.E210 при makemigrations или runserver. Установите Pillow в активное виртуальное окружение: python -m pip install pillow.

403 Forbidden: CSRF verification failed

В форме нет {% csrf_token %}, либо сайт работает за HTTPS-прокси, а домен не указан в CSRF_TRUSTED_ORIGINS со схемой: https://example.ru.

NoReverseMatch: ‘catalog’ is not a registered namespace

Django не знает пространство имён из {% url 'catalog:...' %}. Проверьте app_name = 'catalog' в catalog/urls.py и подключение include('catalog.urls') в config/urls.py.

Частые вопросы

Сколько времени нужно, чтобы сделать сайт на Django?

Каталог из этого руководства собирается за один-два вечера, если вы знаете основы Python. Сайт с личным кабинетом, оплатой и интеграциями — недели работы: основное время уходит на бизнес-логику, вёрстку и тестирование, настройка самого Django занимает малую часть.

Нужно ли знать HTML, CSS и JavaScript?

HTML и базовый CSS нужны: шаблоны Django — это HTML с переменными и тегами. JavaScript для сайта с серверной отрисовкой необязателен. Интерактивность без отдельного фронтенда добавляют библиотекой HTMX, а готовую вёрстку — CSS-фреймворком Bootstrap или Tailwind.

Почему slug из русского названия получается пустым?

Функция django.utils.text.slugify() по умолчанию удаляет кириллицу: slugify('Стол из дуба') вернёт пустую строку. С параметром allow_unicode=True получится стол-из-дуба — адрес с кириллицей. Латинский slug из русского текста делает поле prepopulated_fields в админке или сторонние пакеты транслитерации, например python-slugify.

Django или WordPress (конструктор) для сайта?

Для сайта-визитки или блога без особой логики быстрее взять готовую CMS. Django выбирают, когда нужны свои модели данных, бизнес-логика, личные кабинеты, интеграции и API: всё это пишется кодом без ограничений плагинов. Если нужна именно CMS на Python с удобным редактором страниц, посмотрите Wagtail — он построен на Django.

Можно ли сделать сайт на Django без базы данных?

Технически да: оставьте DATABASES пустым и отключите приложения, которым нужна база, — админку, auth, сессии. Но тогда пропадают главные преимущества Django. Для чисто статического сайта удобнее генератор статики, а SQLite не требует установки и подходит даже для небольших сайтов в продакшне.

Как перенести сайт на другой компьютер или сервер?

Храните код в Git вместе с requirements.txt и миграциями. На новой машине создайте виртуальное окружение, выполните python -m pip install -r requirements.txt и python manage.py migrate. Данные из SQLite переносят копированием файла db.sqlite3 или командами dumpdata и loaddata, загруженные файлы — копированием папки media/.

Итог

Сайт на Django строится в одном порядке независимо от темы: виртуальное окружение и Django, проект и приложение, модели и миграции, админка для наполнения, view и маршруты, шаблоны с общим макетом, статика, формы, тесты и подготовка настроек к продакшну. Собранный каталог — рабочая основа: замените модели на посты, услуги или проекты, и получится блог, сайт компании или портфолио.

Каждый шаг подробнее раскрыт в статьях, на которые мы ссылались по ходу руководства, а общий маршрут изучения фреймворка — в обзоре Django для начинающих.

  • #Django
  • #Python
  • #Веб-разработка
  • #Для начинающих
  • #Практика
↑ ↓ выборEnter открытьEsc закрыть