How to Dockerize Your React App – With Multi-Stage Build

Welcome back, devs! 👋

If you’ve followed our Docker journey so far — from Intro to Docker to running your first container to Dockerizing a Node.js backend — you're ready for something more real-world.

In this blog, we will Dockerize React frontend app built using Vite. But this isn’t just another "Hello Docker" tutorial. No skipping over errors. You’ll see where things break and more importantly, learn how to fix them like a real developer.

By the end of this, you’ll be able to Dockerize any frontend app with confidence.

Excited Adorable GIF - Excited Adorable AGT - Discover & Share GIFs

Let's dive in.

🛠️ Step 1: Create a New React App using Vite

First create a new folder:

mkdir react_app && cd react_app

Now, create React app using Vite:

npm create vite@latest .

Choose:

  • React

  • JavaScript or TypeScript (your choice)

Then install the dependencies:

npm install

And start the app:

npm run dev

Visit http://localhost:5173 and Ta-da! You have a running Vite + React app.

✍️ Step 2: Let’s Update Something

Go to src/App.jsx and change the heading:

<h1>Vite + React</h1>

to

<h1>This is a Dockerized Vite + React App!</h1>

Save the file and you should see the updated heading.

📦 Step 3: Writing the Dockerfile

Now comes the fun part: Dockerizing.

Create a new file in the root directory Dockerfile:

touch Dockerfile

Here’s the Dockerfile we’ll use:

# Step 1:  Use Node.js base image
FROM node:22-alpine

# Step 2: Set working directory
WORKDIR /app

# Step 3: Copy package files and install dependencies
COPY package*.json ./
RUN npm install

# Step 4: Copy rest of the app
COPY . .

# Step 5: Build the app
RUN npm run build

# Step 6: Command to run the app
CMD ["npm", "run", "preview"]

💡 Why this order? Docker caches each layer. If your dependencies have not changed, Step 3: npm install will be reused — super fast builds!

🧱 Step 4: Build the Docker Image

Let’s build the image:

docker build -t react_app:v1 .

Then run it:

docker run -p 5173:5173 react_app:v1

🚫 Step 5: Uh-oh, It Doesn't Work!

When you visit localhost:5173 or localhost:4173, you’ll see:

This is because Vite's preview server only listens to localhost by default, not the Docker container's external network and also when you run npm run preview, it serves the app on port 4173, not 5173.

🧑‍🔧 Step 6: Fix Vite Config for Docker

Edit vite.config.js:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vite.dev/config/
export default defineConfig({
    plugins: [react()],
    // Fix for Docker
    server: {
        host: "0.0.0.0", // Listen on all IPs (including Docker)
        port: 3000, // Change to port 3000 for development mode
    },
    preview: {
        host: "0.0.0.0", // Listen on all IPs (including Docker)
        port: 3000, // Change to port 3000 for preview or production mode
    },
});

This tells Vite to listen to all IPs (including Docker) and also changes the port to 3000 for both development and production preview.

♻️ Step 7: Rebuild the Image

Since we updated the app:

docker build -t react_app:v2 .
docker run -p 3000:3000 react_app:v2

Now visit http://localhost:3000 and you should see:

Simple Dockerfile link: Dockerfile

Now let’s take this one step further.

Multi-Stage Docker Build

We have Dockerized our React app, but we can do better by using multi-stage builds.

Because multi-stage builds are built for one thing only: optimization.

You want:

  • ✅ Smaller image size

  • ✅ Cleaner final containers (no dev dependencies)

  • ✅ Faster rebuilds with reusable layers

Wow Dwayne Johnson GIF - Wow Dwayne Johnson Jumanji - Discover & Share GIFs

Multi-stage builds are simple once you see

Let's use our React app to build in one stage and serve in another — production style.

Step 1: Build Stage

Rename your current Dockerfile to keep it as a backup:

mv Dockerfile Dockerfile.old

Now create a new Dockerfile:

# --- Stage 1: Build the Vite React App ---

# Use Node.js base image for building
FROM node:22-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm install

# Copy rest of the app
COPY . .

# Build the app
RUN npm run build

This builds the static production files into dist/.

🔥 Step 2: Serve with NGINX (Production Stage)

# --- Stage 2: Serve with NGINX ---
FROM nginx:alpine AS production

# Remove default nginx static files
RUN rm -rf /usr/share/nginx/html/*

# Copy built app from builder
COPY --from=builder /app/dist /usr/share/nginx/html

# Expose port 80
EXPOSE 80

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

💡 --from=builder pulls files from the first stage only. No need to copy from your dev machine!

Step 3: Build and Run the Multi-Stage Docker Image

docker build -t react_app_multistage .

Then run it:

docker run -p 3000:80 react_app_multistage

Go to http://localhost:3000 and boom! You’re now running a fully production optimized Docker container.

Multi-stage docker file link: Dockerfile

📉 Compare Image Sizes

Image TypeSize
Simple Docker image~279 MB
Multi-stage Docker image~28–50 MB

Yes — multi-stage builds shrink your image size drastically!

Congratulations Gif - GIFcen

If you have reached this far, you now know how to:

  • ✅ Dockerize a React app using Vite

  • ✅ Fix common issues like Vite’s localhost binding

  • ✅ Use multi-stage builds to optimize your Docker images

🧼 Bonus: Use .dockerignore

Create a .dockerignore file:

node_modules
.dockerignore
Dockerfile
*.log
dist

Why? To prevent Docker from copying unnecessary files during build. Smaller context = faster builds.

Final Thoughts on Multi-Stage Builds

A multi-stage build is just:

FROM ... AS builder
# build here

FROM ... AS final
# copy from builder

It’s a simple way to separate build and runtime environments.

This becomes really important when your frontend or backend apps scale huge node_modules, development dependencies (like: TypeScript, Babel), test tools (like: Jest), source maps all of that can be kept out of production.

This keeps your production images clean, small, and fast.

If you found this helpful, please share it with your fellow developers.

Thanks for reading! 🙌

Happy learning! 🐳

0
Subscribe to my newsletter

Read articles from Om Prakash Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Om Prakash Pandey
Om Prakash Pandey