Browse Source

feat(prod): configure Gunicorn, WhiteNoise static file serving, and Nginx reverse proxy

master
PouyaKhajavi 8 hours ago
parent
commit
91e8a4d423
  1. 22
      .env.example
  2. 18
      backend/config/settings.py
  3. 17
      docker-compose.yml
  4. 6
      entrypoint.sh
  5. 16
      frontend/Dockerfile
  6. 43
      frontend/nginx.conf
  7. 3
      requirements.txt

22
.env.example

@ -1,23 +1,23 @@
# Django Settings
DJANGO_SECRET_KEY=django-insecure-your-secret-key-here
DJANGO_DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,*
# Django Production Settings
DJANGO_SECRET_KEY=generate-a-strong-random-secret-key-here
DJANGO_DEBUG=False
ALLOWED_HOSTS=*
CORS_ALLOW_ALL_ORIGINS=True
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,http://YOUR_SERVER_IP
# Database Configuration (PostgreSQL)
POSTGRES_DB=divar_crawler
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=localhost
POSTGRES_PASSWORD=your_secure_postgres_password
POSTGRES_HOST=db
POSTGRES_PORT=5432
# Redis & Celery
REDIS_URL=redis://localhost:6379/0
REDIS_URL=redis://redis:6379/0
# LLM Config (OpenAI / Gemini)
OPENAI_API_KEY=your-openai-api-key-here
# Optional: LLM_MODEL_NAME=gpt-4o-mini
# LLM Config (OpenAI / OpenRouter)
OPENAI_API_KEY=sk-or-v1-your-openrouter-key
OPENAI_MODEL=google/gemma-4-26b-a4b-it:free
# Telegram Config
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here

18
backend/config/settings.py

@ -57,6 +57,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -147,19 +148,32 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR.parent / 'frontend' / 'dist',
]
] if (BASE_DIR.parent / 'frontend' / 'dist').exists() else []
STORAGES = {
"default": {
"BACKEND": "django.db.models.fields.files.Storage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Default primary key field type
# https://docs.djangoproject.com/en/6.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# CORS Settings
# CORS & CSRF Settings
CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True').lower() in ('true', '1', 't')
if not CORS_ALLOW_ALL_ORIGINS:
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in os.getenv('CORS_ALLOWED_ORIGINS', '').split(',') if origin.strip()]
csrf_origins = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost,http://127.0.0.1,http://localhost:5173,http://localhost:8000')
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in csrf_origins.split(',') if origin.strip()]
# Django REST Framework Settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [

17
docker-compose.yml

@ -2,7 +2,7 @@ services:
db:
image: postgres:16-alpine
container_name: divar_postgres
restart: always
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-divar_crawler}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
@ -20,6 +20,7 @@ services:
redis:
image: redis:7-alpine
container_name: divar_redis
restart: unless-stopped
ports:
- "6379:6379"
healthcheck:
@ -33,7 +34,8 @@ services:
context: .
dockerfile: Dockerfile.backend
container_name: divar_backend
command: python backend/manage.py runserver 0.0.0.0:8000
restart: unless-stopped
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4
ports:
- "8000:8000"
environment:
@ -55,6 +57,7 @@ services:
context: .
dockerfile: Dockerfile.backend
container_name: divar_celery
restart: unless-stopped
command: celery -A config worker --loglevel=info -P gevent -c 20
environment:
- REDIS_URL=redis://redis:6379/0
@ -75,6 +78,7 @@ services:
context: .
dockerfile: Dockerfile.backend
container_name: divar_celery_beat
restart: unless-stopped
command: celery -A config beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler
environment:
- REDIS_URL=redis://redis:6379/0
@ -95,13 +99,10 @@ services:
context: .
dockerfile: frontend/Dockerfile
container_name: divar_frontend
restart: unless-stopped
ports:
- "5173:5173"
environment:
- VITE_BACKEND_URL=http://backend:8000
volumes:
- ./frontend:/app/frontend
- /app/frontend/node_modules
- "80:80"
- "5173:80"
depends_on:
- backend

6
entrypoint.sh

@ -25,10 +25,12 @@ except Exception:
echo "PostgreSQL database is ready!"
fi
# Automatically run Django migrations before starting backend server
if [ "$1" = "python" ] && [ "$2" = "backend/manage.py" ] && [ "$3" = "runserver" ]; then
# Automatically run Django migrations and collectstatic before starting backend server
if [ "$1" = "gunicorn" ] || ([ "$1" = "python" ] && [ "$2" = "backend/manage.py" ]); then
echo "Running Django migrations..."
python backend/manage.py migrate --noinput
echo "Collecting static files..."
python backend/manage.py collectstatic --noinput --clear || true
fi
exec "$@"

16
frontend/Dockerfile

@ -1,12 +1,20 @@
FROM node:22-slim
# Stage 1: Build Vite frontend
FROM node:22-alpine AS build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
RUN npm ci || npm install
COPY frontend ./
RUN npm run build
EXPOSE 5173
# Stage 2: Production Nginx server
FROM nginx:alpine
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
COPY --from=build /app/frontend/dist /usr/share/nginx/html
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

43
frontend/nginx.conf

@ -0,0 +1,43 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression for fast asset loading
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Frontend SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Proxy Django API requests
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy Django Admin interface
location /admin/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy Django static files (admin assets, etc.)
location /static/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

3
requirements.txt

@ -11,4 +11,5 @@ openai>=2.53.0,<2.54
pydantic>=2.13.4,<2.14
python-dotenv>=1.2.2,<1.3
gevent>=24.2.0,<25.0
gunicorn>=22.0.0,<23.0
whitenoise>=6.6.0,<7.0
Loading…
Cancel
Save