Django База [2023]: Деплой Django на VPS в Docker контейнере с SSL 💻 #50
Django

Django База [2023]: Деплой Django на VPS в Docker контейнере с SSL 💻 #50

Razilator

В данной статье мы подробно рассмотрим процесс развертывания проекта Django 4.1 на виртуальном сервере VPS с использованием технологий Docker, Certbot для настройки SSL-сертификатов и Gunicorn в качестве веб-сервера.

Исходить мы будем из данных прошлого урока по докеризации проекта.

Настройка env.prod

В прошлом уроке мы настроили env.dev, теперь нам необходимо создать env.prod с настоящими данными и выключить дебаг режим Django.

Файл env.prod создаем также в папке docker/env/env.prod, как в прошлом уроке.

docker/env/env.prod
SECRET_KEY=<djangokey>
DEBUG=0
ALLOWED_HOSTS=<IPAddressOrDomen>
CSRF_TRUSTED_ORIGINS=<IPAddressOrDomen>
POSTGRES_DB=<dbName>
POSTGRES_USER=<dbUser>
POSTGRES_PASSWORD=<dbPassword>
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
REDIS_LOCATION=redis://redis:6379/1
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
RECAPTCHA_PUBLIC_KEY=<recaptchaPublicKey>
RECAPTCHA_PRIVATE_KEY=<recaptchaPrivateKey>
EMAIL_HOST=<emalHost>
EMAIL_PORT=<emailPort>
EMAIL_USE_TLS=1
EMAIL_HOST_USER=<email>
EMAIL_HOST_PASSWORD=<emailPassword>

В env.prod мы добавляем домен сайта, если у нас есть и он припаркован к VPS, либо IP адрес VPS сервера, если у вас ещё нет домена и вы тестируете сайт публично. Debug режим Django отключаем, вместо ошибок будет выходить страница 500 - ошибка сервера.

Меняем настройки в settings.py

Нам необходимо изменить настройки, а именно строку environ.Env.read_env(env_file=Path('./docker/env/.env.dev')) которую мы прописали в прошлом уроке на environ.Env.read_env(env_file=Path('./docker/env/.env.prod')).

Дополняем requirements.txt

В прошлом уроке мы создали requirements.txt, с помощью команды: pip freeze > requirements.txt. Заходим в этот файл и в самый низ добавляем модуль gunicorn, версию можно не указывать, установится самая последняя доступная версия.

requirements.txt
amqp==5.1.1
asgiref==3.6.0
async-timeout==4.0.2
billiard==3.6.4.0
celery==5.2.7
click==8.1.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
colorama==0.4.6
Django==4.1.5
django-ckeditor-5==0.2.4
django-cleanup==7.0.0
django-environ==0.10.0
django-js-asset==2.0.0
django-mptt==0.14.0
django-recaptcha==3.0.0
django-taggit==3.1.0
django_debug_toolbar==3.8.1
kombu==5.2.4
lxml==4.9.2
Pillow==9.4.0
prompt-toolkit==3.0.38
psycopg2==2.9.5
pytils==0.4.1
pytz==2022.7.1
redis==4.5.1
six==1.16.0
sqlparse==0.4.3
tzdata==2022.7
vine==5.0.0
wcwidth==0.2.6
gunicorn

Примечание (необязательно): рекомендую удалить лишнее из зависимостей, чтобы при билдинге не возникло ошибок. Обычно я оставляю список зависимостей в котором библиотеки, установленные мной вручную. Остальные зависимости установятся уже сами, как дополнения к основным указанным зависимостям.

requirements.txt
celery==5.2.7
Django==4.1.5
django-ckeditor-5==0.2.4
django-cleanup==7.0.0
django-environ==0.10.0
django-mptt==0.14.0
django-recaptcha==3.0.0
django-taggit==3.1.0
django_debug_toolbar==3.8.1
Pillow==9.4.0
psycopg2==2.9.5
pytils==0.4.1
redis==4.5.1
gunicorn

Создание docker-compose.yml

Далее создадим файл docker-compose.yml в корне проекта, он будет немного отличаться от того docker-compose.dev.yml, который мы делали в прошлом уроке, а именно, мы добавим работу с gunicorn в Django, а также добавим образ Certbot'a для SSL.

docker-compose.yml
version: '3.8'

volumes:
  pgdata:
  static:
  media:

services:

  django:
    build:
      context: .
    ports:
      - '8000:8000'
    container_name: django
    env_file:
      - docker/env/.env.prod
    volumes:
      - ./:/app
      - static:/app/static
      - media:/app/media
    depends_on:
      - postgres
      - redis
    command: sh -c "python manage.py collectstatic --no-input &&
                    python manage.py makemigrations &&
                    python manage.py migrate &&
                    gunicorn --workers=4 --reload --max-requests=1000 backend.wsgi -b 0.0.0.0:8000""

  nginx:
    container_name: nginx
    working_dir: /app
    image: nginx:stable-alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - static:/app/static
      - media:/app/media
      - ./docker/nginx/prod/:/etc/nginx/conf.d:ro
      - ./docker/certbot/conf:/etc/letsencrypt:ro
      - ./docker/certbot/www:/var/www/certbot:ro
    links:
      - django
    depends_on:
      - django

  postgres:
    image: postgres:alpine
    container_name: postgres
    restart: always
    env_file:
      - docker/env/.env.prod
    volumes:
      - pgdata:/var/lib/postgresql/data/
  
  redis:
    image: redis:alpine
    container_name: redis
    env_file:
      - docker/env/.env.prod
    expose:
      - 6379
    volumes:
      - ./docker/redis/data:/data
  
  celery-worker:
    build: .
    container_name: celery-worker
    restart: always
    env_file:
      - docker/env/.env.prod
    volumes:
      - ./:/app
      - media:/app/media
    command: celery -A backend worker --loglevel=info --logfile=./docker/logs/celery-worker.log
    depends_on:
      - redis
  
  celery-beat:
      build: .
      container_name: celery-beat
      env_file:
          - docker/env/.env.prod
      depends_on: 
          - redis
      command: celery -A backend beat --loglevel=info --logfile=./docker/logs/celery-beat.log
      volumes:
          - media:/app/media
          - ./:/app

  certbot:
      image: certbot/certbot
      container_name: certbot
      volumes:
          - ./docker/certbot/conf:/etc/letsencrypt:rw
          - ./docker/certbot/www:/var/www/certbot:rw
      command: certonly --webroot --webroot-path=/var/www/certbot/ --email email@mail.com --agree-tos --no-eff-email -d example.com -d www.example.com
      depends_on:
        - nginx

В этом файле я добавил образ certbot, добавил порт 443 для работы с SSL, а также поменял в нужных местах env.dev на env.prod, добавил работу gunicorn в Django, и поменял volumes в nginx для работы с SSL и конфигом продакшена. Также добавил команду, которую необходимо выполнить первый раз при запуске Certbot с Nginx. Команда certonly --webroot --webroot-path=/var/www/certbot/ --email email@mail.com --agree-tos --no-eff-email -d example.com -d www.example.com, вместо example.com свой домен или ip адрес, вместо email@mail.com свой email.

Создаем конфигурации nginx

В папке docker/nginx/prod создаем конфиг django.conf для nginx, который будет использоваться в версии на деплое.

Заведомо до создания конфигурации у нас должен быть сервер, чтобы мы знали IP адрес и домен. Для урока я купил VPS, а домен у меня уже был.

Лично я использую для своих проектов VPS от Timeweb, используя Ubuntu 22.04. И для урока тестовый сервер имеет IP адрес: 81.200.151.210.

Характеристики VPS: 1 CPU / 2 RAM / 15 GB SSD
Характеристики VPS: 1 CPU / 2 RAM / 15 GB SSD

Далее пишем конфигурационный файл:

docker/nginx/prod/django.conf
upstream django {
    server django:8000;
}

server {
    listen 80;
    listen [::]:80;

    server_name доменное_имя;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# server {
#     listen 443 default_server ssl http2;
#     listen [::]:443 ssl http2;

#     server_name доменное_имя;
#     server_tokens off;

#     ssl_certificate /etc/letsencrypt/live/ваше_доменное_имя/fullchain.pem;
#     ssl_certificate_key /etc/letsencrypt/live/ваше_доменное_имя/privkey.pem;

#     client_max_body_size 20M;
#     charset utf-8;

#     gzip  on;
#     gzip_disable "msie6";
#     gzip_min_length 1000;
#     gzip_vary on;
#     gzip_proxied   expired no-cache no-store private auth;
#     gzip_types     text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;

#     location / {
#         proxy_set_header X-Forwarded-Proto https;
#         proxy_set_header X-Url-Scheme $scheme;
#         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#         proxy_set_header Host $http_host;
#         proxy_redirect off;
#         proxy_pass http://django;
#     }

#     location /static/ {
#         alias  /app/static/;
#         expires 15d;
#     }

#      location /media/ {
#         alias  /app/media/;
#         expires 7d;
#     }

#     if ($http_host !~ "^доменное_имя$"){
# 	    rewrite ^(.*)$ https://доменное_имя$1 redirect;
# 	}
# }

Со строки 21 до 63 конфигурация на данный момент закомментирована. Это необходимо, чтобы мы могли сначала запустить Nginx и Certbot без конфигурации для сервера Django и порта 443. Как вы знаете, Nginx будет выдавать ошибку без SSL-сертификата. Поэтому мы запускаем Nginx только для получения сертификата в первый раз. Я покажу вам, как это выглядит далее в уроке. Не волнуйтесь, мы снова раскомментируем нашу конфигурацию, когда получим сертификат.

Создаем необходимые папки для Certbot

Далее в папке docker мы создадим папку certbot, а в ней две папки: conf и www. В них после успешной настройки будут храниться сертификаты.

На данный момент структура проекта выглядит следующим образом
На данный момент структура проекта выглядит следующим образом

Работа с сервером

Далее нам необходимо подключиться к нашему VPS. На Windows это делается через терминал, я ввожу следующую команду: ssh root@ip_адрес_или_домен. В моем случае это ssh root@81.200.151.210. Подключаемся, вводим пароль от сервера.

Устанавливаем Docker и Docker Compose v2. Урок по установке Docker на Ubuntu 22.04, урок по установке Docker Compose v2.

Установил Docker
Установил Docker
Установил Docker Compose
Установил Docker Compose

Далее загружаем наш проект на сервер. Знатоки могут воспользоваться git клонированием, ну или традиционным способом копирования папок на сервер. Для экономии времени я закину проект на git и клонирую его на сервер в папку /home/.

Должно получиться вот так. Я скопировал проект на сервер с помощью git
Должно получиться вот так. Я скопировал проект на сервер с помощью git

Переходим в консоль и в нашу папку с данным проектом: cd ../home/Название_проекта

Работа с Docker

Далее билдим проект на нашем VPS. Вводим следующую команду находясь в папке проекта: docker compose build. При первом запуске лично у меня возникла ошибка: failed to solve: process "/bin/sh -c apk add --virtual .build-deps gcc python3-dev musl-dev postgresql-dev" did not complete successfully: exit code: 4. Скорее всего, какая-то проблема внутри сервера при запросе доступа к пакетам. При повторном запуске команды все начало работать.

Сбилженный проект Django
Сбилженный проект Django

Получение SSL

Теперь нам необходимо получить SSL, я уже попробовал получить SSL на IP Адрес, к сожалению, Certbot выдает ошибку: Requested name 81.200.151.210 is an IP address. The Let's Encrypt certificate authority will not issue certificates for a bare IP address.. Поэтому придется использовать домен. В принципе, на деплое без домена не может быть.

Далее запускаем Docker с командой: docker compose up nginx certbot, подтянутся зависимые другие контейнеры. Ничего. Важно получить подобное сообщение:

Сообщения от Nginx и Certbot
Сообщения от Nginx и Certbot
Сообщения от Nginx и Certbot
Сообщения от Nginx и Certbot

Поздравляю, сертификаты успешно получены на доменное имя. Successfully received certificate.

Сохраненные данные сертификата
Сохраненные данные сертификата

Теперь останавливаем наш контейнер: docker compose stop.

Останавливаем контейнеры
Останавливаем контейнеры

Закомментируем команду certonly --webroot --webroot-path=/var/www/certbot/ --email email@mail.com --agree-tos --no-eff-email -d example.com -d www.example.com.

Закомментируем, чтобы команда не запускалась постоянно
Закомментируем, чтобы команда не запускалась постоянно

Теперь можно раскомментировать конфигурацию nginx: django.conf

docker/nginx/prod/django.conf
upstream django {
    server django:8000;
}

server {
    listen 80;
    listen [::]:80;

    server_name доменное_имя;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 default_server ssl http2;
    listen [::]:443 ssl http2;

    server_name доменное_имя;
    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/ваше_доменное_имя/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ваше_доменное_имя/privkey.pem;

    client_max_body_size 20M;
    charset utf-8;

    gzip  on;
    gzip_disable "msie6";
    gzip_min_length 1000;
    gzip_vary on;
    gzip_proxied   expired no-cache no-store private auth;
    gzip_types     text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;

    location / {
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Url-Scheme $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://django;
    }

    location /static/ {
        alias  /app/static/;
        expires 15d;
    }

     location /media/ {
        alias  /app/media/;
        expires 7d;
    }

    if ($http_host !~ "^доменное_имя$"){
	    rewrite ^(.*)$ https://доменное_имя$1 redirect;
	}
}

Эти обновленные файлы мы должны поместить на сервер.

Запускаем Django в Docker

Наконец мы можем запустить наш Django, введя команду: docker compose up.

Запуск контейнеров
Запуск контейнеров
Запуск контейнеров
Запуск контейнеров
Запуск контейнеров
Запуск контейнеров

Проверяем работу сайта

Перейдя на свое доменное имя, в моем случае это flowmods.net, я могу увидеть свой сайт

Рабочий SSL и наш рабочий Django проект
Рабочий SSL и наш рабочий Django проект

Если есть желание, вы можете восстановить данные из дампа, если его сохраняли. Подробнее в этом уроке, пункт: Сохранение данных для переноса БД из SQLITE в PostgreSQL в Django.

Я создам суперпользователя и попробую протестировать celery и добавить статью. Войти в консоль Django в Docker можно с помощью следующей команды: docker exec -it django sh, а после команда python manage.py createsuperuser.

Процесс создания
Процесс создания

Войдем под админом.

Админка
Админка

Добавлю тестовую статью.

Тестовая статья
Тестовая статья

Не забываем изменить example.com на свой домен.

Меняем example.com на свой домен
Меняем example.com на свой домен

Регистрируем аккаунт.

Регистрация аккаунта
Регистрация аккаунта

Получаем письмо подтверждения.

Письмо подтверждения
Письмо подтверждения
Подтверждаем аккаунта
Подтверждаем аккаунта

Отлично, надеюсь у Вас всё получилось! И эти знания пошли Вам на пользу!

Все вопросы можете задавать на нашем telegram канале: @ProgHub

;