feat: Implement Dockerized multi-tenant Django application with initial setup for database, migrations, and superuser creation.

This commit is contained in:
2025-12-12 18:04:36 +03:00
parent 0046b36e89
commit 4cbc5c07b9
7 changed files with 152 additions and 27 deletions

View File

@@ -37,20 +37,24 @@ COPY myproject/ .
# Создаём директории для статики и медиа # Создаём директории для статики и медиа
RUN mkdir -p /app/staticfiles /app/media RUN mkdir -p /app/staticfiles /app/media
# Создаём непривилегированного пользователя
RUN useradd -m -u 1000 appuser
# Копируем entrypoint скрипт # Копируем entrypoint скрипт
COPY docker/entrypoint.sh /entrypoint.sh 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 USER appuser
# Порт приложения # Порт приложения
EXPOSE 8000 EXPOSE 8000
# Точка входа # Точка входа (запускаем через bash явно, чтобы избежать ошибок Permission denied)
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
# Команда по умолчанию (будет переопределена в docker-compose) # Команда по умолчанию (будет переопределена в docker-compose)
CMD ["web"] CMD ["web"]

View File

@@ -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()

View File

@@ -4,6 +4,7 @@ version: '3.8'
# YAML файл хранится в /Volume1/DockerYAML/mix/ # YAML файл хранится в /Volume1/DockerYAML/mix/
services: services:
# PostgreSQL база данных # PostgreSQL база данных
db: db:
image: postgres:15-alpine image: postgres:15-alpine
@@ -11,11 +12,7 @@ services:
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- /Volume1/DockerAppsData/mixapp/app/docker/.env.docker - /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: volumes:
- /Volume1/DockerAppsData/mixapp/postgres:/var/lib/postgresql/data - /Volume1/DockerAppsData/mixapp/postgres:/var/lib/postgresql/data
healthcheck: healthcheck:
@@ -52,7 +49,16 @@ services:
command: web command: web
env_file: env_file:
- /Volume1/DockerAppsData/mixapp/app/docker/.env.docker - /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: volumes:
# Монтируем код приложения для горячего обновления (опционально, если нужно обновлять без пересборки)
# - /Volume1/DockerAppsData/mixapp/app:/app
# Медиа и статика
- /Volume1/DockerAppsData/mixapp/media:/app/media - /Volume1/DockerAppsData/mixapp/media:/app/media
- /Volume1/DockerAppsData/mixapp/static:/app/staticfiles - /Volume1/DockerAppsData/mixapp/static:/app/staticfiles
ports: ports:
@@ -75,7 +81,11 @@ services:
command: celery-worker command: celery-worker
env_file: env_file:
- /Volume1/DockerAppsData/mixapp/app/docker/.env.docker - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker
environment:
- DB_HOST=db
- REDIS_HOST=redis
volumes: volumes:
- /Volume1/DockerAppsData/mixapp/app:/app
- /Volume1/DockerAppsData/mixapp/media:/app/media - /Volume1/DockerAppsData/mixapp/media:/app/media
depends_on: depends_on:
db: db:
@@ -95,6 +105,11 @@ services:
command: celery-beat command: celery-beat
env_file: env_file:
- /Volume1/DockerAppsData/mixapp/app/docker/.env.docker - /Volume1/DockerAppsData/mixapp/app/docker/.env.docker
environment:
- DB_HOST=db
- REDIS_HOST=redis
volumes:
- /Volume1/DockerAppsData/mixapp/app:/app
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@@ -4,9 +4,41 @@ set -e
# Ожидание готовности PostgreSQL # Ожидание готовности PostgreSQL
wait_for_postgres() { wait_for_postgres() {
echo "Waiting for PostgreSQL..." echo "Waiting for PostgreSQL..."
while ! python -c " python -c "
import psycopg2 import psycopg2
import os 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: try:
conn = psycopg2.connect( conn = psycopg2.connect(
dbname=os.environ.get('DB_NAME', 'inventory_db'), dbname=os.environ.get('DB_NAME', 'inventory_db'),
@@ -17,11 +49,10 @@ try:
) )
conn.close() conn.close()
exit(0) exit(0)
except: except Exception as e:
print(f'Retry error: {e}', file=sys.stderr)
exit(1) exit(1)
" 2>/dev/null; do "
echo "PostgreSQL is unavailable - sleeping"
sleep 2
done done
echo "PostgreSQL is up!" echo "PostgreSQL is up!"
} }
@@ -29,9 +60,33 @@ except:
# Ожидание готовности Redis # Ожидание готовности Redis
wait_for_redis() { wait_for_redis() {
echo "Waiting for Redis..." echo "Waiting for Redis..."
while ! python -c " python -c "
import redis import redis
import os 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: try:
r = redis.Redis( r = redis.Redis(
host=os.environ.get('REDIS_HOST', 'redis'), host=os.environ.get('REDIS_HOST', 'redis'),
@@ -40,11 +95,10 @@ try:
) )
r.ping() r.ping()
exit(0) exit(0)
except: except Exception as e:
print(f'Redis retry error: {e}', file=sys.stderr)
exit(1) exit(1)
" 2>/dev/null; do "
echo "Redis is unavailable - sleeping"
sleep 2
done done
echo "Redis is up!" echo "Redis is up!"
} }
@@ -59,6 +113,9 @@ run_migrations() {
echo "Collecting static files..." echo "Collecting static files..."
python manage.py collectstatic --noinput 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( user = User.objects.create_superuser(
email=email, email=email,
password=password, password=password,
first_name=first_name name=first_name
) )
print(f'Superuser {email} created successfully!') print(f'Superuser {email} created successfully!')
else: else:

View File

@@ -225,7 +225,8 @@ STATICFILES_DIRS = [BASE_DIR / 'static']
# В production используем внешнюю директорию для nginx # В production используем внешнюю директорию для nginx
if not DEBUG: if not DEBUG:
STATIC_ROOT = '/Volume1/DockerAppsData/npm/data/static/' # Внутри контейнера путь всегда /app/staticfiles (куда мы смонтировали volume)
STATIC_ROOT = BASE_DIR / 'staticfiles'
else: else:
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / 'staticfiles'

View File

@@ -97,7 +97,8 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})')) 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}') self.stdout.write(f'Создание домена: {domain_name}')
domain = Domain.objects.create( domain = Domain.objects.create(
domain=domain_name, domain=domain_name,

View File

@@ -7,6 +7,7 @@ Management команда для создания нового тенанта (
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from django.conf import settings
from tenants.models import Client, Domain from tenants.models import Client, Domain
import re import re
@@ -115,7 +116,8 @@ class Command(BaseCommand):
def get_domain_name(self, default_subdomain): def get_domain_name(self, default_subdomain):
"""Получить доменное имя""" """Получить доменное имя"""
while True: 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() domain = input(f'Доменное имя [{default_domain}]: ').strip().lower()
if not domain: if not domain: