diff --git a/docker/Dockerfile b/docker/Dockerfile index 4f04dd9..1e5b955 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -37,20 +37,24 @@ COPY myproject/ . # Создаём директории для статики и медиа RUN mkdir -p /app/staticfiles /app/media +# Создаём непривилегированного пользователя +RUN useradd -m -u 1000 appuser + # Копируем entrypoint скрипт COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY docker/create_public_tenant.py /app/docker/create_public_tenant.py +RUN chmod 755 /entrypoint.sh && chown appuser:appuser /entrypoint.sh + +# Меняем владельца рабочей директории +RUN chown -R appuser:appuser /app -# Создаём непривилегированного пользователя -RUN useradd -m -u 1000 appuser && \ - chown -R appuser:appuser /app USER appuser # Порт приложения EXPOSE 8000 -# Точка входа -ENTRYPOINT ["/entrypoint.sh"] +# Точка входа (запускаем через bash явно, чтобы избежать ошибок Permission denied) +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] # Команда по умолчанию (будет переопределена в docker-compose) CMD ["web"] diff --git a/docker/create_public_tenant.py b/docker/create_public_tenant.py new file mode 100644 index 0000000..85be4b5 --- /dev/null +++ b/docker/create_public_tenant.py @@ -0,0 +1,45 @@ +import os +import django +from django.conf import settings + +import sys + +# Add /app to sys.path so we can import myproject +sys.path.append('/app') + +# Setup Django environment +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') +django.setup() + +from tenants.models import Client, Domain + +def ensure_public_tenant(): + domain_name = os.environ.get('DOMAIN_NAME', 'localhost') + print(f"Checking public tenant for domain: {domain_name}") + + # 1. Ensure Client exists + client, created = Client.objects.get_or_create( + schema_name='public', + defaults={'name': 'Main Tenant'} + ) + if created: + print("Created public tenant client.") + else: + print("Public tenant client already exists.") + + # 2. Ensure Domain exists + # Check if this specific domain exists + domain, created = Domain.objects.get_or_create( + domain=domain_name, + defaults={'tenant': client, 'is_primary': True} + ) + + if created: + print(f"Created domain {domain_name} for public tenant.") + else: + print(f"Domain {domain_name} already exists.") + if domain.tenant != client: + print(f"WARNING: Domain {domain_name} is assigned to another tenant!") + +if __name__ == '__main__': + ensure_public_tenant() diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 232c10f..6ce8033 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,6 +4,7 @@ version: '3.8' # YAML файл хранится в /Volume1/DockerYAML/mix/ services: + # PostgreSQL база данных db: image: postgres:15-alpine @@ -11,15 +12,11 @@ services: 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}"] + test: [ "CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-inventory_db}" ] interval: 10s timeout: 5s retries: 5 @@ -35,7 +32,7 @@ services: volumes: - /Volume1/DockerAppsData/mixapp/redis:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 5 @@ -52,7 +49,16 @@ services: command: web env_file: - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + environment: + - ALLOWED_HOSTS=mix.smaa.by,.mix.smaa.by,localhost,127.0.0.1 + - CSRF_TRUSTED_ORIGINS=https://mix.smaa.by,https://*.mix.smaa.by + - DOMAIN_NAME=mix.smaa.by + - DB_HOST=db + - REDIS_HOST=redis volumes: + # Монтируем код приложения для горячего обновления (опционально, если нужно обновлять без пересборки) + # - /Volume1/DockerAppsData/mixapp/app:/app + # Медиа и статика - /Volume1/DockerAppsData/mixapp/media:/app/media - /Volume1/DockerAppsData/mixapp/static:/app/staticfiles ports: @@ -75,7 +81,11 @@ services: command: celery-worker env_file: - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + environment: + - DB_HOST=db + - REDIS_HOST=redis volumes: + - /Volume1/DockerAppsData/mixapp/app:/app - /Volume1/DockerAppsData/mixapp/media:/app/media depends_on: db: @@ -95,6 +105,11 @@ services: command: celery-beat env_file: - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker + environment: + - DB_HOST=db + - REDIS_HOST=redis + volumes: + - /Volume1/DockerAppsData/mixapp/app:/app depends_on: db: condition: service_healthy diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 84afc21..5c8941f 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,9 +4,41 @@ set -e # Ожидание готовности PostgreSQL wait_for_postgres() { echo "Waiting for PostgreSQL..." - while ! python -c " + python -c " import psycopg2 import os +import sys + +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') + +print(f'Attempting connection to: host={host} port={port} dbname={dbname} user={user}') + +try: + conn = psycopg2.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port + ) + conn.close() + print('Connection successful!') + exit(0) +except Exception as e: + print(f'Error connecting to PostgreSQL: {e}', file=sys.stderr) + exit(1) +" + while [ $? -ne 0 ]; do + echo "PostgreSQL is unavailable - sleeping" + sleep 2 + python -c " +import psycopg2 +import os +import sys try: conn = psycopg2.connect( dbname=os.environ.get('DB_NAME', 'inventory_db'), @@ -17,11 +49,10 @@ try: ) conn.close() exit(0) -except: +except Exception as e: + print(f'Retry error: {e}', file=sys.stderr) exit(1) -" 2>/dev/null; do - echo "PostgreSQL is unavailable - sleeping" - sleep 2 +" done echo "PostgreSQL is up!" } @@ -29,9 +60,33 @@ except: # Ожидание готовности Redis wait_for_redis() { echo "Waiting for Redis..." - while ! python -c " + python -c " import redis import os +import sys + +host = os.environ.get('REDIS_HOST', 'redis') +port = int(os.environ.get('REDIS_PORT', '6379')) +db = int(os.environ.get('REDIS_DB', '0')) + +print(f'Attempting connection to Redis: host={host} port={port} db={db}') + +try: + r = redis.Redis(host=host, port=port, db=db) + r.ping() + print('Redis connection successful!') + exit(0) +except Exception as e: + print(f'Error connecting to Redis: {e}', file=sys.stderr) + exit(1) +" + while [ $? -ne 0 ]; do + echo "Redis is unavailable - sleeping" + sleep 2 + python -c " +import redis +import os +import sys try: r = redis.Redis( host=os.environ.get('REDIS_HOST', 'redis'), @@ -40,11 +95,10 @@ try: ) r.ping() exit(0) -except: +except Exception as e: + print(f'Redis retry error: {e}', file=sys.stderr) exit(1) -" 2>/dev/null; do - echo "Redis is unavailable - sleeping" - sleep 2 +" done echo "Redis is up!" } @@ -59,6 +113,9 @@ run_migrations() { echo "Collecting static files..." python manage.py collectstatic --noinput + + echo "Ensuring public tenant exists..." + python /app/docker/create_public_tenant.py } # Создание суперпользователя если не существует @@ -82,7 +139,7 @@ with schema_context('public'): user = User.objects.create_superuser( email=email, password=password, - first_name=first_name + name=first_name ) print(f'Superuser {email} created successfully!') else: diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index a8604ea..9877b4f 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -225,7 +225,8 @@ STATICFILES_DIRS = [BASE_DIR / 'static'] # В production используем внешнюю директорию для nginx if not DEBUG: - STATIC_ROOT = '/Volume1/DockerAppsData/npm/data/static/' + # Внутри контейнера путь всегда /app/staticfiles (куда мы смонтировали volume) + STATIC_ROOT = BASE_DIR / 'staticfiles' else: STATIC_ROOT = BASE_DIR / 'staticfiles' diff --git a/myproject/tenants/management/commands/activate_registration.py b/myproject/tenants/management/commands/activate_registration.py index 780c675..ab28908 100644 --- a/myproject/tenants/management/commands/activate_registration.py +++ b/myproject/tenants/management/commands/activate_registration.py @@ -97,7 +97,8 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})')) # Создаем домен - domain_name = f"{registration.schema_name}.localhost" + domain_base = getattr(settings, 'TENANT_DOMAIN_BASE', 'localhost') + domain_name = f"{registration.schema_name}.{domain_base}" self.stdout.write(f'Создание домена: {domain_name}') domain = Domain.objects.create( domain=domain_name, diff --git a/myproject/tenants/management/commands/create_tenant.py b/myproject/tenants/management/commands/create_tenant.py index ad90997..2c87a6c 100644 --- a/myproject/tenants/management/commands/create_tenant.py +++ b/myproject/tenants/management/commands/create_tenant.py @@ -7,6 +7,7 @@ Management команда для создания нового тенанта ( """ from django.core.management.base import BaseCommand from django.db import transaction +from django.conf import settings from tenants.models import Client, Domain import re @@ -115,7 +116,8 @@ class Command(BaseCommand): def get_domain_name(self, default_subdomain): """Получить доменное имя""" while True: - default_domain = f'{default_subdomain}.localhost' + domain_base = getattr(settings, 'TENANT_DOMAIN_BASE', 'localhost') + default_domain = f'{default_subdomain}.{domain_base}' domain = input(f'Доменное имя [{default_domain}]: ').strip().lower() if not domain: