Describe the story
Create a Dockerfile which may be optionally used to package this application into a Docker image. While running React applications within containers is not the optimal approach, some companies choose to operate all applications as containers for operational simplicity.
Acceptance criteria
GIVEN the project may be bundled as a Docker image
WHEN the Docker image is created
THEN a multi-stage approach is used to optimize the final image
AND the final image uses nginx to serve the React application
Additional context
Example Dockerfile:
# --- Stage 1: Build Environment ---
FROM node:20-alpine AS builder
WORKDIR /app
# Best Practice: Copy dependency files first to leverage build cache
COPY package*.json ./
RUN npm ci --silent
# Copy source and build
COPY . .
RUN npm run build
# --- Stage 2: Production Environment ---
FROM nginx:stable-alpine
# Best Practice: Run as a non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Best Practice: Copy only the production-ready build artifacts
# Note: For Vite apps, use /app/dist instead of /app/build
COPY --from=builder /app/dist /usr/share/nginx/html
# Optional: Add custom Nginx config for React Router support
# COPY --from=builder /app/etc/nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Example nginx configuration:
# =============================================================================
# nginx.conf — React SPA | Docker | HTTP only (SSL terminates upstream)
# =============================================================================
worker_processes auto; # Scale to available CPU cores
worker_rlimit_nofile 65535; # Match or exceed OS open-file limit
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
multi_accept on; # Accept as many connections as possible
}
http {
# -------------------------------------------------------------------------
# MIME types & defaults
# -------------------------------------------------------------------------
include /etc/nginx/mime.types;
default_type application/octet-stream;
# -------------------------------------------------------------------------
# Logging
# -------------------------------------------------------------------------
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# -------------------------------------------------------------------------
# Performance
# -------------------------------------------------------------------------
sendfile on; # Efficient static file transfer
tcp_nopush on; # Send headers in one packet
tcp_nodelay on; # Disable Nagle for keep-alive connections
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off; # Hide nginx version from responses
# -------------------------------------------------------------------------
# Compression
# -------------------------------------------------------------------------
gzip on;
gzip_vary on; # Vary: Accept-Encoding for CDN/proxies
gzip_proxied any;
gzip_comp_level 6; # Balance between CPU and compression ratio
gzip_min_length 256; # Don't compress tiny responses
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/xml+rss
image/svg+xml
font/woff
font/woff2;
# -------------------------------------------------------------------------
# Server block
# -------------------------------------------------------------------------
server {
listen 80;
server_name _; # Catch-all; set your domain name here
root /usr/share/nginx/html; # Where `npm run build` output is copied
index index.html;
# ---------------------------------------------------------------------
# Security headers
# (Adjust CSP to match your app's actual requirements)
# ---------------------------------------------------------------------
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;
# If you know your app never needs to be embedded cross-origin:
# add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
# ---------------------------------------------------------------------
# Trusted proxy — honour X-Forwarded-For from your load balancer/ingress
# Replace with the actual IP range of your upstream proxy.
# ---------------------------------------------------------------------
# set_real_ip_from 10.0.0.0/8;
# real_ip_header X-Forwarded-For;
# ---------------------------------------------------------------------
# React SPA — client-side routing fallback
# All paths that don't match a real file fall through to index.html
# so React Router (or similar) can handle them.
# ---------------------------------------------------------------------
location / {
try_files $uri $uri/ /index.html;
}
# ---------------------------------------------------------------------
# Hashed static assets — cache aggressively
# Create React App / Vite emit filenames like main.abc123.js
# These are safe to cache for a very long time.
# ---------------------------------------------------------------------
location ~* \.(?:js|css|woff2?|ttf|eot|otf|svg|png|jpg|jpeg|gif|ico|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off; # Reduce log noise for static assets
}
# ---------------------------------------------------------------------
# index.html — must NOT be cached so users always get the latest shell
# ---------------------------------------------------------------------
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
}
# ---------------------------------------------------------------------
# Health check — lightweight endpoint for container orchestration
# ---------------------------------------------------------------------
location = /healthz {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
# ---------------------------------------------------------------------
# Block hidden files (.git, .env, .htaccess, etc.)
# ---------------------------------------------------------------------
location ~ /\. {
deny all;
}
}
}
Describe the story
Create a
Dockerfilewhich may be optionally used to package this application into a Docker image. While running React applications within containers is not the optimal approach, some companies choose to operate all applications as containers for operational simplicity.Acceptance criteria
GIVEN the project may be bundled as a Docker image
WHEN the Docker image is created
THEN a multi-stage approach is used to optimize the final image
AND the final image uses
nginxto serve the React applicationAdditional context
Example Dockerfile:
Example nginx configuration: