Эта статья — результат многократного прохождения всех граблей при деплое Django-приложений. Здесь собрано всё, что нужно знать для развертывания проекта на чистом Ubuntu Server 22.04: от первого подключения по SSH до настроенного мониторинга с автозапуском при перезагрузке.

Что мы будем делать

Берём чистый сервер и поднимаем на нём полноценное production-окружение:

  • Django-приложение через Gunicorn
  • Nginx в роли reverse proxy
  • SSL-сертификаты от Let's Encrypt
  • Автозапуск всех сервисов через systemd
  • Netdata для мониторинга состояния сервера

Все команды проверены на практике. Где возможны ошибки — расскажу о них заранее.

Содержание

  1. Подготовка сервера
  2. Подготовка проекта
  3. Настройка Gunicorn
  4. Настройка Nginx
  5. Настройка SSL через Let's Encrypt
  6. Финальная настройка безопасности
  7. Проверка работоспособности
  8. Дополнительные рекомендации

1. Подготовка сервера

1.1. Первое, что делаем после подключения

Подключаемся к серверу по SSH под root и обновляем систему. Это не формальность — часто в базовых образах Ubuntu отсутствуют критические обновления безопасности:

apt update
apt upgrade -y

1.2. Ставим необходимый софт

Устанавливаем Python, инструменты для работы с виртуальными окружениями, компиляторы и веб-сервер:

apt install -y python3 python3-pip python3-venv python3-dev build-essential libpq-dev nginx curl git

Пакет build-essential нужен для компиляции некоторых Python-пакетов с C-расширениями. libpq-dev понадобится, если будете использовать PostgreSQL (хотя в этом гайде мы остановимся на SQLite).

1.3. Настраиваем firewall

UFW (Uncomplicated Firewall) — простой и надёжный способ закрыть всё лишнее. Разрешаем только SSH, HTTP и HTTPS:

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
ufw status

Флаг --force пропускает интерактивное подтверждение. После включения проверяем статус — должны быть открыты порты 22, 80 и 443.

Важно: Если подключены не через порт 22, сначала разрешите свой SSH-порт отдельно, иначе потеряете доступ к серверу.

1.4. Создаём непривилегированного пользователя

Запускать веб-приложения от root — плохая идея с точки зрения безопасности. Создаём отдельного пользователя:

adduser siteuser
usermod -aG sudo siteuser

При создании система запросит пароль. Остальные поля (имя, телефон и т.д.) можно пропустить, нажимая Enter.

2. Подготовка проекта

2.1. Создаём структуру директорий

Переключаемся на созданного пользователя и готовим место для проекта:


su - siteuser
sudo mkdir -p /var/www/project
sudo chown -R siteuser:siteuser /var/www/project
cd /var/www/project

Директория /var/www/ — стандартное место для веб-приложений в Linux. Можно использовать другую, но зачем изобретать велосипед.

2.2. Загружаем код проекта

Если проект в Git:

git clone <URL_ВАШЕГО_РЕПОЗИТОРИЯ> # ЗАМЕНИТЕ НА СВОЙ URL

Если загружаете вручную, используйте scp или rsync:

# С локальной машины:
rsync -avz --exclude 'venv' --exclude '*.pyc' /path/to/local/project/ siteuser@your-server-ip:/var/www/project/

2.3. Виртуальное окружение

Виртуальное окружение изолирует зависимости проекта от системных пакетов. Это спасает от конфликтов версий и делает проект переносимым:

python3 -m venv venv
source venv/bin/activate

После активации в начале строки появится (venv) — значит, всё работает.

2.4. Устанавливаем зависимости

pip install --upgrade pip
pip install -r requirements.txt

Если requirements.txt нет, но есть Pipfile или poetry.lock — адаптируйте под свой инструмент управления зависимостями.

2.5. Конфигурация через переменные окружения

Критически важный момент: никогда не храните секретные ключи в коде. Django генерирует SECRET_KEY при создании проекта, но для production нужен новый, уникальный ключ.

Генерируем новый ключ:

python3 -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Создаём файл .env в корне проекта:

nano .env

Минимальное содержимое:

DJANGO_SECRET_KEY='ВСТАВЬТЕ_СЮДА_СГЕНЕРИРОВАННЫЙ_КЛЮЧ'  # ЗАМЕНИТЕ!
DJANGO_DEBUG=False

# Если используете дополнительные сервисы, добавьте их данные:
# EMAIL_HOST=smtp.example.com
# EMAIL_PORT=465
# EMAIL_HOST_USER='noreply@example.com'
# EMAIL_HOST_PASSWORD='your_password'

Сохраняем (Ctrl+O, Enter, Ctrl+X).

2.6. Учим Django читать .env

Устанавливаем python-dotenv:

pip install python-dotenv

В начале файла settings.py (после импортов os и Path) добавляем:

from dotenv import load_dotenv

load_dotenv()

Теперь Django будет автоматически подхватывать переменные из .env через os.getenv() или os.environ.get().

2.7. Защищаем .env

Ограничиваем доступ к файлу — только владелец может его читать и изменять:

chmod 600 .env

2.8. Применяем миграции

Создаём структуру базы данных:

python manage.py makemigrations
python manage.py migrate

Если используете кастомную модель пользователя через AUTH_USER_MODEL, убедитесь, что миграции для этого приложения создаются первыми. Иначе Django может запутаться в зависимостях.

2.9. Создаём суперпользователя

Для доступа в админку:

python manage.py createsuperuser

Следуем инструкциям. Пароль должен быть сложным — это production.

2.10. Собираем статику

Django не умеет эффективно отдавать статические файлы. Nginx справляется с этим в сотни раз быстрее, поэтому собираем всю статику в одну директорию:

python manage.py collectstatic --noinput

Флаг --noinput пропускает подтверждение перезаписи файлов.

2.11. Готовим директории для медиа и логов

mkdir -p media logs

2.12. Права доступа

SQLite требует прав на запись не только для файла базы, но и для родительской директории (для временных файлов блокировок):

chmod 664 db.sqlite3
chmod 775 . media logs

3. Настройка Gunicorn

Gunicorn — это WSGI-сервер, который запускает Django-приложение. Nginx будет проксировать запросы на Gunicorn, а тот — выполнять Python-код.

3.1. Тестовый запуск

Проверяем, что всё работает:

gunicorn --bind 0.0.0.0:8000 project.wsgi:application

Если видим строки о запуске workers — отлично. Жмём Ctrl+C и идём дальше.

3.2. Создаём systemd-сервис

systemd — система инициализации в современных Linux-дистрибутивах. Создаём юнит для автоматического управления Gunicorn.

Выходим из виртуального окружения:

deactivate

Создаём файл сервиса:

sudo nano /etc/systemd/system/gunicorn.service

Вставляем конфигурацию:

[Unit]
Description=Gunicorn daemon for project Django application  # ЗАМЕНИТЕ project
After=network.target

[Service]
User=siteuser  # ЗАМЕНИТЕ НА ВАШЕГО ПОЛЬЗОВАТЕЛЯ
Group=www-data
WorkingDirectory=/var/www/project  # ЗАМЕНИТЕ НА ПУТЬ К ВАШЕМУ ПРОЕКТУ
EnvironmentFile=/var/www/project/.env  # ЗАМЕНИТЕ НА ПУТЬ К ВАШЕМУ .env
ExecStart=/var/www/project/venv/bin/gunicorn \  # ЗАМЕНИТЕ ПУТИ
          --workers 3 \
          --bind unix:/var/www/project/project.sock \  # ЗАМЕНИТЕ ПУТИ
          project.wsgi:application  # ЗАМЕНИТЕ project НА ИМЯ ВАШЕГО ПРОЕКТА

[Install]
WantedBy=multi-user.target

Как это работает:

  • After=network.target — запускаем после инициализации сети
  • EnvironmentFile — загружаем переменные из .env в окружение процесса
  • --workers 3 — количество рабочих процессов (обычно (2 × CPU_cores) + 1)
  • --bind unix:... — используем UNIX-сокет вместо TCP-порта (быстрее и безопаснее)
  • WantedBy=multi-user.target — автозапуск при загрузке системы

3.3. Запускаем сервис

sudo systemctl daemon-reload
sudo systemctl start gunicorn
sudo systemctl enable gunicorn

Команда enable добавляет сервис в автозагрузку.

Проверяем статус:

sudo systemctl status gunicorn

Должно быть active (running). Если failed — смотрим логи:

sudo journalctl -u gunicorn -e

3.4. Проверяем сокет

Убеждаемся, что файл сокета создан:

ls -l /var/www/project/project.sock  # ЗАМЕНИТЕ ПУТЬ

4. Настройка Nginx

Nginx будет принимать все входящие запросы, отдавать статику напрямую (быстро), а динамические запросы проксировать на Gunicorn.

4.1. Создаём конфигурацию виртуального хоста

sudo nano /etc/nginx/sites-available/project  # ЗАМЕНИТЕ project

Базовая конфигурация:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;  # ЗАМЕНИТЕ НА ВАШИ ДОМЕНЫ

    client_max_body_size 20M;  # Максимальный размер загружаемых файлов

    # Отключаем логирование для favicon — его часто нет, и логи захламляются
    location = /favicon.ico { 
        access_log off; 
        log_not_found off; 
    }
    
    # Статика: отдаём напрямую через Nginx
    location /static/ {
        alias /var/www/project/staticfiles/;  # ЗАМЕНИТЕ ПУТЬ
        expires 30d;  # Кэшируем на 30 дней
        add_header Cache-Control "public, immutable";
    }

    # Медиафайлы: тоже напрямую
    location /media/ {
        alias /var/www/project/media/;  # ЗАМЕНИТЕ ПУТЬ
        expires 7d;
        add_header Cache-Control "public";
    }

    # Всё остальное: проксируем на Gunicorn
    location / {
        proxy_pass http://unix:/var/www/project/project.sock;  # ЗАМЕНИТЕ ПУТЬ
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Почему UNIX-сокет, а не TCP-порт: UNIX-сокеты работают в пределах одной машины и не требуют сетевого стека ОС. Это быстрее и безопаснее — к сокету нельзя подключиться извне даже при неправильной настройке файрвола.

4.2. Активируем конфигурацию

sudo ln -s /etc/nginx/sites-available/project /etc/nginx/sites-enabled/  # ЗАМЕНИТЕ project

4.3. Удаляем дефолтный виртуальный хост

sudo rm /etc/nginx/sites-enabled/default

4.4. Проверяем синтаксис

sudo nginx -t

Должно быть syntax is ok и test is successful. Если есть ошибки — внимательно проверяем пути и имена в конфиге.

4.5. Перезапускаем Nginx

sudo systemctl restart nginx

На этом этапе сайт должен быть доступен по HTTP. Проверяем через браузер или curl:

curl -I http://example.com  # ЗАМЕНИТЕ НА ВАШ ДОМЕН

Если видим 200 OK или редирект (301/302) — отлично, идём дальше.

5. Настройка SSL через Let's Encrypt

HTTP без шифрования — это прошлый век. Настраиваем HTTPS через бесплатные сертификаты Let's Encrypt.

5.1. Устанавливаем Certbot

sudo apt install -y certbot python3-certbot-nginx

5.2. Получаем сертификат

sudo certbot --nginx -d example.com -d www.example.com  # ЗАМЕНИТЕ НА ВАШИ ДОМЕНЫ

Certbot запросит email (для уведомлений об истечении срока) и попросит согласиться с условиями. Затем автоматически:

  1. Проверит, что домены действительно указывают на этот сервер
  2. Получит сертификат от Let's Encrypt
  3. Модифицирует конфиг Nginx, добавив блок для HTTPS
  4. Настроит редирект с HTTP на HTTPS

Классическая ошибка: если DNS-записи ещё не обновились или указывают на старый IP, Certbot выдаст ошибку вроде:

Detail: <YOUR_IP>: Invalid response

Решение: проверяем DNS через dig +short example.com. Когда команда вернёт правильный IP — повторяем попытку с Certbot.

5.3. Проверяем автообновление

Сертификаты Let's Encrypt действуют 90 дней. Certbot автоматически настраивает обновление через systemd-таймер. Тестируем:

sudo certbot renew --dry-run

Если команда отработала без ошибок — всё в порядке, обновление будет происходить автоматически.

5.4. Проверяем результат

Открываем сайт в браузере: https://example.com. Должен открыться с зелёным замочком.

6. Финальная настройка безопасности

6.1. Права для базы данных

SQLite — особенный. Ему нужны права на запись для файла БД и для родительской директории:

sudo chown siteuser:www-data /var/www/project/db.sqlite3  # ЗАМЕНИТЕ ПУТИ И ПОЛЬЗОВАТЕЛЯ
sudo chmod 664 /var/www/project/db.sqlite3
sudo chown siteuser:www-data /var/www/project
sudo chmod 775 /var/www/project

6.2. Права для media и logs

sudo chown -R siteuser:www-data /var/www/project/media  # ЗАМЕНИТЕ
sudo chown -R siteuser:www-data /var/www/project/logs
sudo chmod -R 775 /var/www/project/media
sudo chmod -R 775 /var/www/project/logs

6.3. Проверяем settings.py

Критически важно: DEBUG = False в production. При DEBUG = True Django показывает полные трейсбеки с путями, настройками и секретными ключами. Это подарок для атакующих.

Убедитесь, что в .env установлено DJANGO_DEBUG=False, а в settings.py это значение корректно читается.

7. Проверка работоспособности

7.1. Базовая проверка

Открываем браузер и проверяем:

  • https://example.com — работает
  • http://example.com — редиректит на HTTPS
  • https://example.com/admin/ — админка доступна
  • Статика и медиа загружаются

7.2. Статус сервисов

sudo systemctl status gunicorn
sudo systemctl status nginx

Оба должны быть active (running).

7.3. Мониторинг логов

Gunicorn:

sudo journalctl -u gunicorn -f

Nginx:

sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

Django (если настроено логирование в файл):

tail -f /var/www/project/logs/django.log  # ЗАМЕНИТЕ ПУТЬ

Флаг -f (follow) показывает логи в реальном времени.

7.4. Тест автозапуска

Перезагружаем сервер:

sudo reboot

После перезагрузки проверяем, что всё поднялось автоматически:

sudo systemctl status gunicorn nginx

8. Дополнительные рекомендации

8.1. Регулярные бэкапы

Настраиваем автоматическое резервное копирование через cron:

sudo crontab -e

Добавляем задачу для ежедневного бэкапа в 3 ночи:

0 3 * * * cp /var/www/project/db.sqlite3 /var/www/project/backups/db_$(date +\%Y\%m\%d).sqlite3

Предварительно создаём директорию:

mkdir -p /var/www/project/backups  # ЗАМЕНИТЕ ПУТЬ

8.2. Настройка мониторинга с Netdata

Netdata — мощная система мониторинга, которая собирает и визуализирует метрики в реальном времени. Установка занимает несколько минут, а польза огромная.

Установка Netdata

bash <(curl -Ss https://get.netdata.cloud/kickstart.sh)

Скрипт автоматически определит систему и установит все необходимое. Подтверждаем установку, когда система попросит.

Проверяем статус:

sudo systemctl status netdata

Безопасный доступ через Nginx

Netdata по умолчанию слушает на localhost:19999. Открывать этот порт в интернет — плохая идея. Настраиваем доступ через поддомен с аутентификацией.

Устанавливаем утилиту для создания паролей:

sudo apt install apache2-utils

Создаём файл с учётными данными:

sudo htpasswd -c /etc/nginx/.htpasswd admin

Вводим надёжный пароль (минимум 12 символов, буквы + цифры + спецсимволы).

Устанавливаем правильные права:

sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd

Почему именно 640: владелец (root) может читать и писать, группа (www-data, к которой принадлежит Nginx) может только читать, остальные — вообще ничего.

Создаём конфиг для Nginx:

sudo nano /etc/nginx/sites-available/netdata

Содержимое:

upstream netdata_backend {
    server 127.0.0.1:19999;
    keepalive 64;
}

server {
    listen 80;
    listen [::]:80;
    server_name monitor.example.com;  # ЗАМЕНИТЕ НА ВАШЕ ИМЯ ПОДДОМЕНА

    auth_basic "Netdata Monitoring";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location / {
        proxy_pass http://netdata_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}

Активируем конфигурацию:

sudo ln -s /etc/nginx/sites-available/netdata /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Не забываем настроить DNS-запись для поддомена monitor.example.com.

Получаем SSL для поддомена

sudo certbot --nginx -d monitor.example.com  # ЗАМЕНИТЕ НА ВАШ ПОДДОМЕН

Теперь панель мониторинга доступна по адресу https://monitor.example.com с защитой паролем.

Что мониторить в Netdata:

  • CPU: загрузка по ядрам, контекстные переключения
  • Memory: использование RAM и swap (если swap активно используется — не хватает памяти)
  • Disk I/O: операции чтения/записи (помогает выявить узкие места)
  • Network: входящий и исходящий трафик
  • Applications: ресурсы конкретных процессов (Gunicorn, Nginx, Python)

Типичное потребление памяти Netdata после установки — около 70-150 МБ.

Заключение

Теперь у вас полноценное production-окружение:

  • Django работает через Gunicorn
  • Nginx эффективно отдаёт статику и проксирует динамику
  • HTTPS настроен с автообновлением сертификатов
  • Всё запускается автоматически при перезагрузке
  • Безопасность на должном уровне
  • Мониторинг позволяет видеть проблемы до того, как они станут критическими

При возникновении проблем первым делом смотрите логи. Большинство ошибок подробно описаны в журналах systemd, Nginx и Django.

Регулярно обновляйте систему и зависимости проекта. Безопасность — это не разовое действие, а постоянный процесс.