Skip to content

Dockerfile #170

@mwarman

Description

@mwarman

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;
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions