Статические ресурсы 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()],
});