React Dockerfile Generator
Generate production-ready multi-stage Dockerfiles for React applications served with Nginx.
Workflow
- •Determine build tool: Vite (default), Create React App, or Next.js static export
- •Identify Node.js version from
.nvmrc,package.jsonengines, or use Node 22 LTS - •Check for existing nginx configuration files
- •Choose optimization: standard, non-root (recommended), or runtime env vars
Key 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.
Image Selection Guide
| Scenario | Runtime Image | Compressed Size |
|---|---|---|
| Standard Nginx | nginx:stable-alpine | ~45 MB |
| Non-root (recommended) | nginx:stable-alpine + USER | ~45 MB |
| With Brotli compression | fholzer/nginx-brotli:latest | ~55 MB |
Standard Pattern
# 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 --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Required nginx.conf
Always create nginx.conf with SPA routing, caching, and security headers:
server {
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;
}
}
Non-Root Pattern (Recommended)
For production security, run Nginx as non-root on port 8080:
# 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 --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
Required nginx-main.conf for non-root operation:
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;
}
Update nginx.conf to listen on port 8080:
server {
listen 8080;
# ... rest of config
}
Build-Time Environment Variables (Vite)
Vite embeds environment variables at build time. Pass them via build arguments:
# 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 --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Build with:
docker build \ --build-arg VITE_API_URL=https://api.example.com \ --build-arg VITE_APP_TITLE="My App" \ -t myapp:prod .
Important: All Vite environment variables must be prefixed with VITE_.
Runtime Environment Variables (Advanced)
For "build once, deploy anywhere" workflows, inject environment variables at container startup:
Step 1: Create public/config.js.template:
window.__ENV__ = {
VITE_API_URL: "__VITE_API_URL__",
VITE_FEATURE_FLAG: "__VITE_FEATURE_FLAG__"
};
Step 2: Create 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;"
Step 3: Update Dockerfile:
FROM nginx:stable-alpine AS production RUN apk add --no-cache gettext COPY --from=builder /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"]
Step 4: Access in React app:
const apiUrl = window.__ENV__?.VITE_API_URL || import.meta.env.VITE_API_URL;
Development Stage
Add a development stage for local dev with hot reload:
# 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 --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Build targets:
# 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 Configuration for Docker HMR
Configure vite.config.ts for hot module replacement in Docker:
import { 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,
},
},
});
Note: usePolling: true increases CPU usage but is required for reliable file change detection in Docker.
Memory Optimization for Large Builds
Node.js defaults to 512MB memory, which may be insufficient for large Vite builds:
FROM 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 build
Cache Optimization
Use BuildKit cache mount for npm packages:
RUN --mount=type=cache,target=/root/.npm \
npm ci
Complete Production Example
# syntax=docker/dockerfile:1
ARG NODE_VERSION=22
# Stage 1: Dependencies
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
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 --from=deps /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 --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html
USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
Required .dockerignore
Always create .dockerignore:
node_modules npm-debug.log* dist build .git .gitignore *.md .env .env.* .vscode .idea coverage *.test.* *.spec.* __tests__ Dockerfile* docker-compose* .dockerignore
Create React App Adjustments
For Create React App (CRA) projects:
- •Build output is in
build/instead ofdist/ - •Environment variables use
REACT_APP_prefix instead ofVITE_
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 --from=builder /app/build /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Verification Checklist
- •Node.js Alpine image for build stage, Nginx Alpine for production
- •Include nginx.conf with SPA routing (
try_files $uri $uri/ /index.html) - •Copy build output:
dist/for Vite,build/for CRA - •Use
USER nginxfor non-root execution (listen on 8080) - •Cache hashed assets (js/css) with long expiration
- •Never cache index.html (entry point must be fresh)
- •Include .dockerignore to exclude node_modules
- •Consider memory limits for large builds (
NODE_OPTIONS) - •Use
usePolling: truein Vite config for Docker HMR