css and js files not loading after deploying django

I’m trying to deploy django to kubernetes, but I’ve searched for many solutions from the internet and asked CHATGPT countless times, and nothing is working. It’s really my first time deploying a django project to a server, but I’m really at a loss as to what I should do. Here are my settings.py, dockerfile and yaml files.
Am I overlooking something?

.env

DEBUG=FALSE
SECRET_KEY=*******
BASE_URL=http://127.0.0.1:8080
ALLOWED_HOSTS=127.0.0.1,admin****.com
CORS_ALLOWED_ORIGINS=https://admin.****.com
......

setttings.py


import os
from pathlib import Path
import environ

env = environ.Env(
    DEBUG = (bool, False)
)

environ.Env.read_env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
environ.Env.read_env(os.path.join(BASE_DIR, 'main/.env'))

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

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURE_HSTS_SECONDS = 3600
# SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# SECURE_HSTS_PRELOAD = True
# SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG')

ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # custom apps
    'user',
    # 3th plugins
    'rest_framework',
    'oauth2_provider',
    'corsheaders',
]

AUTH_USER_MODEL='user.users'


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

ROOT_URLCONF = 'main.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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 = 'main.wsgi.application'

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

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        '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/5.0/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/5.0/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/5.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

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

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


# =========================== custom setting ===========================
BASE_URL=env('BASE_URL')

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
      'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
      'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

OAUTH2_PROVIDER = {
    # this is the list of available scopes
    'SCOPES': {
        'read': 'Read scope',
        'write': 'Write scope', 
        'groups': 'Access to your groups',
        'introspection': 'Introspect token scope',
    }
}

LOGIN_URL = '/admin/login/'

CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '').split(',')

Containerfile

# Use the official Python image as the base image
ARG ARCH=
FROM ${ARCH}python:3.12-slim as builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --default-timeout=100 --no-cache-dir -r requirements.txt
COPY . /app
RUN mkdir -p /app/static
VOLUME /app/static
RUN python manage.py collectstatic --noinput

FROM ${ARCH}python:3.12-slim
WORKDIR /app
COPY --from=builder /app .
COPY --from=builder /app/static .
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

EXPOSE 8080

ENV PYTHONUNBUFFERED 1

# Specify the command to run your Django app
CMD ["gunicorn", "--workers=3", "--bind=0.0.0.0:8080", "main.wsgi:application" ]

yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: staticfiles-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/app/static"
  storageClassName: do-block-storage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: staticfiles-pvc
spec:
  storageClassName: do-block-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
  volumeName: staticfiles-pv
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: auth-db-config
data:
  DB_NAME: auth
  DB_HOST: patroni.default.svc.cluster.local
  DB_PORT: "5432"
---
apiVersion: v1
kind: Secret
metadata:
  name: auth-db-secret
type: Opaque
data:
  DB_USER: ********
  DB_PASSWORD: *********=
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth
spec:
  replicas: 1
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      volumes:
      - name: staticfiles
        persistentVolumeClaim: 
          claimName: staticfiles-pvc
      containers:
        - name: auth
          image: # my private container registry
          env:
          - name: DB_NAME
            valueFrom:
              configMapKeyRef:
                name: auth-db-config
                key: DB_NAME
          - name: DB_HOST
            valueFrom:
              configMapKeyRef:
                name: auth-db-config
                key: DB_HOST
          - name: DB_PORT
            valueFrom:
              configMapKeyRef:
                name: auth-db-config
                key: DB_PORT
          - name: DB_USER
            valueFrom:
              secretKeyRef:
                name: auth-db-secret
                key: DB_USER
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: auth-db-secret
                key: DB_PASSWORD
          ports:
            - containerPort: 8080
          volumeMounts:
          - name: staticfiles
            mountPath: "/app/static"
      imagePullSecrets:
        - name: auth
---
apiVersion: v1
kind: Service
metadata:
  name: auth
spec:
  type: ClusterIP
  selector:
    app: auth
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080


I’m pretty sure static files like /app/static/admin/css/base.css exist on the server

root@auth-b85cfcc87-584kl:/app# ls /app/static/admin/css
autocomplete.css  changelists.css  dashboard.css  login.css        responsive.css      rtl.css  widgets.css
base.css          dark_mode.css    forms.css      nav_sidebar.css  responsive_rtl.css  vendor
root@auth-b85cfcc87-584kl:/app# 


What web server are you using to serve the pages? (nginx? apache?) What is its configuration for serving the static files?

Are the static files being collected into a data container that your web server has access to?

I just configured reverse proxy in kubernetes using ingres-nginx. There is no additional nginx deployment. I have no additional configurations left.
ingress-nginx.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    cert-manager.io/issuer: letsencrypt-nginx
    nginx.ingress.kubernetes.io/add-base-url: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - admin.*****.com
    secretName: letsencrypt-nginx
  rules:
  - host: admin.*****.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: auth
            port:
              number: 8080

But for django deployment, do I need additional configuration in kubernetes?

In the container, static files are in the /app/static directory, where they exist.

root@auth-d95558d64-ncmj4:/app# ls /app
main  manage.py  requirements.txt  static  user
root@auth-d95558d64-ncmj4:/app# ls /app/static/admin/css
autocomplete.css  changelists.css  dashboard.css  login.css        responsive.css      rtl.css  widgets.css
base.css          dark_mode.css    forms.css      nav_sidebar.css  responsive_rtl.css  vendor
root@auth-d95558d64-ncmj4:/app# 

What you see in your Django container is irrelevant. Django doesn’t serve your static files in production, that’s nginx’s job.

You need to use collectstatic to copy all your static files to a data volume that both Django and nginx have access to. Then, you configure your nginx container to map urls beginning with /static/ to whatever directory you have that data volume mapped to in the nginx container.

Note: I know docker reasonably well. I do not know kubernetes at all, so I wouldn’t know exactly how this needs to be configured in that environment.

I’m not really a pro at either docker or kubernetes. Is there a problem with my dockerfile? Because I suspect there is a problem with the dockerfile.

I don’t know kubernetes, so I don’t know if there’s anything different that would be done in a docker file for it or not.

But, we do not run collectstatic as part of the docker build process. We run collectstatic and migrate in the CMD script used to run uwsgi. That way, we’re sure that the static files get copied to the data volume shared by Django and nginx.

I’m trying to test it now using a shared volume. The dockerfile I’m currently using looks like this:

# Use the official Python image as the base image
ARG ARCH=
FROM ${ARCH}python:3.12-slim
ENV PYTHONUNBUFFERED 1
RUN set -ex \
    && apt-get update \
    && apt-get install -y --no-install-recommends \
        build-essential \
        libpq-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN python -m venv /env
ENV PATH="/env/bin:$PATH"
COPY ./requirements.txt /app/requirements.txt
RUN pip install --default-timeout=100 --no-cache-dir -r /app/requirements.txt
# RUN pip install --upgrade pip \
#     && pip install --no-cache-dir -r /app/requirements.txt

RUN runDeps="$( \
        scanelf --needed --nobanner --recursive /env \
        | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
        | sort -u \
        | xargs -r apt-cache depends \
        | awk '{print $2}' \
        | sort -u \
    )" \
    && apt-get install --no-install-recommends -y $runDeps \
    && apt-get remove --purge -y build-essential \
    && apt-get autoremove -y \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . /app

EXPOSE 8080

# Specify the command to run your Django app
CMD ["gunicorn", "--workers=3", "--bind=0.0.0.0:8080", "main.wsgi:application" ]

I suspect that the kubernetes one-click install of ingress-nginx doesn’t have read permissions on the shared volume, because I was able to successfully execute python manage.py collectstatic inside the django container and the files were successfully collected in the /app/static directory