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.
# Using pnpm (Recommended)pnpm dlx create-turbo@latest --example vite-react
# Using npmnpx create-turbo@latest --example vite-react
# Using yarnyarn dlx create-turbo@latest --example vite-react
# Using bunbunx create-turbo@latest --example vite-reactIn 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.jsonSince 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.1FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
# ------------------------# Prepare# ------------------------FROM base AS prepare
RUN corepack enableCOPY . .
RUN pnpm dlx turbo prune web --docker
# ------------------------# Builder# ------------------------FROM base AS builderRUN 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.1FROM node:${NODE_VERSION}-slim AS baseHere 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 MBalpine→ ~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:
bcryptsharpcanvasnode-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 enableCOPY . .
RUN pnpm dlx turbo prune web --dockerThis is the first stage of our multi-stage Docker build.
What happens here?
1. Enable Corepack
RUN corepack enableThis 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 --dockerThis 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.jsonEverything 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.jsonWith the --docker flag:
- Dependency files are separated into the
jsonfolder - Full source code is placed in the
fullfolder
This significantly improves Docker caching because dependency files change less frequently than application source code.
As a result:
pnpm installlayers are reused more often- Docker builds become much faster
Builder Stage
FROM base AS builderRUN corepack enable
COPY --from=prepare /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=prepare /app/out/full/ .
RUN pnpm turbo build --filter=webThis stage installs dependencies and builds the application.
Install Dependencies
RUN pnpm install --frozen-lockfileThe --frozen-lockfile flag ensures that dependency versions exactly match the lockfile.
This is similar to:
npm ciin npm.
Benefits include:
- deterministic installs
- reproducible builds
- preventing accidental dependency updates
Build the Application
RUN pnpm turbo build --filter=webThis builds only the web application instead of the entire monorepo.
Runner Stage
FROM nginx:alpine AS runnerSince 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/htmlThis copies the Vite build output into Nginx’s default public directory.
Expose Port 80
EXPOSE 80Nginx 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.