Deploying a React App with Docker using Nginx

Published on Mar 9, 2025 | Reading time: 3 min


Deploying React applications in production demands efficient resource utilization, consistent environments, and optimized static asset delivery. This guide details a Docker-based strategy using multi-stage builds to separate compilation and runtime environments, ensuring minimal final image size and adherence to production best practices.


Dockerfile Architecture & Technical Workflow

# Build stage
FROM node:alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --frozen-lockfile
COPY . .
RUN npm run build

# Run stage
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html

RUN echo 'server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }' > /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Stage 1: Build Phase (Node.js Compilation)

  1. Base Image:

    • node:alpine provides a minimal Node.js runtime with Alpine Linux (musl libc, ~5MB size), reducing attack surface and build time.
  2. Dependency Resolution:

    • COPY package.json package-lock.json ./ isolates dependency files to leverage Docker layer caching.
    • npm install --frozen-lockfile enforces strict version resolution from package-lock.json, ensuring deterministic builds.
  3. Source Compilation:

    • COPY . . transfers the entire React source code into the container.
    • npm run build triggers the React build script, generating optimized static assets (HTML, JS, CSS) in the /app/build directory.

Stage 2: Runtime Phase (Nginx Server)

  1. Base Image:

    • nginx:alpine offers a lightweight Nginx server (~23MB) with Alpine Linux, optimized for serving static content.
  2. Artifact Deployment:

    • COPY --from=builder /app/build /usr/share/nginx/html transfers compiled assets from the builder stage to Nginx’s default web root.
  3. Nginx Configuration:

    • The RUN echo 'server { ... }' > /etc/nginx/conf.d/default.conf command defines a custom server block:
      • listen 80: Binds to port 80 for HTTP traffic.
      • root /usr/share/nginx/html: Specifies the static asset directory.
      • try_files $uri $uri/ /index.html: Enables client-side routing for SPAs by falling back to index.html on 404 errors.
  4. Container Orchestration:

    • EXPOSE 80: Declares the container’s listening port.
    • CMD ["nginx", "-g", "daemon off;"]: Starts Nginx in foreground mode, a requirement for Docker containers.

Technical Rationale for Nginx

  1. Static Asset Delivery:

    • Nginx uses an event-driven, non-blocking architecture, efficiently serving static files with low memory overhead.
    • Built-in gzip compression and caching headers enhance performance for JS/CSS assets.
  2. Routing Handling:

    • The try_files directive ensures React Router or similar libraries manage client-side routing without server-side configuration.
  3. Security:

    • Alpine Linux’s minimal footprint reduces vulnerability exposure.
    • Nginx provides robust security headers and rate-limiting capabilities.

Build & Deployment Commands

  1. Image Creation:

    docker build -t react-app .
    • -t react-app: Tags the image for identification.
    • .: Builds using the Dockerfile in the current context.
  2. Container Execution:

    docker run -p 80:80 react-app
    • -p 80:80: Binds the host’s port 80 to the container’s port 80.

Key Benefits of Multi-Stage Builds

  1. Size Reduction:

    • Final image excludes Node.js, npm, and build dependencies, reducing size by ~90% compared to single-stage builds.
  2. Security Isolation:

    • Build tools (e.g., compilers, npm) are absent in the runtime image, minimizing exploit risks.
  3. Deterministic Builds:

    • --frozen-lockfile ensures dependency versions remain consistent across environments.

Conclusion

This implementation leverages Docker’s multi-stage build system to isolate development and production environments, combining Node.js’s build capabilities with Nginx’s optimized static asset delivery. The result is a secure, lightweight, and performant container ideal for scalable React deployments in cloud-native ecosystems.