diff --git a/.env.example b/.env.example index 9a4604c..4769d29 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -# Django 설정 -DJANGO_SECRET_KEY=your-secret-key-here +# Django 기본 설정 +DJANGO_SECRET_KEY=your-secret-key-here-change-in-production DJANGO_DEBUG=False -DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com # 환경 설정 DJANGO_ENV=production @@ -9,9 +9,14 @@ DJANGO_ENV=production # 데이터베이스 (PostgreSQL 사용 시) # DATABASE_URL=postgresql://user:password@localhost:5432/dbname -# 정적 파일 +# 정적 파일 (프로덕션) STATIC_ROOT=/app/staticfiles STATIC_URL=/static/ -# 타임존 +# 미디어 파일 +MEDIA_ROOT=/app/mediafiles +MEDIA_URL=/media/ + +# 국제화 TIME_ZONE=Asia/Seoul +LANGUAGE_CODE=en-us diff --git a/.gitignore b/.gitignore index 055ff96..b19729b 100644 --- a/.gitignore +++ b/.gitignore @@ -56,8 +56,8 @@ venv.bak/ local_settings.py db.sqlite3 db.sqlite3-journal -media/ -staticfiles/ +/mediafiles/ +/staticfiles/ # Environment variables .env diff --git a/PRODUCTION_CHECKLIST.md b/PRODUCTION_CHECKLIST.md new file mode 100644 index 0000000..ce086c5 --- /dev/null +++ b/PRODUCTION_CHECKLIST.md @@ -0,0 +1,409 @@ +# 프로덕션 배포 체크리스트 + +## ✅ 배포 전 필수 확인 사항 + +### 1. 환경 설정 (.env) + +```bash +# .env 파일 생성 +cp .env.example .env +nano .env +``` + +**필수 변경 항목:** + +- [ ] `DJANGO_SECRET_KEY` - 새로운 시크릿 키 생성 + ```bash + python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' + ``` + +- [ ] `DJANGO_DEBUG=False` - 반드시 False로 설정 + +- [ ] `DJANGO_ALLOWED_HOSTS` - 실제 도메인 설정 + ```bash + DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com + ``` + +### 2. 정적 파일 설정 + +```bash +# 로컬에서 테스트 +python manage.py collectstatic --noinput + +# 정상 동작 확인 +python check_settings.py +``` + +**확인 항목:** +- [ ] `STATIC_ROOT` 설정됨 +- [ ] `STATIC_URL` 설정됨 +- [ ] staticfiles 디렉토리 생성됨 +- [ ] mediafiles 디렉토리 생성됨 + +### 3. 데이터베이스 + +```bash +# 마이그레이션 확인 +python manage.py showmigrations + +# 미적용 마이그레이션 적용 +python manage.py migrate +``` + +**확인 항목:** +- [ ] 모든 마이그레이션 적용됨 +- [ ] 전략 초기화 완료 + ```bash + python manage.py init_strategies + ``` + +### 4. Docker 설정 + +**docker-compose.yml 확인:** + +```yaml +environment: + - DJANGO_ENV=production + - DJANGO_DEBUG=False + - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} + - DJANGO_ALLOWED_HOSTS=${DJANGO_ALLOWED_HOSTS} +``` + +**확인 항목:** +- [ ] `.env` 파일 존재 +- [ ] 환경 변수 올바르게 설정됨 +- [ ] 볼륨 마운트 설정 확인 +- [ ] 포트 설정 확인 + +### 5. 보안 + +**settings.py 확인:** +- [ ] `DEBUG = False` +- [ ] `SECRET_KEY` 환경 변수에서 로드 +- [ ] `ALLOWED_HOSTS` 제한적으로 설정 +- [ ] HTTPS 강제 (프로덕션 환경) + ```python + SECURE_SSL_REDIRECT = True + SECURE_HSTS_SECONDS = 31536000 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + ``` + +### 6. 성능 + +**gunicorn 설정:** +- [ ] Worker 수 적절히 설정 (CPU 코어 수 × 2 + 1) + ```bash + # docker-compose.yml + command: gunicorn --bind 0.0.0.0:8000 --workers 4 executor.wsgi:application + ``` + +- [ ] 타임아웃 설정 + ```bash + --timeout 120 + ``` + +--- + +## 🚀 배포 단계 + +### 1. 로컬 테스트 + +```bash +# 설정 확인 +python check_settings.py + +# 정적 파일 수집 +python manage.py collectstatic --noinput + +# 개발 서버로 테스트 +python manage.py runserver +``` + +### 2. Docker 빌드 + +```bash +# 이미지 빌드 +docker-compose build web + +# 빌드 확인 +docker images | grep quantbench +``` + +### 3. 컨테이너 시작 + +```bash +# 백그라운드 실행 +docker-compose up -d web + +# 로그 확인 +docker-compose logs -f web +``` + +### 4. 헬스 체크 + +```bash +# API 응답 확인 +curl http://localhost:8000/strategies/ + +# 전략 목록 확인 +curl http://localhost:8000/strategies/ | python -m json.tool + +# 콜백 기능 테스트 +python test_callback.py +``` + +### 5. 모니터링 + +```bash +# 컨테이너 상태 +docker-compose ps + +# 리소스 사용량 +docker stats quantbench-executor + +# 로그 실시간 확인 +docker-compose logs -f +``` + +--- + +## 🔧 문제 해결 + +### STATIC_ROOT 오류 + +**증상:** +``` +ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting +``` + +**해결:** +```bash +# settings.py 확인 +grep STATIC_ROOT executor/settings.py + +# 디렉토리 생성 +mkdir -p staticfiles mediafiles + +# collectstatic 실행 +python manage.py collectstatic --noinput +``` + +### 환경 변수 로드 안됨 + +**증상:** +``` +Using default SECRET_KEY (insecure) +``` + +**해결:** +```bash +# .env 파일 확인 +cat .env + +# Docker에서 환경 변수 전달 확인 +docker-compose config + +# 컨테이너 내부 확인 +docker-compose exec web env | grep DJANGO +``` + +### 정적 파일 404 오류 + +**해결:** + +1. **개발 환경:** + ```python + # settings.py + DEBUG = True # 자동으로 정적 파일 서빙 + ``` + +2. **프로덕션 환경:** + ```bash + # nginx 설정 필요 + location /static/ { + alias /app/staticfiles/; + } + ``` + +### 마이그레이션 충돌 + +**해결:** +```bash +# 컨테이너에서 마이그레이션 초기화 +docker-compose exec web python manage.py migrate --run-syncdb + +# 또는 DB 초기화 +docker-compose down -v +docker-compose up -d +``` + +--- + +## 📊 성능 최적화 + +### 1. Gunicorn Worker 조정 + +**CPU 4코어 기준:** +```bash +workers = 9 # (4 × 2) + 1 +``` + +**docker-compose.yml:** +```yaml +command: gunicorn --bind 0.0.0.0:8000 --workers 9 --timeout 120 executor.wsgi:application +``` + +### 2. 데이터베이스 최적화 + +**PostgreSQL 사용 권장:** +```bash +# .env +DATABASE_URL=postgresql://user:pass@db:5432/executor + +# docker-compose.yml에 PostgreSQL 서비스 추가 +``` + +### 3. 캐싱 + +**Redis 캐시 추가:** +```python +# settings.py +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': 'redis://redis:6379/1', + } +} +``` + +### 4. 로깅 + +**프로덕션 로깅 설정:** +```python +# settings.py +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'filename': '/app/logs/django.log', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': True, + }, + }, +} +``` + +--- + +## 🔐 보안 강화 + +### 1. HTTPS 강제 + +```python +# settings.py (프로덕션) +if not DEBUG: + SECURE_SSL_REDIRECT = True + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_HSTS_SECONDS = 31536000 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True +``` + +### 2. CORS 설정 + +```bash +# 필요시 django-cors-headers 설치 +uv add django-cors-headers +``` + +```python +# settings.py +INSTALLED_APPS += ['corsheaders'] +MIDDLEWARE.insert(0, 'corsheaders.middleware.CorsMiddleware') + +CORS_ALLOWED_ORIGINS = [ + "https://yourdomain.com", +] +``` + +### 3. Rate Limiting + +```bash +# django-ratelimit 설치 +uv add django-ratelimit +``` + +```python +# views.py +from django_ratelimit.decorators import ratelimit + +@ratelimit(key='ip', rate='10/m') +def execute_strategy(request): + ... +``` + +--- + +## 📝 배포 후 확인 + +### 즉시 확인 + +- [ ] 서비스 응답 확인 (HTTP 200) +- [ ] 전략 목록 조회 가능 +- [ ] 전략 실행 정상 작동 +- [ ] 콜백 기능 테스트 + +### 24시간 후 + +- [ ] 에러 로그 확인 +- [ ] 메모리 사용량 확인 +- [ ] CPU 사용량 확인 +- [ ] 디스크 사용량 확인 + +### 주간 점검 + +- [ ] 로그 파일 로테이션 +- [ ] 데이터베이스 백업 +- [ ] 보안 업데이트 확인 +- [ ] 성능 지표 분석 + +--- + +## 🆘 롤백 계획 + +```bash +# 1. 이전 이미지로 복구 +docker-compose down +docker-compose pull # 이전 이미지 +docker-compose up -d + +# 2. 데이터베이스 백업에서 복구 +cat backup.sql | docker-compose exec -T web python manage.py dbshell + +# 3. 로그 확인 +docker-compose logs --tail=100 web +``` + +--- + +## 📚 참고 문서 + +- [Django Deployment Checklist](https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/) +- [Gunicorn Documentation](https://docs.gunicorn.org/) +- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/) +- 프로젝트 문서: + - `DEPLOYMENT_GUIDE.md` + - `API_USAGE_GUIDE.md` + - `CALLBACK_API_GUIDE.md` diff --git a/check_settings.py b/check_settings.py new file mode 100755 index 0000000..6f3dfbe --- /dev/null +++ b/check_settings.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +"""Django 설정 확인 스크립트""" + +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'executor.settings') +django.setup() + +from django.conf import settings + +print("="*80) +print("Django 설정 확인") +print("="*80) +print() + +print("기본 설정:") +print(f" DEBUG: {settings.DEBUG}") +print(f" SECRET_KEY: {'***' + settings.SECRET_KEY[-10:] if len(settings.SECRET_KEY) > 10 else '***'}") +print(f" ALLOWED_HOSTS: {settings.ALLOWED_HOSTS}") +print() + +print("데이터베이스:") +print(f" ENGINE: {settings.DATABASES['default']['ENGINE']}") +print(f" NAME: {settings.DATABASES['default']['NAME']}") +print() + +print("정적 파일:") +print(f" STATIC_URL: {settings.STATIC_URL}") +print(f" STATIC_ROOT: {settings.STATIC_ROOT}") +print(f" STATIC_ROOT 존재: {os.path.exists(settings.STATIC_ROOT)}") +print() + +print("미디어 파일:") +print(f" MEDIA_URL: {settings.MEDIA_URL}") +print(f" MEDIA_ROOT: {settings.MEDIA_ROOT}") +print(f" MEDIA_ROOT 존재: {os.path.exists(settings.MEDIA_ROOT)}") +print() + +print("국제화:") +print(f" TIME_ZONE: {settings.TIME_ZONE}") +print(f" LANGUAGE_CODE: {settings.LANGUAGE_CODE}") +print() + +print("설치된 앱:") +for app in settings.INSTALLED_APPS: + print(f" - {app}") +print() + +print("="*80) +print("설정 확인 완료!") +print("="*80) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9cafa31..add7cdb 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,6 +2,13 @@ set -e echo "Starting Django application..." +echo "Environment: ${DJANGO_ENV:-development}" +echo "" + +# 정적/미디어 파일 디렉토리 생성 +echo "Creating directories..." +mkdir -p /app/staticfiles +mkdir -p /app/mediafiles # 데이터베이스 마이그레이션 echo "Running database migrations..." @@ -11,11 +18,13 @@ python manage.py migrate --noinput echo "Initializing strategies..." python manage.py init_strategies -# 정적 파일 수집 (production 환경에서만) -if [ "$DJANGO_ENV" = "production" ]; then - echo "Collecting static files..." - python manage.py collectstatic --noinput -fi +# 정적 파일 수집 +echo "Collecting static files..." +python manage.py collectstatic --noinput --clear + +echo "" +echo "Django application ready!" +echo "" # 전달된 명령어 실행 exec "$@" diff --git a/executor/settings.py b/executor/settings.py index 68fb2ac..5fc5449 100644 --- a/executor/settings.py +++ b/executor/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,12 +21,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-ma+k#^%b4oe-(bm7y=kswcbbm-i+d=#fft7t_tl0sk0$_4asop' +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-ma+k#^%b4oe-(bm7y=kswcbbm-i+d=#fft7t_tl0sk0$_4asop') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.environ.get('DJANGO_DEBUG', 'True') == 'True' -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',') if os.environ.get('DJANGO_ALLOWED_HOSTS') else ['*'] # Application definition @@ -106,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = os.environ.get('TIME_ZONE', 'UTC') USE_I18N = True @@ -116,7 +117,12 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = os.environ.get('STATIC_URL', '/static/') +STATIC_ROOT = os.environ.get('STATIC_ROOT', BASE_DIR / 'staticfiles') + +# Media files +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'mediafiles' # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field