Статические ресурсы django-vite обслуживаются, но не загружаются при развертывании Nginx

Я развертываю простой проект Django на локальной виртуальной машине Ubuntu server под управлением Docker (3 контейнера, Postgres, nginx и Django). В проекте используется много HTMX и DaisyUI. В моей среде разработки они работали хорошо, когда их обслуживал сервер разработки Bun и использовался django-vite, но теперь в prod все работает отлично, за исключением статических ресурсов, сгенерированных django-vite. Самым странным является то, что файлы доставляются клиентам, но загружаются некорректно (приложение выполняет рендеринг, но отображаются только статические ресурсы, собранные Django, например значки). Если я проверю вкладку "Сеть" в моем devtools, я смогу увидеть, что файлы django-vite обрабатываются). Есть идеи, что может быть причиной этого?

Вот мой файл 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()],
    });

Вот мой файл 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;
            }
        }
    }

Вот соответствующие настройки на 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,
        }
    }

Вот мой файл 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"]

И, наконец, вот мой 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:

The problem ended up being the way my dockerfile was configured. Before bundling static assets I was only copying the static directory and a couple config files, but since tailwind v4 doesn't use the traditional configuration, and instead just scans the files looking for classes (as far as I understand) it wasn't finding anything because templates where not being copied. Simply copying the entire project directory (and some other minor tweaks to the vite.config.mjs) fixed the problem.

Updated dockerfile:

# STAGE 1: Base build stage
FROM python:3.13-slim AS builder
RUN mkdir /app
WORKDIR /app
 
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 
 
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

RUN useradd -m -r appuser && \
    mkdir -p /app/src/staticfiles /app/src/assets && \
    chown -R appuser:appuser /app
USER appuser

COPY --chown=appuser:appuser . .

RUN npm config set strict-ssl false && \
    npm install && \
    npm run build
 
# STAGE 3: Production stage
FROM python:3.13-slim
WORKDIR /app

RUN useradd -m -r appuser && \
    mkdir -p /app/src/staticfiles /app/src/assets && \
    chown -R appuser:appuser /app

USER appuser

COPY --chown=appuser:appuser . .
 
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/

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

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 
 
EXPOSE 8000

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

RUN sed -i 's/\r$//' /app/entrypoint.prod.sh && \
    chmod +x /app/entrypoint.prod.sh
 
CMD ["/app/entrypoint.prod.sh"]

Updated 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/assets"),
    emptyOutDir: false,
    rollupOptions: {
      input: {
        main: "./src/static/js/main.js",
      },
      output: {
        entryFileNames: "assets/[name]-[hash].js",
        chunkFileNames: "assets/[name]-[hash].js",
        assetFileNames: "assets/[name]-[hash].[ext]",
      },
    },
  },
  plugins: [tailwindcss()],
});
Вернуться на верх