Loading...
Loading...
Create optimized, secure multi-stage Dockerfiles for React applications (Vite, CRA, Next.js static). Use when (1) creating a new Dockerfile for a React project, (2) containerizing a React/Vite application, (3) optimizing an existing React Dockerfile, (4) setting up Docker for React with Nginx, or (5) user mentions React and Docker/container together.
npx skill4agent add jkappers/agent-skills react-dockerfile.nvmrcpackage.jsonKey Insight: Unlike server-side Node.js apps, React apps only need Node.js for building—the runtime is static files served by Nginx. This reduces image size from ~1GB to ~50MB.
| Scenario | Runtime Image | Compressed Size |
|---|---|---|
| Standard Nginx | | ~45 MB |
| Non-root (recommended) | | ~45 MB |
| With Brotli compression | | ~55 MB |
# syntax=docker/dockerfile:1
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS production
COPY /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]nginx.confserver {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Hide Nginx version
server_tokens off;
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Cache hashed assets forever (Vite generates unique hashes)
location ~* \.(?:css|js)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Cache static assets
location ~* \.(?:ico|gif|jpe?g|png|svg|woff2?|ttf|eot)$ {
expires 6M;
add_header Cache-Control "public, max-age=15552000";
}
# No cache for index.html (entry point must always be fresh)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml+rss application/atom+xml image/svg+xml;
# Deny hidden files
location ~ /\. {
deny all;
}
}# syntax=docker/dockerfile:1
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS production
# Create nginx directories with correct permissions
RUN mkdir -p /var/run/nginx && \
chown -R nginx:nginx /var/cache/nginx /var/run/nginx && \
chmod -R g+w /var/cache/nginx
# Copy nginx configs
COPY nginx-main.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy static files
COPY /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]worker_processes auto;
pid /var/run/nginx/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}server {
listen 8080;
# ... rest of config
}# syntax=docker/dockerfile:1
FROM node:22-alpine AS builder
WORKDIR /app
# Accept build arguments
ARG VITE_API_URL
ARG VITE_APP_TITLE
# Make available to Vite build
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_APP_TITLE=$VITE_APP_TITLE
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS production
COPY /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]docker build \
--build-arg VITE_API_URL=https://api.example.com \
--build-arg VITE_APP_TITLE="My App" \
-t myapp:prod .VITE_public/config.js.templatewindow.__ENV__ = {
VITE_API_URL: "__VITE_API_URL__",
VITE_FEATURE_FLAG: "__VITE_FEATURE_FLAG__"
};docker-entrypoint.sh#!/bin/sh
set -e
# Replace placeholders with actual environment variables
envsubst < /usr/share/nginx/html/config.js.template > /usr/share/nginx/html/config.js
# Start nginx
exec nginx -g "daemon off;"FROM nginx:stable-alpine AS production
RUN apk add --no-cache gettext
COPY /app/dist /usr/share/nginx/html
COPY public/config.js.template /usr/share/nginx/html/config.js.template
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]const apiUrl = window.__ENV__?.VITE_API_URL || import.meta.env.VITE_API_URL;# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS development
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS production
COPY /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# Development with hot reload
docker build --target development -t myapp:dev .
docker run -p 5173:5173 -v $(pwd)/src:/app/src myapp:dev
# Production
docker build --target production -t myapp:prod .vite.config.tsimport { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0', // Listen on all interfaces
port: 5173,
watch: {
usePolling: true, // Required for Docker file watching
},
hmr: {
host: 'localhost',
port: 5173,
},
},
});usePolling: trueFROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Increase Node.js memory limit for large builds
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run buildRUN \
npm ci# syntax=docker/dockerfile:1
ARG NODE_VERSION=22
# Stage 1: Dependencies
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
# Stage 2: Build
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /app
# Build arguments for Vite
ARG VITE_API_URL
ARG VITE_APP_VERSION
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_APP_VERSION=$VITE_APP_VERSION
ENV NODE_OPTIONS="--max-old-space-size=4096"
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production
FROM nginx:stable-alpine AS production
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.description="Production React application"
# Security: Create non-root setup
RUN mkdir -p /var/run/nginx && \
chown -R nginx:nginx /var/cache/nginx /var/run/nginx /usr/share/nginx/html && \
chmod -R g+w /var/cache/nginx
# Copy nginx configs
COPY nginx-main.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy static files
COPY /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080
HEALTHCHECK \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["nginx", "-g", "daemon off;"].dockerignorenode_modules
npm-debug.log*
dist
build
.git
.gitignore
*.md
.env
.env.*
.vscode
.idea
coverage
*.test.*
*.spec.*
__tests__
Dockerfile*
docker-compose*
.dockerignorebuild/dist/REACT_APP_VITE_FROM node:22-alpine AS builder
WORKDIR /app
ARG REACT_APP_API_URL
ENV REACT_APP_API_URL=$REACT_APP_API_URL
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS production
COPY /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]try_files $uri $uri/ /index.htmldist/build/USER nginxNODE_OPTIONSusePolling: true