skills/jkappers/agent-skills/react-dockerfile

react-dockerfile

SKILL.md

React Dockerfile Generator

Generate production-ready multi-stage Dockerfiles for React applications served with Nginx.

Workflow

  1. Determine build tool: Vite (default), Create React App, or Next.js static export
  2. Identify Node.js version from .nvmrc, package.json engines, or use Node 22 LTS
  3. Check for existing nginx configuration files
  4. 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 of dist/
  • Environment variables use REACT_APP_ prefix instead of 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 --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 nginx for 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: true in Vite config for Docker HMR
Weekly Installs
22
First Seen
Jan 24, 2026
Installed on
gemini-cli19
cursor19
opencode19
codex18
github-copilot16
claude-code16