Skip to content

Dockerizing a Turborepo Vite React App

In this blog, we will learn how to Dockerize a Vite + React application inside a Turborepo.

Instead of setting up Turborepo from scratch, we will use one of the official Turborepo examples and focus entirely on Dockerization best practices.

You can find all official examples in the Turborepo repository:

For this tutorial, we will use the vite-react example.


Creating the Project

You can create the example project using any package manager.

Terminal window
# Using pnpm (Recommended)
pnpm dlx create-turbo@latest --example vite-react
# Using npm
npx create-turbo@latest --example vite-react
# Using yarn
yarn dlx create-turbo@latest --example vite-react
# Using bun
bunx create-turbo@latest --example vite-react

In this tutorial, we will use pnpm, since it is the recommended package manager by the Turborepo team.


Project Structure

After creating the project, you will get a folder structure similar to this:

├── apps/
│ ├── web/ # Vite + React application
│ └── docs/ # Optional second app/docs
├── packages/
│ ├── ui/ # Shared React component library
│ ├── eslint-config/ # Shared ESLint configuration
│ └── typescript-config/ # Shared TypeScript configuration
├── package.json
└── turbo.json

Since we want to Dockerize the React application, we will create a Dockerfile inside:

apps/web/

Dockerfile

Create a Dockerfile inside apps/web and paste the following code:

# ------------------------
# Base
# ------------------------
ARG NODE_VERSION=24.13.1
FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
# ------------------------
# Prepare
# ------------------------
FROM base AS prepare
RUN corepack enable
COPY . .
RUN pnpm dlx turbo prune web --docker
# ------------------------
# Builder
# ------------------------
FROM base AS builder
RUN corepack enable
COPY --from=prepare /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=prepare /app/out/full/ .
RUN pnpm turbo build --filter=web
# ------------------------
# Runner
# ------------------------
FROM nginx:alpine AS runner
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Understanding the Dockerfile

Now let’s go through each section of the Dockerfile and understand why it is structured this way.


Base Stage

ARG NODE_VERSION=24.13.1
FROM node:${NODE_VERSION}-slim AS base

Here we define the Node.js version as a reusable argument.

We are using the slim image variant instead of alpine.

Why slim instead of alpine?

Approximate image sizes:

  • slim → ~75 MB
  • alpine → ~55 MB

Although Alpine images are smaller, they can create compatibility issues for Node.js applications.

slim uses glibc

The slim image uses the standard GNU C library (glibc), which is compatible with most npm packages and native binaries.

Packages like:

  • bcrypt
  • sharp
  • canvas
  • node-gyp
  • database drivers

usually work without additional setup.

Alpine uses musl libc

Alpine Linux uses musl libc instead of glibc.

Because of this, some native Node.js packages may fail to install or compile correctly.

For production applications, slim is often the safer and more reliable choice.


Prepare Stage

FROM base AS prepare
RUN corepack enable
COPY . .
RUN pnpm dlx turbo prune web --docker

This is the first stage of our multi-stage Docker build.

What happens here?

1. Enable Corepack

RUN corepack enable

This enables package managers like pnpm.

2. Copy the Repository

COPY . .

This copies the entire monorepo into the Docker container.

3. Prune the Workspace

RUN pnpm dlx turbo prune web --docker

This is a Turborepo-specific optimization.

Instead of copying the entire monorepo into later stages, Turborepo generates a pruned workspace containing only the files needed for the web application.

The --docker flag improves Docker layer caching.


Difference Between turbo prune and turbo prune --docker

Standard turbo prune

my-turborepo/
└── out/
├── apps/
│ └── web/
│ ├── src/
│ ├── package.json
│ └── vite.config.ts
├── packages/
│ └── ui/
│ ├── src/
│ └── package.json
├── package.json
├── pnpm-lock.yaml
└── turbo.json

Everything is placed together in a single output directory.


turbo prune --docker

my-turborepo/
└── out/
├── json/
│ ├── apps/
│ │ └── web/package.json
│ ├── packages/
│ │ └── ui/package.json
│ ├── package.json
│ └── pnpm-lock.yaml
└── full/
├── apps/
│ └── web/
│ ├── src/
│ ├── package.json
│ └── vite.config.ts
├── packages/
│ └── ui/
│ ├── src/
│ └── package.json
├── package.json
└── turbo.json

With the --docker flag:

  • Dependency files are separated into the json folder
  • Full source code is placed in the full folder

This significantly improves Docker caching because dependency files change less frequently than application source code.

As a result:

  • pnpm install layers are reused more often
  • Docker builds become much faster

Builder Stage

FROM base AS builder
RUN corepack enable
COPY --from=prepare /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=prepare /app/out/full/ .
RUN pnpm turbo build --filter=web

This stage installs dependencies and builds the application.


Install Dependencies

RUN pnpm install --frozen-lockfile

The --frozen-lockfile flag ensures that dependency versions exactly match the lockfile.

This is similar to:

Terminal window
npm ci

in npm.

Benefits include:

  • deterministic installs
  • reproducible builds
  • preventing accidental dependency updates

Build the Application

RUN pnpm turbo build --filter=web

This builds only the web application instead of the entire monorepo.


Runner Stage

FROM nginx:alpine AS runner

Since this is a static React SPA built with Vite, we do not need a Node.js server in production.

Instead, we use Nginx to serve the generated static files.


Copy the Build Output

COPY --from=builder /app/apps/web/dist /usr/share/nginx/html

This copies the Vite build output into Nginx’s default public directory.


Expose Port 80

EXPOSE 80

Nginx serves traffic on port 80.


Start Nginx

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

Normally, Nginx runs as a background daemon.

However, Docker containers require the main process to stay in the foreground.

The following option prevents Nginx from detaching:

daemon off;

Without this, the container would immediately exit.


Conclusion

We successfully Dockerized a Vite + React application inside a Turborepo using:

  • multi-stage Docker builds
  • Turborepo pruning
  • Docker layer caching optimizations
  • Nginx for serving static files

This setup provides:

  • smaller production images
  • faster Docker builds
  • improved caching
  • cleaner deployments

CODE Reference: Code GitHub

In the next article, we will look into setting up CI/CD for this Dockerized Turborepo application.