django-vite static assets being served but not loading on an Nginx deployment

hello everyone, I’ve been fighting with this problem for 4 whole days to no avail. I’m trying to deploy a simple project on a local ubuntu server VM running docker. I have three containers, Postgres, nginx and Django. I used a lot of HTMX and DaisyUI, and on my dev environment they worked really nicely being served by a Bun dev server and using django-vite, but now that I’m deploying, everything works perfectly fine except for the static assets generated by django-vite. The weirdest part is the files are being delivered to the clients but not loading correctly (the app renders, but only the static assets collected by Django, like icons, are being displayed. If I check the network tab on my devtools I see the django-vite files are being served). Any idea what could be causing this?

Here is my vite.config.mjs

import { defineConfig } from "vite";

import { resolve } from "path";

import tailwindcss from "@tailwindcss/vite";



export default defineConfig({

  base: "/static/",

  build: {

    manifest: "manifest.json",

    outDir: resolve("./src/staticfiles"),

    emptyOutDir: false,

    write: true,

    rollupOptions: {

      input: {

        main: "./src/static/js/main.js",

      },

      output: {

        entryFileNames: "js/\[name\].\[hash\].js",

        chunkFileNames: "js/chunks/\[name\].\[hash\].js",

        assetFileNames: "assets/\[name\].\[hash\]\[extname\]",

      },

    },

  },

  plugins: \[tailwindcss()\],

});

Here is my nginx.conf

worker_processes 1;



events {

    worker_connections 1024;

}



http {

    include mime.types;

    default_type application/octet-stream;



    \# sendfile on;

    \# tcp_nopush on;

    \# tcp_nodelay on;

    \# keepalive_timeout 65;



    upstream django {

        server django-web:8000;

        keepalive 32;

    }



    \# Map HTTPS from X-Forwarded-Proto

    map $http_x_forwarded_proto $forwarded_scheme {

        default $scheme;

        https https;

    }



    \# Map for determining if request is secure

    map $forwarded_scheme $is_secure {

        https 1;

        default 0;

    }



    server {

        listen 80;

        listen \[::\]:80;

        server_name mydomain.com;



        add_header Strict-Transport-Security "max-age=31536000" always;

        add_header X-Content-Type-Options "nosniff" always;

        add_header X-Frame-Options "DENY" always;

        add_header Cross-Origin-Opener-Policy "same-origin" always;

        add_header Cross-Origin-Embedder-Policy "require-corp" always;

        add_header Cross-Origin-Resource-Policy "same-site" always;

        add_header Referrer-Policy "same-origin" always;



        real_ip_header X-Forwarded-For;

        real_ip_recursive on;



        location /static/ {

            alias /app/src/staticfiles/;

            autoindex off;

            sendfile on;

            sendfile_max_chunk 1m;

            tcp_nopush on;

            tcp_nodelay on;



            types {

                application/javascript js mjs;

                text/css css;

                image/x-icon ico;

                image/webp webp;

            }

            

            \# Security headers

            add_header X-Content-Type-Options "nosniff" always;

            add_header X-Frame-Options "DENY" always;

            add_header Cross-Origin-Opener-Policy "same-origin" always;

            add_header Cross-Origin-Embedder-Policy "require-corp" always;

            add_header Cross-Origin-Resource-Policy "same-site" always;

            add_header Referrer-Policy "same-origin" always;

            

            \# This was a desperate attempt to get the files to load

            add_header Access-Control-Allow-Origin "\*" always;

            add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;

            add_header Access-Control-Allow-Headers "\*" always;

            add_header Cache-Control "public, max-age=31536000" always;

        }



        \# Handles all other requests

        location / {

            proxy_set_header Host $http_host;

            proxy_set_header X-Real-IP $remote_addr;

            proxy_pass http://django;

        }

    }

}

Here are the relevant settings on settings.py

DEBUG = False



ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1").split(",")

CSRF_TRUSTED_ORIGINS = os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "http://127.0.0.1").split(",")



SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

SESSION_COOKIE_SECURE = True

CSRF_COOKIE_SECURE = True

SECURE_BROWSER_XSS_FILTER = True

SECURE_CONTENT_TYPE_NOSNIFF = True

X_FRAME_OPTIONS = "DENY"

SECURE_HSTS_SECONDS = 31536000

SECURE_HSTS_INCLUDE_SUBDOMAINS = True

SECURE_HSTS_PRELOAD = True



INSTALLED_APPS = \[

    "django.contrib.admin",

    "django.contrib.auth",

    "django.contrib.contenttypes",

    "django.contrib.sessions",

    "django.contrib.messages",

    "django.contrib.staticfiles",

    \# Third-party apps

    "django_vite",

    \# my apps

    ...

\]



WSGI_APPLICATION = "myproject.wsgi.application"



STATIC_URL = "static/"

MEDIA_URL = "media/"



STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_ROOT = BASE_DIR / "media"



STATICFILES_DIRS = \[BASE_DIR / "static"\]



DJANGO_VITE = {

    "default": {

        "dev_mode": True if os.getenv("DJANGO_VITE_DEV_MODE") == "True" else False,

        "manifest_path": BASE_DIR / "staticfiles" / "manifest.json",

        "dev_server_port": 5173,

    }

}

Here is my Dockerfile

\# STAGE 1: Base build stage

FROM python:3.13-slim AS builder

 

\# Create the app directory

RUN mkdir /app

 

\# Set the working directory

WORKDIR /app

 

\# Set environment variables to optimize Python

ENV PYTHONDONTWRITEBYTECODE=1

ENV PYTHONUNBUFFERED=1 

 

\# Install dependencies first for caching benefit

RUN pip install --upgrade pip 

COPY requirements.txt /app/ 

RUN pip install --no-cache-dir -r requirements.txt



\# STAGE 2: node build stage

FROM node:current-slim AS node-builder



WORKDIR /app



\# Copy package.json first for better cache utilization

COPY package.json ./



\# Install production dependencies only with specific platform

RUN npm config set strict-ssl false

RUN npm install



\# Copy the rest of the build files

COPY tailwind.config.js ./

COPY vite.config.mjs ./

COPY src/static ./src/static



\# Build

RUN npm run build



\# Verify build output exists

RUN ls -la /app/src/staticfiles || true

 

\# STAGE 3: Production stage

FROM python:3.13-slim

 

RUN useradd -m -r appuser && \\

   mkdir /app && \\

   chown -R appuser /app

 

\# Copy the Python dependencies from the builder stage

COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/

COPY --from=builder /usr/local/bin/ /usr/local/bin/

 

\# Set the working directory

WORKDIR /app



\# create static folder

RUN mkdir -p /app/src/staticfiles && \\

    chown -R appuser:appuser /app/src/staticfiles



\# Copy the Node.js build artifacts from node-builder stage

COPY --from=node-builder --chown=appuser:appuser /app/src/staticfiles /app/src/staticfiles



\# Copy application code

COPY --chown=appuser:appuser . .

 

\# Set environment variables to optimize Python

ENV PYTHONDONTWRITEBYTECODE=1

ENV PYTHONUNBUFFERED=1 

 

\# Switch to non-root user

USER appuser

 

\# Expose the application port

EXPOSE 8000 



\# Make entry file executable

RUN chmod +x  /app/entrypoint.prod.sh

 

\# Start the application using Gunicorn

CMD \["/app/entrypoint.prod.sh"\]

And lastly here is my docker-compose.yml

services:

  db:

    image: postgres:17

    ports:

      - "5432:5432"

    volumes:

      - postgres_data:/var/lib/postgresql/data

    env_file:

      - .env



  django-web:

    build: .

    container_name: django-docker

    depends_on:

      - db

    volumes:

      - static_volume:/app/src/staticfiles

    env_file:

      - .env



  frontend-proxy:

    image: nginx:latest

    ports:

      - "80:80"

    volumes:

      - ./nginx.conf:/etc/nginx/nginx.conf:ro

      - static_volume:/app/src/staticfiles:ro

    depends_on:

      - django-web

volumes:

  postgres_data:

  static_volume: