diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef20fe2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,57 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info +.eggs +dist +build +*.egg + +# Virtual environment +venv +.venv +env +.env.local + +# IDE +.vscode +.idea +*.swp +*.swo + +# Tests +.pytest_cache +.coverage +htmlcov + +# Documentation +*.md +docs/ + +# Local files +*.log +*.sqlite3 +db.sqlite3 + +# Media and static (монтируются как volumes) +myproject/media/* +myproject/staticfiles/* + +# Temporary files +*.tmp +*.temp +.DS_Store +Thumbs.db + +# Windows batch files +*.bat + +# Keep important config files +!myproject/.env.example diff --git a/.gitignore b/.gitignore index dd996b7..ae323f5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ staticfiles/ .env .env.local *.env +docker/.env.docker +docker/entrypoint.sh # IDE .vscode/ diff --git a/docker/.env.docker.example b/docker/.env.docker.example new file mode 100644 index 0000000..04de10d --- /dev/null +++ b/docker/.env.docker.example @@ -0,0 +1,28 @@ +# Django settings +SECRET_KEY=change-this-to-a-secure-random-key-in-production-min-50-chars +DEBUG=False +ALLOWED_HOSTS=yourdomain.com,*.yourdomain.com,localhost,127.0.0.1 + +# Database (PostgreSQL) +DB_NAME=inventory_db +DB_USER=postgres +DB_PASSWORD=your-secure-postgres-password-here +DB_HOST=db +DB_PORT=5432 + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=0 + +# Celery +CELERY_BROKER_URL=redis://redis:6379/0 + +# Tenant Admin (создаётся при первом запуске) +TENANT_ADMIN_EMAIL=admin@example.com +TENANT_ADMIN_PASSWORD=change-this-secure-password +TENANT_ADMIN_NAME=Admin + +# Django-tenants +# Основной домен для public схемы +PUBLIC_SCHEMA_DOMAIN=yourdomain.com diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..4f04dd9 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,56 @@ +# Dockerfile для Django приложения с Celery +FROM python:3.11-slim + +# Переменные окружения +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DJANGO_SETTINGS_MODULE=myproject.settings + +# Установка системных зависимостей +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Для PostgreSQL + libpq-dev \ + postgresql-client \ + # Для Pillow и pillow-heif + libjpeg-dev \ + libpng-dev \ + libwebp-dev \ + libheif-dev \ + libde265-dev \ + # Для сборки Python пакетов + gcc \ + g++ \ + # Утилиты + curl \ + && rm -rf /var/lib/apt/lists/* + +# Рабочая директория +WORKDIR /app + +# Копируем requirements и устанавливаем зависимости +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем проект +COPY myproject/ . + +# Создаём директории для статики и медиа +RUN mkdir -p /app/staticfiles /app/media + +# Копируем entrypoint скрипт +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Создаём непривилегированного пользователя +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app +USER appuser + +# Порт приложения +EXPOSE 8000 + +# Точка входа +ENTRYPOINT ["/entrypoint.sh"] + +# Команда по умолчанию (будет переопределена в docker-compose) +CMD ["web"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..232c10f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,108 @@ +version: '3.8' + +# Все файлы хранятся в /Volume1/DockerAppsData/mixapp/ +# YAML файл хранится в /Volume1/DockerYAML/mix/ + +services: + # PostgreSQL база данных + db: + image: postgres:15-alpine + container_name: mix_postgres + restart: unless-stopped + env_file: + - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + environment: + POSTGRES_DB: ${DB_NAME:-inventory_db} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" + volumes: + - /Volume1/DockerAppsData/mixapp/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-inventory_db}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - mix_network + + # Redis для кеша и Celery брокера + redis: + image: redis:7-alpine + container_name: mix_redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - /Volume1/DockerAppsData/mixapp/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - mix_network + + # Django Web приложение + web: + build: + context: /Volume1/DockerAppsData/mixapp/app + dockerfile: docker/Dockerfile + container_name: mix_web + restart: unless-stopped + command: web + env_file: + - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + volumes: + - /Volume1/DockerAppsData/mixapp/media:/app/media + - /Volume1/DockerAppsData/mixapp/static:/app/staticfiles + ports: + - "8000:8000" + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - mix_network + + # Celery Worker для обработки задач + celery-worker: + build: + context: /Volume1/DockerAppsData/mixapp/app + dockerfile: docker/Dockerfile + container_name: mix_celery_worker + restart: unless-stopped + command: celery-worker + env_file: + - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + volumes: + - /Volume1/DockerAppsData/mixapp/media:/app/media + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - mix_network + + # Celery Beat для периодических задач + celery-beat: + build: + context: /Volume1/DockerAppsData/mixapp/app + dockerfile: docker/Dockerfile + container_name: mix_celery_beat + restart: unless-stopped + command: celery-beat + env_file: + - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - mix_network + +networks: + mix_network: + driver: bridge diff --git a/docker/entrypoint.sh.example b/docker/entrypoint.sh.example new file mode 100644 index 0000000..84afc21 --- /dev/null +++ b/docker/entrypoint.sh.example @@ -0,0 +1,135 @@ +#!/bin/bash +set -e + +# Ожидание готовности PostgreSQL +wait_for_postgres() { + echo "Waiting for PostgreSQL..." + while ! python -c " +import psycopg2 +import os +try: + conn = psycopg2.connect( + dbname=os.environ.get('DB_NAME', 'inventory_db'), + user=os.environ.get('DB_USER', 'postgres'), + password=os.environ.get('DB_PASSWORD', 'postgres'), + host=os.environ.get('DB_HOST', 'db'), + port=os.environ.get('DB_PORT', '5432') + ) + conn.close() + exit(0) +except: + exit(1) +" 2>/dev/null; do + echo "PostgreSQL is unavailable - sleeping" + sleep 2 + done + echo "PostgreSQL is up!" +} + +# Ожидание готовности Redis +wait_for_redis() { + echo "Waiting for Redis..." + while ! python -c " +import redis +import os +try: + r = redis.Redis( + host=os.environ.get('REDIS_HOST', 'redis'), + port=int(os.environ.get('REDIS_PORT', '6379')), + db=int(os.environ.get('REDIS_DB', '0')) + ) + r.ping() + exit(0) +except: + exit(1) +" 2>/dev/null; do + echo "Redis is unavailable - sleeping" + sleep 2 + done + echo "Redis is up!" +} + +# Применение миграций и создание суперпользователя +run_migrations() { + echo "Running migrations for shared apps..." + python manage.py migrate_schemas --shared + + echo "Running migrations for tenant schemas..." + python manage.py migrate_schemas --tenant + + echo "Collecting static files..." + python manage.py collectstatic --noinput +} + +# Создание суперпользователя если не существует +create_superuser() { + echo "Creating superuser if not exists..." + python manage.py shell << EOF +from django.contrib.auth import get_user_model +from django.db import connection +from django_tenants.utils import schema_context +import os + +User = get_user_model() + +# Создаём суперпользователя в public схеме из переменных окружения +with schema_context('public'): + email = os.environ.get('TENANT_ADMIN_EMAIL', 'admin@example.com') + password = os.environ.get('TENANT_ADMIN_PASSWORD', 'changeme') + first_name = os.environ.get('TENANT_ADMIN_NAME', 'Admin') + + if not User.objects.filter(email=email).exists(): + user = User.objects.create_superuser( + email=email, + password=password, + first_name=first_name + ) + print(f'Superuser {email} created successfully!') + else: + print(f'Superuser {email} already exists.') +EOF +} + +case "$1" in + web) + wait_for_postgres + wait_for_redis + run_migrations + create_superuser + echo "Starting Gunicorn..." + exec gunicorn myproject.wsgi:application \ + --bind 0.0.0.0:8000 \ + --workers 3 \ + --threads 2 \ + --timeout 120 \ + --access-logfile - \ + --error-logfile - \ + --capture-output + ;; + celery-worker) + wait_for_postgres + wait_for_redis + echo "Starting Celery Worker..." + exec celery -A myproject worker \ + -l info \ + -Q celery,photo_processing \ + --concurrency=2 + ;; + celery-beat) + wait_for_postgres + wait_for_redis + echo "Starting Celery Beat..." + exec celery -A myproject beat -l info + ;; + migrate) + wait_for_postgres + run_migrations + create_superuser + ;; + shell) + exec python manage.py shell + ;; + *) + exec "$@" + ;; +esac diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index 8c027c5..e23bcc9 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -21,8 +21,11 @@ env = environ.Env( SECRET_KEY=(str, 'django-insecure-default-key-change-in-production'), ) -# Read .env file -environ.Env.read_env(BASE_DIR / '.env') +# Read .env file (if exists - for local development) +# In Docker, environment variables are set via docker-compose env_file +env_file = BASE_DIR / '.env' +if env_file.exists(): + environ.Env.read_env(env_file) # Quick-start development settings - unsuitable for production @@ -34,7 +37,14 @@ SECRET_KEY = env('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env('DEBUG') -ALLOWED_HOSTS = ['*'] # Для разработки. В продакшене указать конкретные домены +# Allowed hosts: читаем из переменной окружения или используем wildcard для разработки +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) + +# CSRF trusted origins для работы за nginx proxy +CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[ + 'https://mix.smaa.by', + 'https://*.mix.smaa.by', +]) # ============================================ @@ -212,7 +222,12 @@ USE_TZ = True STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static'] -STATIC_ROOT = BASE_DIR / 'staticfiles' + +# В production используем внешнюю директорию для nginx +if not DEBUG: + STATIC_ROOT = '/Volume1/DockerAppsData/npm/data/static/' +else: + STATIC_ROOT = BASE_DIR / 'staticfiles' # ============================================ diff --git a/requirements.txt b/requirements.txt index 19e2081..cb62d56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,4 @@ tzdata==2025.2 Unidecode==1.4.0 vine==5.1.0 wcwidth==0.2.14 +gunicorn==21.2.0