Dockerfile для React (Vite) и статической раздачи
Фронтенд (Vite/React) в продакшене - это статические файлы (HTML, JS, CSS). Собираем их в Node-контейнере, раздаём через nginx. Тот же multi-stage, что и для Go, но со своей спецификой.
Базовый Dockerfile
# ---------- build ----------
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---------- serve ----------
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Stage 1 (build) - устанавливает зависимости и собирает dist/. Stage 2 (serve) - копирует готовую статику в nginx.
SPA routing: проблема 404
React Router обрабатывает маршруты на клиенте. Но если пользователь обновит страницу на /course/docker/why-docker, nginx ищет файл по этому пути - не находит - отдаёт 404.
Решение - настроить nginx отдавать index.html для всех маршрутов:
# nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# SPA fallback - все маршруты → index.html
location / {
try_files $uri $uri/ /index.html;
}
# Кэширование статики
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Не кэшировать index.html (чтобы обновления доезжали)
location = /index.html {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
Обновлённый Dockerfile:
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Переменные окружения в runtime
Vite подставляет import.meta.env.VITE_* переменные при сборке. После npm run build они вшиты в JS-бандл. Проблема: разные окружения (staging, production) требуют разных значений.
Способ 1: Build args (простой)
FROM node:20-alpine AS build
ARG VITE_API_URL=http://localhost:8080
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN VITE_API_URL=${VITE_API_URL} npm run build
# Для staging
docker build --build-arg VITE_API_URL=https://staging.example.com -t frontend:staging .
# Для production
docker build --build-arg VITE_API_URL=https://api.example.com -t frontend:prod .
Минус: нужно пересобирать образ для каждого окружения.
Способ 2: Runtime-подстановка (гибкий)
Создаём entrypoint-скрипт, который подставляет переменные при запуске контейнера:
#!/bin/sh
# docker-entrypoint.sh
# Подставляем переменные окружения в JS-бандл
for file in /usr/share/nginx/html/assets/*.js; do
sed -i "s|__VITE_API_URL__|${VITE_API_URL}|g" "$file"
done
exec nginx -g "daemon off;"
В коде React используй плейсхолдер:
const API_URL = import.meta.env.VITE_API_URL || '__VITE_API_URL__';
Оптимизация размера образа
# Базовый nginx:alpine ~40 МБ
# + dist (React/Vite) ~5-15 МБ
# Итого: ~45-55 МБ
Советы по уменьшению:
- .dockerignore - исключи
node_modules,.git,dist npm ci --production=false- devDependencies нужны для сборки, но не попадут в runtime (другой stage)- Vite code splitting -
build.rollupOptions.output.manualChunks- меньше JS = меньше образ
Gzip и Brotli
nginx может отдавать сжатые файлы. Добавь в nginx.conf:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
gzip_comp_level 6;
Или пересобери статику с pre-compression (vite-plugin-compression) - nginx отдаст готовые .gz файлы без нагрузки на CPU.
Полный production Dockerfile
# ---------- build ----------
FROM node:20-alpine AS build
ARG VITE_API_URL=http://localhost:8080
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN VITE_API_URL=${VITE_API_URL} npm run build
# ---------- serve ----------
FROM nginx:alpine
# Кастомный конфиг для SPA + кэширование + gzip
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Статика из build stage
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Мини-задание
- Собери Dockerfile для React/Vite приложения с multi-stage
- Создай
nginx.confсtry_filesдля SPA - Проверь: открой вложенный маршрут, обнови страницу - должен работать, а не 404
- Посмотри размер образа:
docker images - Попробуй
--build-arg VITE_API_URL=https://api.example.com
Итог
- Multi-stage для фронта: Node (сборка) → nginx (раздача). Образ ~45-55 МБ.
nginx.confсtry_files $uri /index.html- обязателен для SPA (React Router).- Кэширование: статика на 1 год (
immutable), index.html - без кэша (no-store). - Переменные: build args (просто) или runtime-подстановка (гибко).
npm ciвместоnpm install- детерминированная установка по lock-файлу.
Типичная ошибка
Забыть nginx.conf с try_files. Результат: SPA работает при переходе по ссылкам (React Router), но при обновлении страницы (F5) на /course/docker/lesson-3 - 404. Это ловится только при тестировании в контейнере, потому что vite dev обрабатывает все маршруты сам.
Вторая ошибка - npm install вместо npm ci в Dockerfile. npm install может обновить lock-файл, и сборка в CI даст другой результат, чем локально.
Мини-практика (10-15 минут)
Создай минимальное React-приложение и контейнеризируй его:
# 1. Создай проект
npm create vite@latest /tmp/docker-frontend - --template react
cd /tmp/docker-frontend
npm install
# 2. Напиши Dockerfile (multi-stage: node → nginx)
# 3. Напиши nginx.conf с try_files
# 4. Собери и запусти
docker build -t frontend-test .
docker run --rm -p 8080:80 frontend-test
# 5. Проверь SPA routing
# Открой http://localhost:8080
# Перейди по внутренней ссылке
# Обнови страницу (F5) - не должно быть 404