Оптимизация среды разработки Django с помощью контейнеров Docker

Оптимизация среды разработки Django с помощью контейнеров Docker

28 марта 2023 г.

В этом руководстве я покажу, как создать общую и воспроизводимую среду разработки, которую может использовать вся ваша команда при работе над приложение Django.

Мы будем использовать Docker для контейнеризации не только вашего приложения Django, но и базы данных PostgreSQL, экземпляров Celery и кэш-сервера Redis. При этом мы будем использовать передовой опыт, чтобы повысить продуктивность разработчиков и упростить обслуживание конфигурации.

Настройка Docker

В этой статье предполагается, что мы используем Ubuntu 22, но не беспокойтесь, если у вас Windows или Mac. Инструкции по установке для всех платформ можно найти на веб-сайте Docker.

Чтобы приступить к установке Docker для Ubuntu, сначала нужно убедиться, что в apt есть пакеты, необходимые для связи с репозиториями по HTTP.

sudo apt-get update
sudo apt-get install 
    ca-certificates 
    curl 
    gnupg 
    lsb-release

Далее нам потребуется официальный ключ GPG от Docker для проверки установки.

sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Теперь мы добавим Docker в качестве источника репозитория.

echo 
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Мы должны иметь возможность выпустить apt update, чтобы начать работу с новым репозиторием, а затем установить Docker.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

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

sudo docker run hello-world

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

Мы также продолжим и добавим:

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker

Контейнеризация Django

В качестве первого шага мы сосредоточимся на написании Dockerfile для контейнера Django. Файлы Docker — это, по сути, рецепт для создания среды, в которой есть именно то, что требуется для приложения, не больше и не меньше.

Давайте взглянем на файл Dockerfile для Django, который мы будем использовать:

FROM python:3.11-alpine

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

COPY . .

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

Чтобы понять, что здесь происходит, начнем с первой строки.

Оператор FROM определяет образ, от которого мы наследуем или из которого строим. В этом случае мы начинаем с образа, созданного для предоставления python3.11. alpine означает, что образ очень простой (т. е. в нем не будет много распространенных программ, таких как bash) и поэтому занимает гораздо меньше места на диске, чем некоторые другие образы. Это может быть важно, например, если вы проводите тесты на такой платформе, как GitHub, где образ будет часто загружаться.

Команда WORKDIR устанавливает текущий каталог, а также создает этот каталог, если он еще не существует. Следовательно, команда COPY в следующей строке относится к каталогу /app, который мы установили чуть выше.

Команда COPY копирует файл requirements.txt с хост-компьютера в образ Docker. Как только это будет доступно, мы запустим pip install, чтобы убедиться, что все зависимости доступны.

Наконец, мы запускаем еще одну команду COPY, чтобы скопировать весь код из текущего каталога в образ.

На этом этапе вы можете задаться вопросом, почему мы скопировали файл requirements.txt, когда мы просто собираемся скопировать все несколько шагами позже.

Почему бы не написать Dockerfile так?

FROM python:3.11-alpine

WORKDIR /app

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

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

Чтобы быть уверенным, это сработает просто отлично!

Проблема заключается в том, как Docker перестраивает образы. Для большей эффективности Docker кэширует изображения по слоям.

Каждая команда, которую мы запускаем, создает новый слой. При перестроении Docker будет повторно использовать слои кеша, когда это возможно. Однако приведенный выше макет Dockerfile потребует повторного запуска pip install при касании любого кода. Скопировав файл requirements.txt и запустив pip install над последним COPY, Docker сможет кэшировать зависимости как отдельный слой, а это означает, что мы можем изменять код приложения, не запуская трудоемкий pip install процесс.

При использовании команды COPY есть несколько подводных камней, на которые следует обратить внимание. Хотя мы хотим скопировать весь код, мы не хотим тянуть ненужные файлы и увеличивать размер конечного изображения. Помимо потери дискового пространства и увеличения времени сборки, это также может привести к частым перестроениям, если загружаются постоянно изменяющиеся файлы. К счастью, именно в этом помогает файл .dockerignore.

Синтаксис файла .dockerignore совпадает с .gitignore, если вы написали один из них. Любой файл, соответствующий шаблону в .dockerignore, будет проигнорирован, поэтому мы можем использовать COPY, не опасаясь перетаскивания ненужных файлов.

Вот пример того, как мы можем предотвратить попадание скомпилированного байт-кода Python внутрь изображения. Мы также проигнорируем каталог .git, так как он нам определенно не нужен внутри образа.

*.pyc
__pycache__
.git

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

Вы можете создать изображение, как показано ниже, используя аргумент -t или тег, чтобы дать изображению имя.

docker build -t myawesomeapp .

Вы можете запустить контейнер Docker и открыть порт 8000, чтобы протестировать его после завершения сборки образа.

docker run -p 8000:8000 myawesomeapp

Когда контейнер запущен и работает, страница приветствия Django должна отображаться при посещении http://localhost:8000 в браузере.

Создание простого файла Compose

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

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

Мы начнем с базового docker-compose.yml, который содержит только службу приложения.

services:
  app:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    restart: unless-stopped
    volumes:
      - $PWD:/app
    ports:                                                 
      - 8000:8000    

В этом примере служба называется app, но она может называться как угодно, если используется допустимый синтаксис YAML.

Ключ build указывает, что мы хотим, чтобы docker-compose запускал перестроение образа Docker при изменении зависимостей. Это полезно, так как нам не придется вручную перестраивать образ при внесении изменений, например добавлении нового модуля в файл requirements.txt. Клавиша command переопределит любой CMD, указанный в Dockerfile. Нам не нужно указывать command, если CMD был указан в Dockerfile, но я хотел бы включить его здесь для ясности.

Перезапуск определяет, что мы хотели бы сделать, если контейнер остановится. По умолчанию остановленный контейнер не перезапускается. Здесь мы unless-stopped, чтобы объявить, что мы хотим, чтобы контейнер перезапустился, если он не был остановлен вручную. Самое главное, это означает, что наши службы будут перезапускаться после перезагрузки компьютера, что, вероятно, и требуется в среде разработки.

Тома предоставляют контейнеру доступ к каталогам хоста. Левая часть назначения тома сопоставляется с хостом, а правая определяет, где том должен появиться в контейнере.

$PWD — это просто сокращение для каталога, в котором запущен docker-compose, поэтому нам не нужно жестко кодировать этот путь, который, вероятно, отличается для каждого разработчика в команде.

Но какая польза от томов? Сопоставление кода с контейнером очень распространено в настройках разработки. В противном случае код зависает во времени на момент создания образа.

Без томов нам пришлось бы перестраивать образ каждый раз, когда мы меняли код.

Наконец, у нас есть ключ port. Как и при запуске контейнера вручную, мы указываем, что порт 8000 на узле должен перенаправлять трафик на порт 8000 внутри контейнера.

Создание и запуск службы приложений

С нашим первым файлом docker-compose.yml мы должны быть готовы запустить приложение.

Запустите команду docker-compose up в корневом каталоге проекта Django, чтобы запустить контейнер приложения.

Поскольку это первый запуск приложения, Compose создаст образ перед запуском приложения. После того, как образ создан, вы должны оставить приложение работающим и доступным через порт 8000. Я предпочитаю запускать Compose в фоновом режиме, что можно сделать с помощью команды docker-compose up -d. р>

Вы заметите, что если вы добавите новое приложение в проект Django и включите представление «hello world», представление будет автоматически доступно без необходимости перестраивать контейнер. Это связано с объемом, который мы определили в файле docker-compose.yml.

С другой стороны, если мы добавим djangorestframework в файл requirements.txt, он будет недоступен, пока мы не перестроим его. Это связано с тем, что, хотя наш код отображается в контейнер, каталоги, в которых Python хранит сторонний код, — нет. Мы можем запустить команду docker-compose build, чтобы перестроить образ с учетом новых дополнений в файле requirements.txt.

Это, однако, оставит старый контейнер работающим, если мы запустили Compose в фоновом режиме. Чтобы совместить создание и запуск контейнера, вы можете использовать команду docker-compose up --build.

Добавление службы базы данных

Теперь мы добавим экземпляр PostgreSQL в файл docker-compose.yml. Мы хотели бы, чтобы данные сохранялись, чтобы они сохранялись после удаления контейнера, а также нам нужно, чтобы сервер приложений запускался после инициализации базы данных.

Давайте посмотрим на обновленный файл.

services:
   app:
     build: .
     command: python manage.py runserver 0.0.0.0:8000
     restart: unless-stopped
     depends_on:
       - database
     volumes:
       - $PWD:/app
     env_file:
       - app.env
     ports:                                                 
       - 8000:8000 

   database:
     image: postgres:13
     restart: unless-stopped
     environment:
       - POSTGRES_USER=postgres
       - POSTGRES_PASSWORD=example_password
       - POSTGRES_DB=example_db
     volumes:
       - ./postgres-data:/var/lib/postgresql/data
     ports:
       - 5432:5432

Здесь есть несколько новых понятий. Во-первых, мы контролируем, какие переменные среды будут существовать внутри контейнеров с помощью ключей env_file и environment. Эти две функции очень похожи, но env_file извлекает значения из отдельного файла. Это удобно, когда у вас есть несколько служб, каждая из которых использует перекрывающийся набор переменных.

Вот что должен содержать указанный выше файл app.env на данный момент:

POSTGRES_USER=postgres
POSTGRES_PASSWORD=example_password
POSTGRES_HOST=database
POSTGRES_PORT=5432
POSTGRES_DB=example_db

Здесь следует отметить одну интересную вещь: для POSTGRES_HOST установлено значение database, чтобы оно соответствовало имени службы. Службы автоматически доступны из одного контейнера в другой, при этом имя каждой службы добавляется в качестве записи DNS.

Теперь у нас должна быть возможность изменить файл Django settings.py для подключения к базе данных.

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["POSTGRES_DB"],
        "USER": os.environ["POSTGRES_USER"],
        "PASSWORD": os.environ["POSTGRES_PASSWORD"],
        "HOST": os.environ["POSTGRES_HOST"],
        "PORT": os.environ["POSTGRES_PORT"],
    }
}

Предполагая, что вы снова запустили docker-compose up -d для создания базы данных, должна быть возможность запустить миграцию базы данных Django для заполнения исходной схемы.

docker exec -it tutorial_app_1 python manage.py migrate

Имя вашего контейнера приложения может отличаться, поэтому вам следует сначала запустить docker ps, чтобы узнать, какое имя вам нужно использовать.

Добавление службы Caddy

Обычно не рекомендуется запускать Django с сервером разработки, обращенным к Интернету, поэтому мы будем использовать Caddy для обслуживания статических файлов и передачи запросов в Django. В качестве бонуса веб-сервер Caddy выполнит настройку SSL, взаимодействуя с LetsEncrypt от нашего имени.

Давайте взглянем на обновленный docker-compose.yml и посмотрим, что изменилось.

services:
   app:
     build: .
     command: gunicorn tutorial.wsgi:application -w 4 -b 0.0.0.0:8000
     restart: unless-stopped
     depends_on:
       - database
     volumes:
       - $PWD:/app
     env_file:
       - app.env
     ports:                                                 
       - 8000:8000    

  caddy:
    image: caddy:2.4.5-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy_data:/data
      - ./caddy_config:/config
      - ./static:/static

   database:
     image: postgres:13
     restart: unless-stopped
     environment:
       - POSTGRES_USER=postgres
       - POSTGRES_PASSWORD=example_password
       - POSTGRES_DB=example_db
     volumes:
       - ./postgres-data:/var/lib/postgresql/data
     ports:
       - 5432:5432

Служба приложений теперь использует Gunicorn вместо встроенного сервера разработки Django. Сервер Caddy имеет тома для своего конфигурационного файла и для доступа к каталогу, в который мы будем помещать статические файлы. Caddy будет использовать тома caddy_data и caddy_config для хранения части своего внутреннего состояния, а это значит, что мы можем позволить себе потерять контейнер, но при необходимости воссоздать его без проблем. .

Чтобы переключиться на использование Gunicorn, вам нужно добавить gunicorn в файл requirements.txt и выполнить команду docker-compose build.

Теперь давайте рассмотрим конфигурацию Caddyfile.

localhost {
  handle_path /static/* {
    root * /static
    file_server
  }

  @app {
    not path /static/*
  }

  reverse_proxy @app {
    to app:8000
  }
}

Эта конфигурация передает запросы на все URL-адреса, кроме тех, которые начинаются с /static, в службу приложений. Если вы помните, мы используем здесь имя app, потому что оно совпадает с именем службы в файле docker-compose.yml. Caddy обрабатывает все запросы к /static с помощью директивы file_server, поэтому Django никогда не участвует в обслуживании статических файлов.

Когда эти обновления будут установлены, вы сможете запустить docker-compose up -d, чтобы запустить Caddy.

Однако, если вы попали на локальный хост в своем браузере, вы заметите, что самозаверяющий сертификат не является доверенным. Обычно Caddy добавляет свои собственные сертификаты в системное хранилище доверенных сертификатов. Однако, поскольку мы запускаем Caddy внутри контейнера, он не может выполнить это обновление. Однако при желании вы можете скопировать корневой сертификат из контейнера, чтобы избежать предупреждений браузера.

sudo cp ./caddy_data/caddy/pki/authorities/local/root.crt ./

Например, для Firefox вы можете перейти в «Настройки», «Конфиденциальность и безопасность». Безопасность и щелкните Просмотр сертификатов. На вкладке «Полномочия» вы должны увидеть кнопку «Импорт». Импортируйте файл root.crt, который мы скопировали из каталога caddy_data. После этого посещение локального хоста больше не должно вызывать предупреждение SSL.

Добавление экземпляров сельдерея

Теперь мы углубимся в добавление экземпляров Celery, чтобы мы могли выполнять некоторые фоновые задания в нашем новом проекте.

Celery требует наличия брокера и серверной части результатов и поддерживает несколько направлений для обоих. Здесь популярен Redis, и для простоты мы будем использовать его в качестве брокера и серверной части результатов. Добавить Redis в существующий docker-compose.yml несложно. Я покажу это ниже, опуская то, что мы уже написали, поскольку это не изменится.

  redis:
    image: 'redis:7.0'
    restart: unless-stopped
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 6379:6379

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

После добавления redis в среду нам потребуется обновить файл app.env, чтобы предоставить приложениям и рабочим контейнерам достаточно информации для подключения. Добавьте следующее в app.env, чтобы сделать переменные среды доступными для контейнеров.

CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

Обычно при создании приложения, использующего Celery, мне нравится разделять задания на разные типы, которые назначаются своим собственным очередям. Это дает ряд преимуществ, таких как локализация проблем с одним типом задания в одной очереди, вместо того, чтобы вызывать проблемы во всем приложении.

Наличие нескольких очередей требует, чтобы мы запускали контейнер для каждой очереди. Обычно это приводит к дублированию, поскольку нам приходится переопределять большую часть одной и той же конфигурации снова и снова. К счастью, мы можем использовать шаблоны YAML, чтобы избежать этой проблемы.

Вот обновленный файл docker-compose.yml с двумя экземплярами Celery.

x-worker-opts: &worker-opts
  build: .
  restart: unless-stopped
  volumes:
    - $PWD:/app
  env_file:
    - app.env
  depends_on:
    - redis

services:
  tutorial-worker1:
    command: tools/start_celery.sh -Q queue1 --concurrency=1
    <<: *worker-opts

  tutorial-worker2:
    command: tools/start_celery.sh -Q queue2 --concurrency=1
    <<: *worker-opts

Сценарий оболочки start_celery.sh содержит довольно длинную полную команду для запуска Celery. Команда заключена в watchmedo, который перезагружает экземпляр Celery каждый раз, когда файл Python изменяется в каталоге проекта.

#!/bin/sh
watchmedo auto-restart --directory=./ --pattern=*.py --recursive -- celery -A tutorial worker "$@" --loglevel=info

Синтаксис bash $@ заменяет аргументы сценария оболочки на эту позицию. В данном случае это означает вставку аргумента очереди в команду.

Дальнейшие шаги

Конечно, многое еще предстоит сделать, когда речь заходит о Docker, например, об адаптации этой настройки для работы с конвейером CI/CD. К счастью, в Docker и Compose есть функции, которые упрощают запуск одного и того же приложения в разных средах.

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

Удачного кодирования!


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE