Bad Request (400): Django / Nginx / Gunicorn / Kubernetes

Hi, I have a Django project running on Kubernetes with Redis, Postgres, Celery, Flower, and Nginx. I deploy it using Minikube and Kubectl on my localhost. Everything looks fine; the pod logs appear good, but when I’m trying to tunnel the Nginx service, it returns a Bad Request (400). The logs, the project files, the dockerfile, and the different YAML files are attached.

Django pod logs:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, website
Running migrations:
  No migrations to apply.

138 static files copied to '/app/staticfiles'.
[2024-05-11 15:21:33 +0000] [9] [INFO] Starting gunicorn 22.0.0
[2024-05-11 15:21:33 +0000] [9] [INFO] Listening at: http://0.0.0.0:8000 (9)
[2024-05-11 15:21:33 +0000] [9] [INFO] Using worker: sync
[2024-05-11 15:21:33 +0000] [10] [INFO] Booting worker with pid: 10

Nginx pod logs:

Nginx pod logs:
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2024/05/14 19:15:32 [notice] 1#1: using the "epoll" event method
2024/05/14 19:15:32 [notice] 1#1: nginx/1.25.5
2024/05/14 19:15:32 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) 
2024/05/14 19:15:32 [notice] 1#1: OS: Linux 6.6.16-linuxkit
2024/05/14 19:15:32 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/05/14 19:15:32 [notice] 1#1: start worker processes
2024/05/14 19:15:32 [notice] 1#1: start worker process 20
2024/05/14 19:15:32 [notice] 1#1: start worker process 21
2024/05/14 19:15:32 [notice] 1#1: start worker process 22
2024/05/14 19:15:32 [notice] 1#1: start worker process 23
2024/05/14 19:15:32 [notice] 1#1: start worker process 24
2024/05/14 19:15:32 [notice] 1#1: start worker process 25
2024/05/14 19:15:32 [notice] 1#1: start worker process 26
2024/05/14 19:15:32 [notice] 1#1: start worker process 27
10.244.0.1 - - [14/May/2024:19:15:43 +0000] "GET / HTTP/1.1" 400 154 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "-"
10.244.0.1 - - [14/May/2024:19:15:43 +0000] "GET /favicon.ico HTTP/1.1" 400 154 "http://127.0.0.1:58187/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "-"

Project setting.py:

"""
Django settings for premier_league_predictions project.

Generated by 'django-admin startproject' using Django 4.2.2.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

import os
import environ
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Create environment object
env = environ.Env(
    # set casting, default value
    DEBUG=(bool, False)
)

# Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# False if not in os.environ because of casting above
DEBUG = env("DEBUG")

# Raises Django's ImproperlyConfigured exception if SECRET_KEY not in os.environ
SECRET_KEY = env("SECRET_KEY")

ALLOWED_HOSTS = ["localhost", "127.0.0.1"]

# Application definition

INSTALLED_APPS = [
    "website",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "premier_league_predictions.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "premier_league_predictions.wsgi.application"

# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases


DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql_psycopg2",
        "NAME": env("DB_NAME"),
        "USER": env("DB_USER"),
        "PASSWORD": env("DB_PASSWORD"),
        "HOST": env("DB_HOST"),
        "PORT": env("DB_PORT"),
    }
}

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]

# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

STATICFILES_DIRS = [
    BASE_DIR / "static",
]

MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

# Retrieve Redis connection details from environment variables
REDIS_URL = f"redis://{env('REDIS_HOST')}:{env('REDIS_PORT')}/"

# Django cache settings
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": REDIS_URL,
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    }
}

# Configure Django session engine to use Redis for session storage
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

# Cache time to live is 15 minutes.
CACHE_TTL = 60 * 15

# Celery Configuration
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL

Project urls.py:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("website.urls")),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Project wsgi.py:

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "premier_league_predictions.settings")
application = get_wsgi_application()

Dockerfile:

FROM python:3.11.8-slim
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 8000

Django deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: django
spec:
  replicas: 1
  selector:
    matchLabels:
      app: django
  template:
    metadata:
      labels:
        app: django
    spec:
      containers:
        - name: django
          image: ***/***:latest
          command: ["/bin/sh", "-c"]
          args:
            - >
              python manage.py migrate &&
              python manage.py collectstatic --no-input &&
              gunicorn premier_league_predictions.wsgi:application --bind 0.0.0.0:8000
          ports:
            - containerPort: 8000
          env:
            - name: DEBUG
              valueFrom:
                configMapKeyRef:
                  name: django-cm
                  key: debug

            - name: SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: django-credentials
                  key: key

            - name: DB_NAME
              valueFrom:
                configMapKeyRef:
                  name: postgres-cm
                  key: name

            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: user

            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: password

            - name: DB_HOST
              valueFrom:
                configMapKeyRef:
                  name: django-cm
                  key: db_host

            - name: DB_PORT
              valueFrom:
                configMapKeyRef:
                  name: django-cm
                  key: db_port

            - name: REDIS_HOST
              valueFrom:
                configMapKeyRef:
                  name: django-cm
                  key: redis_host

            - name: REDIS_PORT
              valueFrom:
                configMapKeyRef:
                  name: django-cm
                  key: redis_port
          volumeMounts:
            - name: django-staticfiles
              mountPath: /data/staticfiles
            - name: django-media
              mountPath: /data/media
      imagePullSecrets:
        - name: dockerhub-credentials
      volumes:
        - name: django-staticfiles
          persistentVolumeClaim:
            claimName: django-staticfiles-pvc
        - name: django-media
          persistentVolumeClaim:
            claimName: django-media-pvc

Django pv.yaml:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: django-staticfiles-pv
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1Gi
  hostPath:
    path: /data/django-staticfiles-pv

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: django-media-pv
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 1Gi
  hostPath:
    path: /data/django-media-pv

Django pvc.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: django-staticfiles-pvc
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeName: django-staticfiles-pv

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: django-media-pvc
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeName: django-media-pv

Django service.yaml:

kind: Service
apiVersion: v1
metadata:
  name: django-service
spec:
  selector:
    app: django
  ports:
    - port: 8000
      targetPort: 8000

Nginx deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d
            - name: django-staticfiles
              mountPath: "/data/staticfiles"
            - name: django-media
              mountPath: "/data/media"
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-cm
        - name: django-staticfiles
          persistentVolumeClaim:
            claimName: django-staticfiles-pvc
        - name: django-media
          persistentVolumeClaim:
            claimName: django-media-pvc

Nginx config-map.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-cm
data:
  default.conf: |
    server {
        listen 80;
        server_name _;

        location / {
            proxy_pass http://django-service:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }

Nginx service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: nginx

II would be very grateful for help in accessing the application using the URL. I’ve tried a lot, but I feel that I lack knowledge

Thanks!