Dockerfile Builder for React Static Site (Nginx)

Build and serve a React app in a single Nginx-based Dockerfile

Generate a multi-stage Dockerfile that builds a React, Vite, or Create React App project with Node and serves the static output through a minimal Nginx image, plus a matching nginx.conf with SPA routing. Runs entirely in your browser. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

Why serve a React app with Nginx instead of Node?

A built React app is just static HTML, CSS, and JavaScript — there is no server-side rendering to do. Nginx serves static files faster and with a far smaller memory footprint than a Node process, and the runtime image contains no Node, no npm, and no source code.

A Dockerfile builder for static React sites that emits a multi-stage Dockerfile: one stage builds your app with Node, and a second tiny stage serves the compiled output with Nginx. It also generates a matching nginx.conf with sensible caching headers and optional single-page-app routing.

How it works

The build stage uses node:<version>-alpine. It copies package.json and your lockfile first and runs a clean install (npm ci, yarn install --frozen-lockfile, or pnpm install --frozen-lockfile) so dependencies are cached independently of your source. It then copies the rest of the project and runs your build script, producing static assets in dist, build, or out.

The runtime stage starts from nginx:alpine, copies only the built assets into /usr/share/nginx/html, and copies the generated nginx.conf over the default config. That config sets a one-year immutable cache on hashed assets and, when SPA fallback is on, uses try_files $uri $uri/ /index.html; so any unmatched route is handed to your client-side router instead of returning a 404.

The generated files

Dockerfile:

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost/ || exit 1

nginx.conf (with SPA fallback):

server {
    listen 80;

    root /usr/share/nginx/html;
    index index.html;

    # Cache hashed assets for one year
    location ~* \.(js|css|png|jpg|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Never cache index.html
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }
}

The immutable directive tells browsers the file will never change (because the filename includes a hash), so they do not even send a conditional request. index.html is served without caching so users always get the latest version pointing to those hashed assets.

SPA routing in detail

When a user navigates directly to /dashboard/settings in a React Router app, Nginx receives a request for that path. Without the try_files … /index.html fallback, it looks for a file at that path, finds nothing, and returns a 404. The fallback tells Nginx to serve index.html for any unmatched path, handing routing control to React. Disable this for multi-page sites where each path corresponds to a real file.

Tips and notes

  • Add a .dockerignore with node_modules, .git, and dist so the build context is small and the dependency cache is not invalidated by stale local builds.
  • If your app reads runtime configuration (API base URL, feature flags), remember the bundle is built at image build time — environment variables baked in then cannot change without rebuilding. One pattern: serve a config.js from Nginx that the entrypoint script writes at container start, and load it via a <script> tag in index.html.
  • Hashed asset filenames (app.4f3c1.js) are safe to cache for a year because a new build produces a new filename — never long-cache index.html itself or you will break deployments for returning users.