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