Всем привет, меня зовут Аббакумов Валерий.
Я Python разработчик, в основном занимаюсь бэкэндом веб приложений и каждый раз когда дело доходит до разворачивания нового проекта по моей щеке начинает течь слеза.
Думаю, никто не будет спорить с тем, что локальное разворачивание нового проекта может вызвать неограниченное количество проблем. В данной статье я хочу представить выстраданную годами структуру проекта и организацию его окружения, которые помогают избежать большей части проблем, связанных с локальным разворачиванием проекта.
Пример будет представлен для Django проекта и PDM в качестве менеджера зависимостей, но концептуально должен подходить для любого проекта на любом языке и с любым набором сервисов. Так же у меня есть наработки для докерфайла с использованием Poetry, если это кому-то интересно, могу добавить информацию и для этого менеджера зависимостей.
Что будет в этой статье?
Структура проекта. Как разложить все по полкам
Управление переменными окружения. Как, где и почему лежат переменные
Создание локальных файлов. Основной скрипт подготовки окружения для работы
Docker. Описание docker-compose и Dockerfile файлов
Запуск проекта. Основные команды
Прочие файлы. Примеры некоторых файлов проекта, которые зачастую проблемно составить быстро и сразу
Заключение. Краткий вывод и пожелания
Структура проекта
Очень часто можно наблюдать как корень проекта представляется своего рода свалкой без какой либо структуры, что в свою очередь значительно усложняет понимание того, как проект организован и как его запускать. Очень хорошо если присутствует хотя бы детально описанный README.md и еще лучше, если он постоянно актуализируется, но зачастую приходится спрашивать у своих коллег что вообще происходит в из репозиториях.
Очень важно группировать компоненты по их назначению, это автоматически формирует интуитивно понятную структуру. За 5 лет разработки я пришел к следующей минимальной структуре приложения, которая не перегружает корень проекта, является интуитивно понятной и достаточно гибкой для использования и расширения.
Важно отметить, что подобного рода структура наиболее эффективна именно для разработки на локальной машине, конечно же, для реального крупного приложения, работающего в production
среде нужно куда больше. Масштабировать проще и лучше с k8s
, про хранение секретов должна быть как минимум отдельная статья, а про CI-CD целый сборник статей. Но в любом случае, все описанное ниже никак не исключает хороший системный подход к разворачиванию и доставке кода на ваши сервера, но в огромной степени упрощает жизнь разработчиков, которые работают на своих тачках и вынуждены поднимать всю инфраструктуру локально.
├── .local_files # Локальные файлы проекта под gitignore
│ ├── asgi # Локальные файлы основного приложения
│ │ ├── log # Логи
│ │ │ └── ...
│ │ ├── media # Медиа файлы
│ │ │ └── ...
│ │ ├── static # Статика
│ │ │ └── ...
│ │ ├── tmp # Временные файлы контейнера приложения
│ │ │ └── ...
│ │ └── ...
│ ├── celery # Локальные файлы celery
│ │ ├── log # Логи
│ │ │ └── ...
│ │ └── ...
│ ├── nginx # Локальные файлы nginx
│ │ ├── certs # Сертификаты
│ │ │ └── ...
│ │ ├── conf # Конфигурация nginx
│ │ │ └── ...
│ │ ├── log # Логи
│ │ │ └── ...
│ │ └── ...
│ ├── postgres # Локальные файлы postgres
│ │ ├── backup # Директория с бэкапами
│ │ │ └── ...
│ │ ├── data # Данные бд (при удалении и перезапуске контейнера инициализируется новая бд)
│ │ │ └── ...
│ │ ├── log # Логи
│ │ │ └── ...
│ │ └── ...
│ ├── redis # Локальные файлы redis
│ │ ├── data # Данные redis
│ │ │ └── ...
│ │ ├── log # Логи
│ │ │ └── ...
│ │ └── ...
│ └── ...
├── docker # Директория для инструментов, и конфигурации docker
│ ├── composes # Опциональные расширения docker-compose файла
│ │ ├── docker-compose.dev.yml # DEV сборка приложения
│ │ ├── docker-compose.prod.yml # PROD сборка приложения
│ │ ├── docker-compose.docs.yml # Собирать документацию
│ │ ├── docker-compose.build-asgi-locally.yml # Локальная сборка приложения
│ │ ├── docker-compose.build-docs-locally.yml # Локальная сборка документации
│ │ ├── docker-compose.postgres.yml # Запускать postgres в docker
│ │ ├── docker-compose.redis.yml # Запускать redis в docker
│ │ ├── docker-compose.override.yml # Переопределение под gitignore
│ │ └── ...
│ ├── confs # Конфиги, которые прокидываются в volumes или dockerfiles
│ │ ├── nginx.dev.conf
│ │ ├── nginx.prod.conf
│ │ └── ...
│ ├── dockerfiles # Специфические докерфайлы
│ │ ├── asgi.dockerfile # Докерфайл приложения
│ │ ├── postgres.dockerfile # Докерфайл postgress
│ │ └── ...
│ ├── inits # Скрипты, выполняемые при первом запуске контейнера или сборке
│ │ ├── postgresql.init.sql
│ │ └── ...
│ └── ...
├── docs # Директория для документации, я использую mkdocs (Очень рекомендую, помогает делать красивые end to end странички для онбординга сотрудников в проект)
│ ├── index.md
│ └── ...
├── pipelines # пайплайны GitLab
│ └── ...
├── scripts # Скрипты для быстрого доступа к частоиспользуемым командам
│ ├── bash.sh
│ ├── dbshell.sh
│ ├── init.sh
│ ├── manage.sh
│ ├── shell.sh
│ └── ...
├── src # Код самого приложения
│ ├── apps # Приложения с бизнес-логикой
│ │ ├── __init__.py
│ │ └── ...
│ ├── config # Конфигурация django приложения
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── celery.py
│ │ ├── urls.py
│ │ └── ...
│ ├── contrib # Модули не привязанные к бизнес-логике
│ │ ├── __init__.py
│ │ └── ...
│ ├── plugins # Опциональные приложения с бизнес-логикой
│ │ ├── __init__.py
│ │ └── ...
│ ├── settings # Директория с настройками приложения
│ │ ├── __init__.py
│ │ ├── apps_settings.py # Настройки бизнес-логики
│ │ ├── base_settings.py # Настройки путей, режима работы, url
│ │ ├── deps_settings.py # Креды сторонних приложений
│ │ ├── django_settings.py # Специфические для django настройки
│ │ ├── local_settings.py # Локальные настройки под gitignore в большинстве случаев здесь импортятся все остальные настройки кроме
│ │ ├── local_settings.sample.py # Пример локальных настроек
│ │ ├── test_settings.py # Настройки для тестов
│ │ └── ...
│ ├── __init__.py # Тут я указываю версию проекта __version__ = "0.0.1"
│ ├── manage.py # Не забудте указать os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.local_settings")
│ └── ...
├── .dockerignore # Файлы, которые игнорирует docker
├── .env # Автоматически генерируемый файл переменных окружения (под gitignore)
├── .env.base # Базовые переменные окружения
├── .env.local # Локальные переменные окружения (под gitignore)
├── .env.local.sample # Пример локальных переменных окружения
├── .gitignore # Файлы, которые игнорирует git
├── .gitlab-ci.yml # Конфигурация пайплайнов gitlab
├── .pre-commit-config.yaml # Конфигурация прекоммита
├── .prepare.sh # Скрипт подготовки окружения (выполняетс в compose)
├── compose # Утилита для вызова команд docker compose в нашем окружении
├── docker-compose.yml # Основной docker-compose файл
├── mkdocs.yml # Конфигурация докментации
├── pyproject.toml # Файл метаданных проекта
├── pdm.lock # Лок PDM (Возможно вы используете poetry или что-то еще)
├── CHANGELOG.md # Файл истории изменения версий
├── README.md # Файл с описанием проекта и взаимодействия с ним
└── ...
-
local_files # Локальные файлы проекта
Локальные файлы проекта очень полезно хранить в отдельной директории на хосте под
gitignore
. Всегда можно получить доступ к старым логам, бэкапам, временным файлам, сертификатам, конфигурациям приложений и так далее, можно добавлять и удалять файлы сразу в контейнере оставаясь на хосте и не вызываяdocker exec
команды.
Кстати, если вы очень хотите видеть содержание директорий типа data для postgres, redis и прочих сервисов, которые вы добавите сами и смонтируете, не забудьте пошарить права для хоста, иначе без sudo доступ к каталогам вы не получите, а следовательно ваша IDE будет отображать пустые каталоги. -
docker # Директория для инструментов, и конфигурации docker
Все что касается организации
docker
следует также выносить в отдельную директорию, более того файлы в этой директории можно группировать по назначению, а именноcomposes
,dockerfiles
,inits
иconfs
, конечно это опционально и некоторые группы можно как убрать так и добавить, но не стоит перегружать директории разнородными файлами это усложняет понимание общей структуры -
docs # Директория для документации, я использую mkdocs
При установке mkdocstrings можно настроить автодокументирование вашей кодовой базы, ниже будет приведен пример настройки документации
-
pipelines # Пайплайны
GitLab
.Позволю себе не описывать настройку и всю боль, которую я получил от опыта настройки пайплайнов в
GitLab
, думаю ,это выходит за рамки данной статьи scripts # Скрипты для быстрого доступа к часто используемым командам.
Сюда лучше всего складывать все команды, которые так или иначе будут вызываться в командной строке в корне проекта, например вызовshell_plus
,dbshell
,manage.py
, дамп бд и так далее. Для того чтобы не перегружать корень проекта в репозитории скрипты сгруппированы в отдельной директории, а локально для удобства на каждый скрипт в директории создается символическая ссылка в корне проектаsrc # Код самого приложения
Код приложения вынесен в отдельную директорию для удобства создания образа без всего лишнего. А также для логического разграничения окружения проекта и самой кодовой базы-
.env* # Файлы с переменными окружения
Вы можете заметить, что файлы с данного типа не выделены в отдельную группу каталогом, а лежат в каталоге. На это есть 2 причины: во-первых, вынося переменные окружения в подкаталог теряется интуитивная ясность того, что скорее всего нужно что-то заполнить, во-вторых
.env
файл лежит там же, где и основнойdocker-compose.yml
файл, что позволяет не устраивать танцы с бубнами
Управление переменными окружения
В большинстве проектов в репозитории либо вообще нет никаких переменных окружения либо есть какой-то пример, который нужно значительно редактировать либо пример в котором не ясно, что нужно редактировать.
Зачастую мы имеем определенный набор переменных окружения, которые с большой долей вероятности не нужно будет изменять и которые в свою очередь не являются кредами. Данные переменные имеет смысл выносить в репозиторий отдельным файлом .env.base
# Основные настройки
# Префикс docker контейнеров
COMPOSE_PROJECT_NAME=<you_project>
# Файл настроек django
DJANGO_SETTINGS_MODULE=settings.local_settings
# Использовать debug режим
DEBUG=False
# Запущено в prod среде
PRODUCTION=False
# Количество запускаемых рабочих
WORKERS_COUNT=4
# Идентификация юзера
USER_NAME="$(echo $USER)"
USER_ID="$(id -u)"
GROUP_ID="$(id -g)"
# Образ приложения
# Путь до docker registry
DOCKER_REGISTRY=<you_project_docker_registry>
# Название образа
DOCKER_IMAGE=<you_project_docker_image>
# Тег образа
TAG=<you_project_docker_tag>
# Директории
# Корень проекта
PROJECT_DIR=$(cd $(dirname $(readlink -e $0)) && pwd)
# Директория с кодом приложения
SOURCE_DIR=${PROJECT_DIR}/src
# Директория docker
DOCKER_DIR=${PROJECT_DIR}/docker
# Директория с docker-compose файлами
DOCKER_COMPOSES_DIR=${DOCKER_DIR}/composes
# Директория с конфигурациями для различных серфисов
DOCKER_CONFS_DIR=${DOCKER_DIR}/confs
# Директория с Dockerfile
DOCKER_DOCKERFILES_DIR=${DOCKER_DIR}/dockerfiles
# Директория со скриптами инициализации
DOCKER_INITS_DIR=${DOCKER_DIR}/inits
# Директория с локальными файлами
LOCAL_FILES_DIR=${PROJECT_DIR}/.local_files
# Очень важно разделить ворты в сети докер и порты в сети хоста
# чтобы была возможность менять порты с любой стороны по мере необходимости
# Порты на хосте
ASGI_PORT_EXTERNAL=8000
POSTGRES_PORT_EXTERNAL=5432
REDIS_PORT_EXTERNAL=6379
NGINX_PORT_EXTERNAL=80
FLOWER_PORT_EXTERNAL=5555
DOCS_EXTERNAL_PORT=8001
# Порты в сети докера
ASGI_PORT_INTERNAL=8000
POSTGRES_PORT_INTERNAL=5432
REDIS_PORT_INTERNAL=6379
NGINX_PORT_INTERNAL=80
FLOWER_PORT_INTERNAL=5555
DOCS_PORT_INTERNAL=8001
# Настройки docker-compose
# Основной файл docker-compose
COMPOSE_FILE="${PROJECT_DIR}/docker-compose.yml"
# Запускать redis в докере
RUN_WITH_REDIS=True
# Запускать postgres в докере
RUN_WITH_POSTGRES=True
# Добавить docker-compose.override.yml
RUN_WITH_OVERRIDE_DOCKER_COMPOSE=True
# Собирать образ локаьно
RUN_WITH_BUILD_LOCALLY=False
# Запускать сервер с документацией
RUN_WITH_DOCS=False
# Настройки почты
MAIL_PORT=465
MAIL_SERVER=smtp.gmail.com
# Использовать S3
USE_S3_STORAGE=False
# Ключ для генерации токенов
SECRET_KEY=example_key
Помимо этого всегда есть необходимость определить или переопределить какие-то переменный локально. Для этого используем .env.local
, который генерируется на основании .env.local.sample
# Основные настройки
DEBUG=True
# Настройки админ юзера, обычно я их тяну в manage.py команде при инициализации
# проекта для автоматического создания юзера
ADMIN_USERNAME=admin
ADMIN_PASSWORD=password_example
# Собирать образ приложения локально (альтернатива стягиванию из регистри)
RUN_WITH_BUILD_LOCALLY=True
# Запустить сервер с документацией
RUN_WITH_DOCS=True
# Настройки postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password_example
POSTGRES_DB=postgres
POSTGRES_DEBUG=False
# Настройки redis
REDIS_HOST=redis
REDIS_PORT=6379
# Настройки почты
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=${MAIL_USERNAME}
MAIL_PORT=465
MAIL_SERVER=smtp.gmail.com
# Настройки sentry
SENTRY_DSN=
# Настройки S3
USE_S3_STORAGE=False
AWS_STORAGE_BUCKET_NAME=
AWS_S3_ACCESS_KEY_ID=
AWS_S3_SECRET_ACCESS_KEY=
AWS_S3_ENDPOINT_URL=
Далее переменные, объявленные в этих 2 файлах будут собраны в итоговый .env
файл для удобства использования
Создание локальных файлов
Приведенный ниже скрипт:
создает всю структуру локальных файлов проекта
-
создает локальные настройки, которые также находятся под
gitignore
:для переменных окружения
.env.local
для запуска контейнеров
docker-compose.override.yml
для приложения
local_settings.py
создает символические ссылки на часто используемые скрипты
создает общий
.env
файл, который используется для запуска приложениязаписывает в переменные окружения конфигурацию запуска docker compose
#!/bin/bash
# Функция для вывода переменных, определенных во время исполнения скрипта
print_script_variables() {
set | sort | comm -13 .env.tmp -
}
# Записываем переменные окружения и функции оболочки во временный файл
set | sort > .env.tmp
# Импортируем базовые переменные окружения
source ".env.base"
# Создаем локальные файлы их их шаблонов, если они еще не созданы
cp -n ${PROJECT_DIR}/.env.local.sample ${PROJECT_DIR}/.env.local
cp -n ${SOURCE_DIR}/settings/local_settings.sample.py ${SOURCE_DIR}/settings/local_settings.py
# Импортируем локальные переменные окружения
if [[ -f .env.local ]]; then
source ".env.local"
fi
# Создаем директорию с локальными фойлами
mkdir -p "${LOCAL_FILES_DIR}"
# Создаем символические ссылки на скрипты (сюда можно добавить создание и загрузку дампа бд)
ln -sf "${PROJECT_DIR}/scripts/bash.sh" "${PROJECT_DIR}/bash"
ln -sf "${PROJECT_DIR}/scripts/dbshell.sh" "${PROJECT_DIR}/dbshell"
ln -sf "${PROJECT_DIR}/scripts/manage.sh" "${PROJECT_DIR}/manage"
ln -sf "${PROJECT_DIR}/scripts/shell.sh" "${PROJECT_DIR}/shell"
ln -sf "${PROJECT_DIR}/scripts/init.sh" "${PROJECT_DIR}/init"
# Создание локальных файлов для postgres:
mkdir -p "${LOCAL_FILES_DIR}/postgres"
mkdir -p "${LOCAL_FILES_DIR}/postgres/log"
mkdir -p "${LOCAL_FILES_DIR}/postgres/data"
mkdir -p "${LOCAL_FILES_DIR}/postgres/backup"
# Создание локальных файлов для redis:
mkdir -p "${LOCAL_FILES_DIR}/redis"
mkdir -p "${LOCAL_FILES_DIR}/redis/log"
mkdir -p "${LOCAL_FILES_DIR}/redis/data"
# Создание локальных файлов для asgi:
mkdir -p "${LOCAL_FILES_DIR}/asgi"
mkdir -p "${LOCAL_FILES_DIR}/asgi/log"
mkdir -p "${LOCAL_FILES_DIR}/asgi/tmp"
mkdir -p "${LOCAL_FILES_DIR}/asgi/media"
# Создание локальных файлов для centry:
mkdir -p "${LOCAL_FILES_DIR}/celery"
mkdir -p "${LOCAL_FILES_DIR}/celery/log"
# Создание локальных файлов для nginx:
mkdir -p "${LOCAL_FILES_DIR}/nginx"
mkdir -p "${LOCAL_FILES_DIR}/nginx/conf"
mkdir -p "${LOCAL_FILES_DIR}/nginx/log"
mkdir -p "${LOCAL_FILES_DIR}/nginx/certs"
# Создания docker-compose.override.yml (под gitignore)
touch "${DOCKER_COMPOSES_DIR}/docker-compose.override.yml"
# Создание конфигов nginx из их шаблонов
export NGINX_PORT_INTERNAL=${NGINX_PORT_INTERNAL}
export ASGI_PORT_INTERNAL=${ASGI_PORT_INTERNAL}
export DOCS_PORT_INTERNAL=${DOCS_PORT_INTERNAL}
export HOST_NAME=${HOST_NAME}
# Следующие команды заполняют шаблон значениями переменных окружения
envsubst '${NGINX_PORT_INTERNAL},${ASGI_PORT_INTERNAL},${DOCS_PORT_INTERNAL},${HOST_NAME}' \
< "${DOCKER_CONFS_DIR}/nginx.dev.conf" > \
"${LOCAL_FILES_DIR}/nginx/conf/nginx.dev.conf"
envsubst '${NGINX_PORT_INTERNAL},${ASGI_PORT_INTERNAL},${DOCS_PORT_INTERNAL},${HOST_NAME}' \
< "${DOCKER_CONFS_DIR}/nginx.prod.conf" > \
"${LOCAL_FILES_DIR}/nginx/conf/nginx.prod.conf"
# Дополнение docker-compose.yaml в зависимости от значений переменных окружения
if [[ "${RUN_WITH_REDIS}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.redis.yml"
fi
if [[ "${RUN_WITH_POSTGRES}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.postgres.yml"
fi
if [[ "${RUN_WITH_OVERRIDE_DOCKER_COMPOSE}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.override.yml"
fi
if [[ "${RUN_WITH_DOCS}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.docs.yml"
fi
if [[ "${RUN_WITH_BUILD_LOCALLY}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.build-asgi-locally.yml"
if [[ "${RUN_WITH_DOCS}" = "True" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.build-docs-locally.yml"
fi
fi
if [[ "${PRODUCTION}" = "False" ]]; then
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.dev.yml"
else
COMPOSE_FILE="${COMPOSE_FILE}:${DOCKER_COMPOSES_DIR}/docker-compose.prod.yml"
fi
# Записываем в файл переменные окружения, объявленные в процессе выполнения текущего скрипта
EXCLUDE_RULES="^\(SHLVL=\|LINES=\|COLUMNS=\|BASH_SOURCE=\|BASH_LINENO=\|BASH_ARGV=\|BASH_ARGC=\|EXCLUDE_RULES=\|_=\|FUNCNAME=\|PIPESTATUS=\)"
print_script_variables | grep -v $EXCLUDE_RULES > ${PROJECT_DIR}/.env
# Удаляем временный файл
rm -f .env.tmp
Данный скрипт вызывается при каждом запуске compose
с помощью которого мы в взаимодействуем с приложением, подробнее будет описано ниже
Примечание:
Начало и конец скрипта, связанные с вычленением объявленных переменных конечно же является ужаснейшим костылем, но никакого более элегантного решения я самостоятельно найти не смог. Буду очень рад, если кто-то предложит красивую альтернативу, не требующую установки сторонних приложений, не присутствующих в стандартных оболочках
Docker
Мы будем использовать набор из нескольких docker-compose*.yaml
файлов а также много стадийный Dockerfile
Основные рекомендации по docker-compose
файлам:
Выделяйте общие части в переменные
Используйте абсолютные пути в
volumes
во избежание конфликтов с другими проектамиСтарайтесь по-максимуму использовать переменные окружения и по-минимуму хардкод
Не забывайте указывать зависимости и помните, что иногда вам придется добавлять кастомный скрипт для проверки доступности приложения в контейнере
Помните, что вы можете тонко настроить выделение ресурсов, сети, маски подсетей и так далее. А еще существует
docker swarm
если вы ну очень не хотите использоватьk8s
, в любом случае, читайте документацию
Основной файлdocker-compose.yaml
Здесь мы описываем сервисы, которые не являются опциональными либо те, что по логическим соображениям должны находиться вместе, тут все на ваше усмотрение, так как всегда есть возможность добавить любой сервис в отдельном файле
# Выделяем основную часть настроек приложения в переменную, чтобы не повторяться
x-asgi-base: &asgi-base
# Тянем образ из нашего docker-registry
image: ${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${TAG}
restart: on-failure
user: ${USER_ID}:${GROUP_ID}
# Прокидываем переменные окружения
env_file:
- .env
# Все наши сервисы для удобства будут в одной сети
networks:
- app-network
services:
# Сервис основного приложения
asgi:
# Вставляем часть документа, описанную выше
<<: *asgi-base
hostname: asgi
container_name: ${COMPOSE_PROJECT_NAME}-asgi
# Прокидываем порты. Делаем разделение настройки внешних и внутренних портов для того,
# чтобы можно было запускать несколько подобных приложений без необходимости изменять порты внутри докера
ports:
- "${ASGI_PORT_EXTERNAL}:${ASGI_PORT_INTERNAL}"
# Важно маунтить volumes по абсолютным путям так как с относительными могут возникнуть конфликты в нейминге
volumes:
- ${LOCAL_FILES_DIR}/asgi/log/:/var/log/asgi/
- ${LOCAL_FILES_DIR}/asgi/tmp/:/tmp/
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
# Сервис периодических задач
celery-beat:
# Аналогично asgi
<<: *asgi-base
hostname: celery-beat
container_name: ${COMPOSE_PROJECT_NAME}-celery-beat
command: bash -c '
celery -A config beat -l info --logfile=/var/log/celery/celery_beat.log -s celerybeat-schedule'
volumes:
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/celery/
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/asgi/
- ${LOCAL_FILES_DIR}/asgi/tmp/:/tmp/
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
# Сервис дефолтной очереди задач (конечно же можно /нужно увеличить количество очередей)
celery-default:
# Аналогично asgi
<<: *asgi-base
hostname: celery-default
container_name: ${COMPOSE_PROJECT_NAME}-celery-default
command: bash -c '
celery -A config worker -l info --concurrency=5 --logfile=/var/log/celery/celery_default.log -Q default'
volumes:
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/celery/
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/asgi/
- ${LOCAL_FILES_DIR}/asgi/tmp/:/tmp/
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
# Сервис мониторинга celery задач
flower:
# Аналогично asgi
<<: *asgi-base
hostname: flower
container_name: ${COMPOSE_PROJECT_NAME}-flower
command: bash -c 'celery -A config flower --address=0.0.0.0 --port=${FLOWER_PORT_INTERNAL}'
ports:
- "${FLOWER_PORT_EXTERNAL}:${FLOWER_PORT_INTERNAL}"
volumes:
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/celery/
- ${LOCAL_FILES_DIR}/celery/log/:/var/log/asgi/
- ${LOCAL_FILES_DIR}/asgi/tmp/:/tmp/
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
# HTTP-сервис
nginx:
image: nginx:1.25.3
hostname: nginx
container_name: ${COMPOSE_PROJECT_NAME}-nginx
restart: on-failure
ports:
- "${NGINX_PORT_EXTERNAL}:${NGINX_PORT_INTERNAL}"
volumes:
- ${LOCAL_FILES_DIR}/nginx/log/:/var/log/nginx/
- ${LOCAL_FILES_DIR}/asgi/tmp/:/tmp/
# Важно не забыть прокинуть статику и медиафайлы основного приложения
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
depends_on:
- asgi
networks:
- app-network
networks:
app-network:
name: ${COMPOSE_PROJECT_NAME}
Файл для разработки docker/composes/docker-compose.dev.yaml
Тут мы просто расширяем наш основной docker-compose.yaml
необходимыми для среды разработки элементами, а именно маунтим volumes
для возможности установки библиотек через менеджер зависимостей, а также маунтим код проекта для поддержки работы в режиме hot-reload
services:
# Прописываем команду для запуска dev сервера
asgi:
command: bash -c 'python manage.py runserver 0.0.0.0:${ASGI_PORT_INTERNAL}'
# Прокидываем код, pdm и pyproject.toml для возможности установки библиотек
volumes:
- ${SOURCE_DIR}/:/project/src/
- ${PROJECT_DIR}/pdm.lock:/project/pdm.lock
- ${PROJECT_DIR}/pyproject.toml:/project/pyproject.toml
celery-beat:
volumes:
- ${SOURCE_DIR}/:/project/src/
celery-default:
volumes:
- ${SOURCE_DIR}/:/project/src/
flower:
volumes:
- ${SOURCE_DIR}/:/project/src/
# Прокидыываем dev конфигурацию nginx
nginx:
volumes:
- ${LOCAL_FILES_DIR}/nginx/conf/nginx.dev.conf:/etc/nginx/conf.d/default.conf
Файл для прод среды docker/composes/docker-compose.prod.yaml
Понимаю, что "Для прод среды" - сильно громко сказано, но суть в том, что мы уже не хотим синхронизировать код на хосте и в контейнере и хотим иметь более чем одного запущенного воркера. В реальном проекте, конечно такой конфиг может быть в prod
окружении, но я бы все-таки порекомендовал обратиться к DevOps инженерам.
services:
# Прописываем команду для запуска gunicorn на uvicorn воркерах
asgi:
command: bash -c 'python -m gunicorn config.asgi:application --bind 0.0.0.0:8000 --worker-class uvicorn.workers.UvicornWorker --timeout 180 --workers $WORKERS_COUNT --log-level debug --access-logfile - --error-logfile - --pid /tmp/gunicorn.pid'
# Прокидыываем prod конфигурацию nginx
nginx:
volumes:
- ${LOCAL_FILES_DIR}/nginx/conf/nginx.prod.conf:/etc/nginx/conf.d/default.conf
Файл для запуска сервера документации docker/composes/docker-compose.docs.yml
По-хорошему этот сервис должен запускаться только во время локальной разработки. Mkdocs имеет возможность собрать dist проекта, что я рекомендую делать в пайплайнах и уже раздавать статику на ваших серверах
services:
# Сервис документации
docs:
image: ${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${TAG}
container_name: ${COMPOSE_PROJECT_NAME}-docs
env_file:
- ${PROJECT_DIR}/.env
environment:
- LOCAL_PYTEST=1
- ENVIRONMENT_TYPE=docker
volumes:
- ${PROJECT_DIR}:/src
- ${LOCAL_FILES_DIR}/asgi/static:/mnt/resource/static
- ${LOCAL_FILES_DIR}/asgi/media:/mnt/resource/media
- ${LOCAL_FILES_DIR}/asgi/tmp:/tmp
# Запускаем веб сервер документации
command: mkdocs serve --dev-addr 0.0.0.0:${DOCS_PORT_INTERNAL}
ports:
- "${DOCS_EXTERNAL_PORT}:${DOCS_PORT_INTERNAL}"
networks:
- app-network
Файл для redis
docker/composes/docker-compose.redis.yml
Наверное самая простая часть во всем нашем окружении, честно говоря, множество сервисов можно описать примерно так же и этого будет достаточно для большей части задач, но надеюсь, что в данной статье описано достаточно кейсов для того, чтобы вы не ограничивались минимальными возможностями настройки вашего окружения
services:
redis:
image: redis:7.4.0
container_name: ${COMPOSE_PROJECT_NAME}-redis
networks:
- app-network
ports:
- "${REDIS_PORT_EXTERNAL}:${REDIS_PORT_INTERNAL}"
# Добавляем redis в качестве зависимости
asgi:
depends_on:
- redis
celery-default:
depends_on:
- redis
celery-beat:
depends_on:
- redis
flower:
depends_on:
- redis
Файл для postgres
docker/composes/docker-compose.postgres.yml
Для postgres
использую кастомный Dockerfile
так как по непонятным мне причинам в дефолтном образе отсутствует утилита для создания дампа
services:
postgres:
# Собираем образ
build:
context: ${PROJECT_DIR}
dockerfile: ${DOCKER_DOCKERFILES_DIR}/postgres.dockerfile
container_name: ${COMPOSE_PROJECT_NAME}-postgres
command: ["postgres", "-c", "log_statement=all"]
shm_size: 1g
# Прокидываем скрипт инициализации и локлаьные файлы
volumes:
- ${DOCKER_INITS_DIR}/postgresql.init.sql:/docker-entrypoint-initdb.d/init.sql
- ${LOCAL_FILES_DIR}/postgres/data/:/var/lib/postgresql/data/
- ${LOCAL_FILES_DIR}/postgres/log/:/var/log/postgresql/
- ${LOCAL_FILES_DIR}/postgres/backup/:/backup/
# Прокидываем переменные для конфигурации базы
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "${POSTGRES_PORT_EXTERNAL}:${POSTGRES_PORT_INTERNAL}"
networks:
- app-network
# Добавляем postgres в качестве зависимости
asgi:
depends_on:
- postgres
links:
- postgres:postgres
celery-default:
depends_on:
- postgres
links:
- postgres:postgres
celery-beat:
depends_on:
- postgres
links:
- postgres:postgres
flower:
depends_on:
- postgres
links:
- postgres:postgres
Файл для локальной сборки приложения docker/composes/docker-compose.build-asgi-locally.yml
Данный файл необходим, когда вы не можете / не хотите / вынуждены собрать образ приложения локально, например вы добавляете библиотеку
x-asgi-local-build: &asgi-local-build
image: app:latest
# Собираем образ из нужного слоя докерфайла
build:
context: ${PROJECT_DIR}
dockerfile: ${DOCKER_DOCKERFILES_DIR}/asgi.dockerfile
target: development
args:
- USER_NAME=${USER_NAME}
- USER_ID=${USER_ID}
- GROUP_ID=${GROUP_ID}
# Обновляем сборку образа в сервисах
services:
asgi:
<<: *asgi-local-build
celery-beat:
<<: *asgi-local-build
celery-default:
<<: *asgi-local-build
flower:
<<: *asgi-local-build
Файл для локальной сборки документации docker/composes/docker-compose.build-docs-locally.yml
Этот файл является своего рода аппендиксом, так как я не придумал, как иначе разрешить кейс, когда, мне нужно локально собрать образ приложения, но не нужно запускать сервис документации
services:
docs:
image: app:latest
build:
context: ${PROJECT_DIR}
dockerfile: ${DOCKER_DOCKERFILES_DIR}/asgi.dockerfile
target: development
args:
- USER_NAME=${USER_NAME}
- USER_ID=${USER_ID}
- GROUP_ID=${GROUP_ID}
Файл сборки postgresdocker/dockerfiles/postgres.dockerfile
FROM postgres:16.4-bullseye
# Устанавливаем утилиту для возможности создания дампа
RUN apt-get -y update
RUN apt-get -y install postgresql-16-repack
Файл сборки приложенияdocker/dockerfiles/postgres.dockerfile
На самом деле ниже описана квинтэссенция моих познаний и обрывков организации Dockerfile
которые я успел увидеть за всю мою практику программирования. Еще 2 редакции этого файла назад я думал, что в нем уже нечего исправлять, но как вы, наверное, догадались, с тех пор файл претерпел значительные изменения, потому, буду безмерно рад любым советам по улучшению данного файла
# ---STAGE_1---
# Описываем стадию базового python образа
# Указываем платформу сборки через переменную окружения
FROM --platform=$BUILDPLATFORM python:3.12.6-slim AS python-base
# Определяем аргументы, которые можно будет передать как аргументы сборки
ARG USER_ID=1000
ARG GROUP_ID=1000
ARG USER_NAME="user"
ARG GROUP_NAME="user"
# Описываем базовое окружение для python
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=10 \
PDM_REQUEST_TIMEOUT=10 \
PDM_VERSION=2.19.1 \
PDM_CACHE_DIR="/opt/.cache/pdm" \
PDM_LOG_DIR="/opt/.local/pdm" \
PDM_CHECK_UPDATE=false \
PDM_HOME="/opt/pdm" \
PROJECT_PATH="/project" \
APPLICATION_PATH="/project/src" \
VENV_PATH="/project/.venv" \
TZ="Europe/Moscow" \
LANG="en_US.UTF-8" \
LC_ALL="en_US.UTF-8"
# Добавляем PDM_HOME в PATH
ENV PATH="$PDM_HOME/bin:$VENV_PATH/bin:$PATH"
# ---STAGE-2---
# Описываем стадию сборки pdm
FROM python-base AS base-pdm-builder
# Устанавливаем все, необходимые зависимости для сборки
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
DEBCONF_NONINTERACTIVE_SEEN=true DEBIAN_FRONTEND=noninteractive apt-get update \
&& apt-get install -y -qq --no-install-recommends \
build-essential \
ca-certificates \
gcc \
git \
curl \
pkg-config \
libcairo2-dev \
libjpeg-dev \
libgif-dev \
python3-venv \
&& curl -sSL https://pdm-project.org/install-pdm.py | python3 - \
# отчищаем кэш для уменьшения размера образа
&& apt-get clean \
&& rm -rf /tmp/* /var/lib/apt/lists/*
# Указвыаем рабочую директорию
WORKDIR $PROJECT_PATH
# Прокидываем файлы, необходимые для корректной работы pdm
COPY --link ./pdm.lock ./pyproject.toml ./README.md ./
COPY --link ./src/__init__.py $APPLICATION_PATH/__init__.py
# ---STAGE-3---
# Стадия сборки зависимостей приложения для dev среды
FROM base-pdm-builder AS development-pdm-builder
RUN pdm install --check --dev
# ---STAGE-4---
# Стадия сборки зависимостей приложения для test среды
FROM base-pdm-builder AS test-pdm-builder
RUN pdm install --check -G test --no-self
# ---STAGE-5---
# Стадия сборки зависимостей приложения для prod среды
FROM base-pdm-builder AS production-pdm-builder
RUN pdm install --check --production --no-self
# ---STAGE-6---
# Базовая стадия сборки зависимостей для python образа
FROM python-base AS base-project-builder
ARG USER_ID
ARG GROUP_ID
WORKDIR $APPLICATION_PATH
# Устанавливаем все, необходимые зависимости для работы приложения
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
DEBCONF_NONINTERACTIVE_SEEN=true DEBIAN_FRONTEND=noninteractive apt-get update \
&& apt-get install -y -qq --no-install-recommends \
locales \
libmagic1 \
pkg-config \
libcairo2-dev \
libjpeg-dev \
libgif-dev \
# Настраиваем язык
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& dpkg-reconfigure --frontend=noninteractive locales \
&& update-locale LANG=en_US.UTF-8 \
# Настраиваем пользователя и группу
&& addgroup --gid $GROUP_ID $GROUP_NAME \
&& adduser -q --gecos '' --disabled-password --ingroup $GROUP_NAME --shell /bin/bash --uid $USER_ID $USER_NAME \
# Отчищаем кэш
&& apt-get clean \
&& rm -rf /tmp/* /var/lib/apt/lists/* \
# Создаем необходимые директории и файлы
&& mkdir -p /mnt/resource/static \
&& mkdir -p /mnt/resource/media \
&& mkdir -p $APPLICATION_PATH \
&& touch $APPLICATION_PATH/celerybeat-schedule \
# Настраиваем права к директориям
&& chown -R $USER_ID:$GROUP_ID /tmp/ \
&& chown -R $USER_ID:$GROUP_ID /mnt/ \
&& chown -R $USER_ID:$GROUP_ID /opt/ \
&& chown -R $USER_ID:$GROUP_ID /project/ \
&& chown -R $USER_ID:$GROUP_ID /var/log/
# ---STAGE-7---
# Стадия сборки приложения для dev среды
FROM base-project-builder AS development
# Копируем необходимые данные из предыдущих стадий
COPY --link --chown=$USER_ID:$GROUP_ID --from=development-pdm-builder $PDM_HOME $PDM_HOME
COPY --link --chown=$USER_ID:$GROUP_ID --from=development-pdm-builder $PROJECT_PATH $PROJECT_PATH
# Копируем код приложения с хоста
COPY --link ./src $APPLICATION_PATH
# Копируем конфигурацию и наполнение документации
COPY --link ./docs $APPLICATION_PATH/docs
COPY --link ./mkdocs.yml $APPLICATION_PATH/mkdocs.yml
# Создвем символическую ссылку на pyproject.toml для корректной работы PDM и удаляем не нужные в образе файлы
RUN ln -s $PROJECT_PATH/pyproject.toml $APPLICATION_PATH/pyproject.toml \
&& find . -name "*.pyc" -delete
# Меняем пользователя
USER $USER_NAME
# ---STAGE-8---
# Стадия сборки приложения для test среды
FROM base-project-builder AS test
# Копируем необходимые данные из предыдущих стадий
COPY --link --from=test-pdm-builder $PDM_HOME $PDM_HOME
COPY --link --from=test-pdm-builder $PROJECT_PATH $PROJECT_PATH
# Копируем код приложения с хоста
COPY --link ./src $APPLICATION_PATH
RUN chown $USER_ID:$GROUP_ID $PROJECT_PATH/pyproject.toml \
&& ln -s $PROJECT_PATH/pyproject.toml $APPLICATION_PATH/pyproject.toml \
&& find . -name "*.pyc" -delete
# Меняем пользователя
USER $USER_NAME
# ---STAGE-9---
# Стадия сборки приложения для prod среды
FROM base-project-builder AS production
# Копируем необходимые данные из предыдущих стадий
COPY --link --from=production-pdm-builder $PDM_HOME $PDM_HOME
COPY --link --from=production-pdm-builder $PROJECT_PATH $PROJECT_PATH
# Копируем код приложения с хоста
COPY --link ./src $APPLICATION_PATH
# Меняем пользователя
USER $USER_NAME
Запуск проекта
Зависимости:
python version - 3.12.6
docker - 27.2.1
Точка входа
Запуск проекта осуществляется с помощью скрипта compose
По факту он просто подготавливает окружение, подтягивает переменные среды и прокидывает передаваемые аргументы в docker compose
.
Если вы используете более раннюю версию docker
вам нужно использовать команду docker-compose
вместо docker compose
#!/bin/bash
bash .prepare.sh
source ".env"
ARGS_STRING=""
while [ $1 ]
do
ARGS_STRING+="$1 "
shift
done
docker compose ${ARGS_STRING}
Далее все как обычно
./compose up -d --build
./compose restart
./compose down
./compose exec -it ...
...
Управление зависимостями
Для управления зависимостями используется pdm v2.19.1
Чтобы добавить зависимость используйте команду
./compose exec -it asgi pdm add <dependency>
Чтобы добавить в группу зависимость используйте команду
./compose exec -it asgi pdm add -dG <group> <dependency>
Чтобы удалить зависимость используйте команду
./compose exec -it asgi pdm remove <dependency>
Прочие файлы
В данном разделе представлены файлы из моего каркаса приложения, которые также могут быть интересны в формате текущей статьи но не несут за собой фундаментальной значимости для освещаемой темы
Пример конфигурации pyproject.toml
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "YOUR_PROJECT"
dynamic = ["version"]
description = "Основное приложение для YOUR_PROJECT"
readme = "README.md"
requires-python = ">=3.12"
authors = [
{ name = "Your Name", email = "your@email.ru"},
]
maintainers = [
{ name = "Your Name", email = "your@email.ru"},
]
classifiers = [
"Development Status :: 3 - Alfa",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Framework :: Django",
"Framework :: Pydantic",
]
# Набор библиотек, которые вам скорее всего понадобятся, если вы что-то делаете на django
dependencies = [
# Чисто django
"django>=5.1.1",
"djangorestframework>=3.15.2",
"django-extensions>=3.2.3",
# Для CORS в джанго
"django-cors-headers>=4.4.0",
"whitenoise>=6.7.0",
# Для работы с celery
"celery>=5.4.0",
"django-celery-beat>=2.7.0",
"django-celery-results>=2.5.1",
"flower>=2.0.1",
# Для работы с redis (в новых версиях с ним теперь можно работать и синхронно и асинхронно)
"redis>=5.1.0",
"django-redis>=5.4.0",
# Я использую Pydantic для сериализации и валидации
"pydantic[email]>=2.9.2",
"pydantic-extra-types>=2.9.0",
# Супер удобная либа для работы с хранением файлов
"django-storages[s3]>=1.14.4",
# Для веб сервера
"uvicorn>=0.31.0",
"gunicorn>=23.0.0",
"uvicorn-worker>=0.2.0",
# Супер удобная либа для работы с переменными окружения
"envparse>=0.2.0",
# Прочее
"toml>=0.10.2",
"markdown>=3.7",
"psycopg2-binary>=2.9.9",
"sentry-sdk[celery]>=2.15.0",
"mock>=5.1.0",
"IPython>=8.29.0",
"requests>=2.32.3",
"pyjwt>=2.10.1",
"Pillow>=11.1.0",
]
[tool.pdm]
version = { source = "file", path = "src/__init__.py" }
distribution = true
# Стандартные группы и либы, которые я использую для разных сборок в большинстве своих проектов
[tool.pdm.dev-dependencies]
# Тесты
test = [
"pytest>=8.3.3",
"coverage>=7.6.1",
"pytest-cov>=6.0.0",
"pytest-django>=4.9.0",
"pytest-celery>=1.1.3",
]
# Документация
doc = [
"mkdocs>=1.6.1",
"mkdocstrings[python]>=0.26.1",
"mkdocs-material>=9.5.39",
]
# Линтеры
lint = [
"black>=24.8.0",
"ruff>=0.6.8",
]
# Прекоммит
dev = [
"pre-commit>=3.8.0",
]
[project.urls]
Homepage = "YOUR_HOME_PAGE"
Repository = "YOUR_REPOSITORY"
# Очень удобная либа для бампа версий bump-my-version
[tool.bumpversion]
current_version = "0.0.2"
tag = true
tag_name = "v{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
commit = true
message = "Bump version: {current_version} → {new_version}"
[[tool.bumpversion.files]]
filename = "src/__init__.py"
[[tool.bumpversion.files]]
filename = "README.md"
search = "Версия - v{current_version}"
replace = "Версия - v{new_version}"
[tool.ruff]
target-version = "py312"
line-length = 120
extend-exclude = [
".git",
".git-rewrite",
".ruff_cache",
".mypy_cache",
".venv",
".local_files",
"__pypackages__",
"fixtures",
"migrations/versions",
]
[tool.ruff.lint]
# Стандартные настройки линтера тут часто бывает вкусовщина
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"S", # flake8-bandit
"Q", # flake8-quotes
"PL", # Pylint
"RUF100", # Unused noqa directive
]
ignore = [
"B008", # do not perform function calls in argument defaults
"B904", # use 'raise ... from err'
"B905", # use explicit 'strict=' parameter with 'zip()'
"C901", # too complex
"C417", # Unnecessary `map` usage (rewrite using a `list` comprehension)
"F403", # used; unable to detect undefined names
"F405", # may be undefined, or defined from star imports
"E501", # Line too long handled by black
"N818", # Exception name should be named with an Error suffix
"S308", # mark_safe
"S603", # mark_safe
"S607", # mark_safe
"PLR0911", # Too many return statements
"PLR0912", # Too many branches
"PLR0913", # Too many arguments to function call
"PLR0915", # Too many statements
"PLW2901", # Loop variable overwritten by assignment target
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
# Настройки запуска тестов
[tool.pytest.ini_options]
# С данными аргументами тесты:
# - не будут запускать миграции
# - будут запускать сначала упавшие тесты
# - остановятся после первого провала
# - создадут тестовую бд
# - запустятся с выбранной настройкой django
addopts = "--no-migrations --failed-first --verbose --maxfail=1 -p no:warnings --create-db --ds=settings.test_settings"
norecursedirs = ["static", "migrations", "templates"]
python_files = "test_*.py"
django_find_project = false
[tool.coverage.run]
# Не отображать в отчете покрытия тестами
omit = ['*/tests/*', '*/migrations/*', 'settings/*']
Конфигурация mkdocs
site_name: YOUR_SITE
nav:
- Главная: index.md
theme:
name: material
highlightjs: true
hljs_languages:
- yaml
- python
language: ru
features:
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- navigation.expand
- navigation.path
- navigation.indexes
- navigation.top
- toc.follow
markdown_extensions:
- admonition
- codehilite
- pymdownx.betterem
- pymdownx.caret
- pymdownx.mark
- pymdownx.tilde
- pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.superfences
- pymdownx.inlinehilite
plugins:
- autorefs
- search
- tags
- mkdocstrings:
default_handler: python
handlers:
python:
import:
# latest instead of stable
- http://python-eve.org/objects.inv
options:
extensions:
- griffe_inherited_docstrings
find_stubs_package: true
summary: true
relative_crossrefs: true
merge_init_into_class: true
inherited_members: true
allow_inspection: true
annotations_path: source
line_length: 60
separate_signature: true
signature_crossrefs: false
modernize_annotations: true
docstring_style: google
docstring_section_style: spacy
docstring_options:
trim_doctest_flags: true
warn_unknown_params: true
ignore_init_summary: true
show_docstring_attributes: false
show_docstring_functions: true
show_docstring_classes: true
show_docstring_modules: true
show_docstring_description: true
show_docstring_examples: true
show_docstring_other_parameters: true
show_docstring_parameters: true
show_docstring_raises: true
show_docstring_receives: true
show_docstring_returns: true
show_docstring_warns: true
show_docstring_yields: true
show_root_heading: true
show_root_toc_entry: true
show_root_full_path: false
show_root_members_full_path: false
show_symbol_type_heading: true
show_symbol_type_toc: true
show_source: true
show_object_full_path: false
show_signature: true
show_signature_annotations: true
show_category_heading: true
show_if_no_docstring: true
show_labels: true
show_bases: true
show_inheritance_diagram: true
show_submodules: false
heading_level: 2
parameter_headings: true
members_order: source
group_by_category: true
paths:
- /srs
preload_modules:
- __future__
- pydantic
- datetime
- abc
filters:
- "!^_"
- "__"
- "!^__slots__"
- "!^__all__"
Пример .pre-commit-config.yaml
default_language_version:
python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-toml
- id: check-yaml
args:
- --unsafe
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.0
hooks:
- id: pyupgrade
args: [--py312-plus]
- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
args: [src, --config, ./pyproject.toml, --skip-magic-trailing-comma]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.3
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.14.0
hooks:
- id: reorder-python-imports
args: [--py312-plus]
Пример .gitignore
/.env.local
/.pdm-python
/.venv/
/.ruff_cache/
/.cache/
/.deploy/
/.local_files/
/src/settings/local_settings.py
/src/migrations/versions/
/src/celerybeat-schedule
/src/celerybeat-schedule.db
/src/settings/local.py
/shell
/restore_postgres
/manage
/dbshell
/bash
/.env
/gl-code-quality-report.json
/docker/composes/docker-compose.override.yml
/init_clean_project
venv/
.idea/
/src/static/
/src/report.xml
/src/coverage.xml
/src/.coverage
/ca.crt
/src/report.html
Пример .dockerignore
# Distribution / packaging / PyCharm
.Python
.idea/
.venv/
# Project local
/.local_files/
/.ruff_cache/
.env
.env.local
# Project
/pipelines/
/scripts/
/src/celerybeat-schedule
/src/coverage.xml
/src/.coverage
/src/report.xml
/src/settings/local_settings.py
# Git
.git
.gitignore
.gitattributes
# CI
.gitlab-ci.yml
# Docker
.docker
.dockerignore
docker-compose.yml
**/dockerfiles
**/composes
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]
Пример базовых настроек base_settings.py
from __future__ import annotations
from pathlib import Path
import toml
from envparse import env
# Путь до корня src проекта
BASE_DIR = Path(__file__).resolve().parent.parent
# Метаданные из toml файла
with Path(BASE_DIR.parent, "pyproject.toml").open("r") as toml_file:
TOML_METADATA = toml.load(toml_file)
# Версия проекта
VERSION = TOML_METADATA["tool"]["bumpversion"]["current_version"]
# При DEBUG = True будет работать hot reload для uvicorn
DEBUG = env.bool("DEBUG", default=False)
# Продакшн режим
PRODUCTION = env.bool("PRODUCTION", default=False)
# Тестовый режим
TEST_MODE = False
# Ключ секретов для генерации токенов
SECRET_KEY = env.str("SECRET_KEY", default="")
# Адрес сайта
SITE_PROTOCOL = env.str("SITE_PROTOCOL", default="http")
SITE_DOMAIN = env.str("SITE_DOMAIN", default="127.0.0.1")
SITE_PORT = env.str("SITE_PORT", default=None)
SITE_URL = f"{SITE_PROTOCOL}://{SITE_DOMAIN}"
SITE_API_PATH = env.str("SITE_API_PATH", default="api/v1")
if SITE_PORT:
SITE_URL = f"{SITE_URL}:{SITE_PORT}"
SITE_API_URL = f"{SITE_URL}/{SITE_API_PATH}"
Пример конфигурации Nginx
Прошу обратить внимания на то, что данная конфигурация достаточно скудная, сам я ее считаю более чем позорной, но тем не менее она является достаточной для локальной разработки
# nginx.conf
server {
listen 0.0.0.0:${NGINX_PORT_INTERNAL};
server_name 0.0.0.0 ${HOST_NAME};
set $backend asgi:${ASGI_PORT_INTERNAL};
set $docs docs:${DOCS_PORT_INTERNAL};
resolver 127.0.0.11 valid=10s;
client_max_body_size 100M;
location /media/ {
alias /media/;
autoindex off;
}
location /docs/ {
proxy_pass http://$docs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_pass_request_headers on;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
send_timeout 1800;
}
location / {
proxy_pass http://$backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_pass_request_headers on;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
send_timeout 1800;
}
}
Заключение
В данной статье мы осветили основные концепции и идеи того, как можно делать удобные и понятные окружения для любых проектов. Очень надеюсь, что ничего фундаментально важного не забыл и вы смогли найти в этой статье что-то новое для себя. Буду рад любой критике или предложениям по улучшению как статьи так и самого подхода к организации окружения и проекта в целом. Сильно вдаваться в детали не стал, много о чем еще можно поговорить на данную тему, буду рад побеседовать с вами в комментариях к статье.
Комментарии (20)
pvzh
12.02.2025 15:30Не силён в Джанго, его значит можно деплоить как ASGI-приложение. Допустим, тогда вопрос, зачем gunicorn и uvicorn-worker? Ведь в Докер-окружении одного uvicorn достаточно.
Extralait Автор
12.02.2025 15:30Года 4 назад видел именно такой пример запуска, он прижился у меня в проектах, почитаю подробно свежую документацию, Спасибо за комментарий!
pvzh
12.02.2025 15:30У меня как-раз ощущение, что многие тащат gunicorn в ASGI-проекты по какой-то старой привычке. Сам uvicorn уже и несколько воркеров может, хотя при Докере это возможно лишнее.
Пруф про не обязательный Gunicorn
Extralait Автор
12.02.2025 15:30Ой, супер, спасибо, переработаю эту часть у себя на проектах, реально пропустил когда-то апдейт
MadridianFox
12.02.2025 15:30Спасибо что поделились, всегда интересно смотреть как это устроено у других.
Удобно конечно когда весь проект в одном репозитории, хотя и возникают вопросики - а что делать когда придётся использовать второй/третий репозитории? Проекты имеют обыкновение расти и распиливаться на микросервисы. Либо волевым решением постановить что будет монорепо, либо выносить конфигурацию запуска в отдельный репозиторий.Я выбрал второй вариант. Я работаю с проектами, где в каждом несколько десятков репозиториев с кодом - приложения и библиотеки. Ну и им необходим десяток сервисов - база, редис и т.д.
Чтобы это не превратилось к гигантский монолитный docker-compose.yml файл, созданы отдельные файлы под каждый компонент. Ну а чтобы облегчить типовые операции над приложениями написана утилита elc. А все конфиги необходимые для запуска приложений и сервисов проекта вынесены в репозиторий
Extralait Автор
12.02.2025 15:30На прошлой работе тоже был проект с отдельным репозиторием для стягивание репозиториев остальных приложений и их запуска, но до отдельной утилиты так и не добрались. Большое спасибо за комментарий, попробую тоже накидать пример с удобным разделением и запуском микросервисов
Tony-Sol
12.02.2025 15:30pipelines # пайплайны GitLab
Не надо так, ведь есть стандартная директория .gitlab
Да, к сожалению переместить туда
.gitlab-ci.yml
нельзя, но можно сделать его буквально из одной строкиinclude: ".gitlab/ci.yaml"
ahmpro
12.02.2025 15:30к сожалению переместить туда .gitlab-ci.yml нельзя, но можно сделать его буквально из одной строки
в настройках конкретного проекта CI/CD -> General pipelines можно переопределить путь, удобно когда репозиторий кочует между гитлабами
Tony-Sol
12.02.2025 15:30Это да, в целом то можно
и даже
для self-hosted gitlab'а в admin area можно для всех реп задать (Settings -> CI/CD -> Continuous Integration and Deployment -> Default CI/CD configuration file)
но для этого должны быть права не ниже maintainer (а то как бы и не owner, тут не помню)
репозиторий кочует между гитлабами
Ну не знаю, ИМХО наоборот не удобно отходить от пути по умолчанию - это ж при каждом переезде надо помнить про эту кастомизацию
Tony-Sol
12.02.2025 15:30Рекомендую рассмотреть использование утилиты mise - сильно упрощает работу с окружением per-project, например позволит схлопнуть все
.env
и.python-version
(раз статья в разрезе python разработки) файлы и кастомные скрипты автоматизации (как например создание локальных файлов из статьи) в единый форматExtralait Автор
12.02.2025 15:30Большое спасибо, почитаю! Я как раз работу с переменными хотел переработать, но не знал, как
ahmpro
Это противоречит всем современным практикам: один образ - много окружений, но никак не собирать разные образы под разные окружения
ahmpro
Я вижу что это про локальную разработку, но ведь когда-то это попадет на прод, и крайне желательно иметь единообразие между разными окружениями, меняется лишь инфраструктура вокруг
Extralait Автор
Мне казалось, что нежелательно тянуть dev, test и doc зависимости в финальный образ. Или такой подход разделения на группы релевантен исключительно для работы с библиотеками?
ahmpro
dev зависимостей и правда не должно быть на проде, но всё остальное вполне себе часть проекта
Extralait Автор
А как поступать, если вам необходимо доставлять образ в продакшн среду клиента предварительно обфусцировав код?
ahmpro
обфуцировав прежде чем вы из этого сделаете образ и начнете гонять дальше образ по workflow, было б очень странно когда оттестировали один вариант, а в проде запускается совершенно другое, это аналогично тому что вы запускали бы сервис в прод без какой либо валидации
Extralait Автор
Принял, большое спасибо, за разъяснения, пересмотрю тогда пайплайны