feat: Add Docker infrastructure for multi-tenant Django application with services for database, caching, and task processing.

This commit is contained in:
2026-01-17 18:27:57 +03:00
parent 10ab804396
commit 9b4b1277ee
5 changed files with 9 additions and 9 deletions

68
docker/Dockerfile Normal file
View File

@@ -0,0 +1,68 @@
# 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 . .
# Создаём директории для статики и медиа
RUN mkdir -p /app/staticfiles /app/media
# Создаём непривилегированного пользователя
RUN useradd -m -u 1000 appuser
# Установка gosu для безопасного понижения привилегий
RUN set -eux; \
apt-get update; \
apt-get install -y gosu; \
rm -rf /var/lib/apt/lists/*; \
# Проверка работы gosu
gosu nobody true
# Копируем entrypoint скрипт
COPY docker/entrypoint.sh /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
# USER appuser - УДАЛЕНО: запускаем entrypoint от root для настройки прав
# Порт приложения
EXPOSE 8000
# Точка входа
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
# Команда по умолчанию
CMD ["web"]

View File

@@ -0,0 +1,61 @@
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}")
email = os.environ.get('TENANT_ADMIN_EMAIL', 'admin@example.com')
name = os.environ.get('TENANT_ADMIN_NAME', 'System Administrator')
# 1. Ensure Client exists
client, created = Client.objects.get_or_create(
schema_name='public',
defaults={
'name': 'System Tenant',
'owner_email': email,
'owner_name': name
}
)
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!")
# 3. Init system data (System Customer, etc.)
# SKIP for public tenant as it doesn't have these tables (they separate in tenant schemas)
# print("Initializing system data for public tenant...")
# from django.core.management import call_command
# try:
# call_command('init_tenant_data', schema='public')
# except Exception as e:
# print(f"Error initializing system data: {e}")
if __name__ == '__main__':
ensure_public_tenant()

111
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,111 @@
services:
# PostgreSQL база данных
db:
image: postgres:15-alpine
container_name: mix_postgres
restart: unless-stopped
env_file:
- /Volume1/DockerAppsData/mixapp/app/myproject/docker/.env.docker
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: 10
start_period: 30s
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
image: mix_web:latest
container_name: mix_web
restart: unless-stopped
command: web
env_file:
- .env.docker
volumes:
# ВАЖНО: монтируем именно папку проекта в /myproject, чтобы /app/manage.py существовал
- /Volume1/DockerAppsData/mixapp/app/myproject:/app
# Медиа и статика (как ожидает Django)
- /Volume1/DockerAppsData/mixapp/media:/app/myproject/media
- /Volume1/DockerAppsData/mixapp/static:/app/myproject/staticfiles
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- mix_network
- proxy-net
# Celery Worker
celery-worker:
build:
context: /Volume1/DockerAppsData/mixapp/app
dockerfile: docker/Dockerfile
image: mix_celery:latest
container_name: mix_celery_worker
restart: unless-stopped
command: celery-worker
env_file:
- .env.docker
volumes:
- /Volume1/DockerAppsData/mixapp/app/myproject:/app
- /Volume1/DockerAppsData/mixapp/media:/app/myproject/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
image: mix_celery:latest
container_name: mix_celery_beat
restart: unless-stopped
command: celery-beat
env_file:
- .env.docker
volumes:
- /Volume1/DockerAppsData/mixapp/app/myproject:/app
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- mix_network
networks:
mix_network:
driver: bridge
proxy-net:
external: true
name: proxy-net

265
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,265 @@
#!/bin/bash
set -e
# Ожидание готовности PostgreSQL
wait_for_postgres() {
echo "Waiting for PostgreSQL..."
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'),
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 Exception as e:
print(f'Retry error: {e}', file=sys.stderr)
exit(1)
"
done
echo "PostgreSQL is up!"
}
# Ожидание готовности Redis
wait_for_redis() {
echo "Waiting for Redis..."
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'),
port=int(os.environ.get('REDIS_PORT', '6379')),
db=int(os.environ.get('REDIS_DB', '0'))
)
r.ping()
exit(0)
except Exception as e:
print(f'Redis retry error: {e}', file=sys.stderr)
exit(1)
"
done
echo "Redis is up!"
}
# Создание папок media и staticfiles с правильными правами
setup_directories() {
echo "Setting up media and static directories..."
# Определяем пути (в Docker BASE_DIR = /app, поэтому MEDIA_ROOT = /app/myproject/media)
MEDIA_ROOT="/app/myproject/media"
STATIC_ROOT="/app/myproject/staticfiles"
# Создаем папки если их нет (рекурсивно)
# Важно: создаем структуру папок для tenants
mkdir -p "$MEDIA_ROOT/tenants" "$STATIC_ROOT" 2>/dev/null || true
# Пытаемся установить права доступа
# Сначала меняем владельца на appuser (так как мы root)
chown -R appuser:appuser "$MEDIA_ROOT" "$STATIC_ROOT" 2>/dev/null || true
# Используем 777 для папок media, чтобы контейнер мог писать независимо от прав на хосте
# Это безопасно, так как доступ контролируется на уровне Docker volume
# Устанавливаем права рекурсивно на все существующие файлы и папки
find "$MEDIA_ROOT" -type d -exec chmod 777 {} \; 2>/dev/null || true
find "$MEDIA_ROOT" -type f -exec chmod 666 {} \; 2>/dev/null || true
chmod -R 755 "$STATIC_ROOT" 2>/dev/null || true
echo "Media directory created/checked: $MEDIA_ROOT (permissions set)"
echo "Static directory created/checked: $STATIC_ROOT"
}
# Применение миграций и создание суперпользователя
run_migrations() {
echo "Running migrations for shared apps..."
gosu appuser python manage.py migrate_schemas --shared
echo "Running migrations for tenant schemas..."
gosu appuser python manage.py migrate_schemas --tenant
echo "Collecting static files..."
gosu appuser python manage.py collectstatic --noinput
# Устанавливаем права ПОСЛЕ collectstatic
echo "Setting permissions on static files..."
STATIC_ROOT="/app/myproject/staticfiles"
find "$STATIC_ROOT" -type d -exec chmod 755 {} \; 2>/dev/null || true
find "$STATIC_ROOT" -type f -exec chmod 644 {} \; 2>/dev/null || true
echo "Ensuring public tenant exists..."
gosu appuser python /app/docker/create_public_tenant.py
}
# Создание PlatformAdmin если не существует
create_platform_admin() {
echo "Creating PlatformAdmin if not exists..."
python manage.py shell << EOF
from platform_admin.models import PlatformAdmin
import os
# Создаём PlatformAdmin из переменных окружения
email = os.environ.get('PLATFORM_ADMIN_EMAIL', 'admin@platform.com')
password = os.environ.get('PLATFORM_ADMIN_PASSWORD')
name = os.environ.get('PLATFORM_ADMIN_NAME', 'Platform Admin')
if not password:
print('WARNING: PLATFORM_ADMIN_PASSWORD not set. Skipping PlatformAdmin creation.')
print('Create PlatformAdmin manually via Django shell:')
print(' from platform_admin.models import PlatformAdmin')
print(' PlatformAdmin.objects.create_superuser(email="...", name="...", password="...")')
else:
if not PlatformAdmin.objects.filter(email=email).exists():
admin = PlatformAdmin.objects.create_superuser(
email=email,
name=name,
password=password
)
print(f'PlatformAdmin {email} created successfully!')
else:
print(f'PlatformAdmin {email} already exists.')
EOF
}
# Если manage.py не в текущей директории, но есть в подпапке myproject
if [ ! -f "manage.py" ] && [ -d "myproject" ]; then
# Пытаемся войти в директорию, перенаправляя ошибки в /dev/null
if cd myproject 2>/dev/null; then
echo "Changing directory to myproject..."
# Устанавливаем PYTHONPATH чтобы Python мог найти модуль myproject
export PYTHONPATH=$(pwd):$PYTHONPATH
echo "PYTHONPATH set to: $PYTHONPATH"
else
# Если не можем войти в директорию (проблема с правами), устанавливаем PYTHONPATH из текущей директории
echo "Warning: Cannot access myproject directory (permission denied). Setting PYTHONPATH to include myproject..."
export PYTHONPATH=/app/myproject:$PYTHONPATH
echo "PYTHONPATH set to: $PYTHONPATH"
fi
fi
case "$1" in
web)
wait_for_postgres
wait_for_redis
setup_directories
run_migrations
create_platform_admin
echo "Starting Gunicorn..."
exec gosu appuser gunicorn myproject.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 3 \
--threads 2 \
--timeout 600 \
--access-logfile - \
--error-logfile - \
--capture-output
;;
celery-worker)
wait_for_postgres
wait_for_redis
setup_directories
echo "Starting Celery Worker for photo processing and product import..."
exec gosu appuser celery -A myproject worker \
-l info \
--concurrency=4 \
-Q celery,photo_processing
;;
celery-beat)
wait_for_postgres
wait_for_redis
echo "Starting Celery Beat..."
exec gosu appuser celery -A myproject beat -l info
;;
migrate)
wait_for_postgres
# Миграции тоже запускаем от gosu
gosu appuser python manage.py migrate_schemas --shared
gosu appuser python manage.py migrate_schemas --tenant
gosu appuser python manage.py collectstatic --noinput
# Права уже выставлены setup_directories (который запускается перед этим в case web/celery,
# но для migrate мы можем вызвать его явно или просто поправить права на статику)
# В данном блоке setup_directories не вызывался в оригинальном скрипте, но лучше вызвать если хотим гарантий
# setup_directories
# Для migrate обычно важно просто создать схемы.
create_platform_admin
;;
collectstatic)
wait_for_postgres
setup_directories
echo "Collecting static files..."
gosu appuser python manage.py collectstatic --noinput
echo "Setting permissions on static files..."
STATIC_ROOT="/app/myproject/staticfiles"
find "$STATIC_ROOT" -type d -exec chmod 755 {} \; 2>/dev/null || true
find "$STATIC_ROOT" -type f -exec chmod 644 {} \; 2>/dev/null || true
echo "Static files collected and permissions set."
;;
shell)
exec gosu appuser python manage.py shell
;;
*)
exec "$@"
;;
esac

View File

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