A Practical Docker Workflow for Vite Across Multiple Machines
I write code across both macOS and Linux machines. In the past, switching computers frequently caused package errors, broken native bindings, and mismatched Node.js versions.
To fix this, I set up a workflow that keeps the Node.js runtime and dependencies inside Docker, while keeping source code and Git on the host machine. You edit code in your local IDE, and Docker runs Vite and manages node_modules inside an isolated Linux volume. When you clone the repository onto another computer, running Compose gives you the exact same working environment immediately.
Navigate this post
Split the project by responsibility
This setup separates source code, dependencies, and runtime configuration so each part has a single owner:
| Part | Location | Purpose |
|---|---|---|
| Source code & package files | Host project directory | Your editor and Git work with standard project files. |
| Installed npm packages | Docker-managed node_modules volume |
Linux packages stay isolated inside the Linux container. |
| Runtime instructions | Committed Dockerfile and compose.yaml |
Ensures every machine uses the same Node.js version, commands, ports, and mounts. |
Your editor writes directly to your local project folder. Docker reads those changes through a bind mount, while the node_modules volume stays local to Docker on each machine.
What this guide assumes
This setup is designed for a client-rendered Vite application using npm. The repository includes a committed package-lock.json, along with standard npm run dev and npm run build scripts. Server-side rendering, CI pipelines, and production web servers require their own configurations.
Here are the core Docker concepts used throughout this workflow:
- Image
-
A read-only blueprint built from a
Dockerfile. Here, it contains Node.js, installed packages, application code, and the startup command. - Container
-
A running instance of an image. Vite and Node.js run inside this process.
- Bind mount
-
A local host directory mapped directly inside the container. Saving a file in your IDE updates the file inside the container immediately.
- Volume
-
Storage managed directly by Docker that persists across container restarts. This setup uses a named volume for
node_modules. - Compose service
-
A configuration block in
compose.yamlthat defines how Docker builds, mounts, and runs the frontend container.
Create the development environment
Add Dockerfile, .dockerignore, and compose.yaml to the root of your project beside package.json.
Build the image from the lockfile
The Dockerfile installs dependencies strictly from package-lock.json:
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS development
WORKDIR /workspace
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
Copying package.json and package-lock.json before copying the rest of the project source code allows Docker to cache the installed dependencies. When you change source files, Docker reuses the existing dependency layer and skips reinstalling packages. If you edit package-lock.json, Docker invalidates the cache and runs npm ci again. The cache mount (--mount=type=cache,target=/root/.npm) keeps downloaded npm package archives available across builds to speed up downloads. For full details on caching, see the Docker build-cache optimization guide.
npm ci removes any existing local packages and installs exactly what is locked in package-lock.json, ensuring consistent installs across machines. If your team uses custom registry settings, commit a project .npmrc file alongside your code. See the npm ci documentation for configuration options.
This setup uses node:24-bookworm-slim. Check the Node.js release schedule when selecting your base image.
Choosing your base image version
A major version tag like node:24 automatically receives patch updates. If your project requires byte-for-byte reproducibility across all machines, pin an exact image digest instead.
Inside the container, Vite must bind to 0.0.0.0. By default, Vite binds to 127.0.0.1, which only accepts connections from inside the container. The --host 0.0.0.0 flag tells Vite to accept connections forwarded from your host browser.
Keep host-only files out of the image
Add a .dockerignore file to keep unnecessary local files out of your Docker build:
Excluding host node_modules prevents local macOS or Windows packages from being copied into the Linux container during image builds. See the Docker .dockerignore guide for pattern rules.
Describe the running service
compose.yaml sets up port forwarding and directory mounts:
services:
frontend:
build:
context: .
target: development
ports:
- "127.0.0.1:5173:5173"
volumes:
- .:/workspace
- node_modules:/workspace/node_modules
volumes:
node_modules:
Compose mounts the host project folder at /workspace, and then mounts the named volume node_modules at /workspace/node_modules. Because the volume mount is more specific, it shadows the nested folder and prevents host packages from overwriting the container's Linux packages.
See the running workspace
When you start the service, Docker builds the base image, starts the container, mounts your host source code, and attaches the dependency volume:
---
title: "How Compose assembles the Vite workspace"
---
flowchart TB
accTitle: How Compose assembles the Vite workspace
accDescr: Docker builds an image, Compose starts a container, then mounts the host project and a Docker volume for node modules. Vite serves the application to the host browser.
D["Dockerfile"] -->|"Builds"| I["Image with Node.js and npm packages"]
I -->|"Starts"| C["Development container"]
H["Host project"] -->|"Bind mounts at /workspace"| C
V["Docker volume"] -->|"Mounts at /workspace/node_modules"| C
C -->|"Runs Vite on port 5173"| B["Host browser"]
Docker creates the named volume on the first startup and populates it with the packages installed during the build. This volume persists when you stop or remove the container. For technical details on volume initialization, review the Docker volume mount documentation.
Use the workflow each day
Each computer manages its own local node_modules volume, while Git keeps package.json and package-lock.json synchronized across machines.
| Scenario | Command | What Happens |
|---|---|---|
| Fresh repository clone | docker compose up --build |
Builds the image, creates the local volume, and starts Vite. |
| Normal daily start | docker compose up |
Reuses the existing image and dependency volume. |
| Changes to Dockerfile | docker compose up --build |
Rebuilds the image while preserving the existing dependency volume. |
New dependencies after git pull |
docker compose run --rm frontend npm ci |
Updates the packages in the local volume to match the lockfile. |
| Upgraded Node.js base image | docker compose build && docker compose run --rm frontend npm ci |
Updates the base runtime image and rebuilds local packages. |
| Normal stop | docker compose down |
Stops the container while keeping the dependency volume intact. |
| Full dependency reset | docker compose down --volumes |
Deletes the dependency volume so the next start performs a clean install. |
Start the development service for a new clone:
Open http://127.0.0.1:5173/ in your browser. Any edits in src/ sync to the container via the bind mount, triggering Hot Module Replacement (HMR) in the browser.
Rebuilding images does not update existing volumes
Running docker compose up --build rebuilds the base image, but it leaves an existing named volume untouched. After pulling updates to package.json or package-lock.json, always run docker compose run --rm frontend npm ci to sync the volume.
Add dependencies inside Docker
Run package installations inside the container so binaries compile correctly for Linux:
Both commands update package.json and package-lock.json in your local project folder via the bind mount. Commit these updated files to Git.
Update dependencies after a pull
When another team member updates dependencies, pull the latest code, stop the service, and update the volume:
Stop or reset the environment
Stop the container at the end of the day:
Resetting removes installed packages
docker compose down --volumes permanently deletes the named node_modules volume. Use this command when you want a completely fresh install. The next startup will download and reinstall all dependencies.
Verify the production build
Run the Vite production build inside the Linux container:
The bind mount writes the compiled output to the dist/ directory on your host machine.
Note that while Vite compiles TypeScript to JavaScript during builds, it transpiles files individually without whole-program type-checking. Add a tsc --noEmit check in your package.json scripts and run it before deploying. For more details, review the Vite TypeScript guide.
Troubleshoot the right boundary
When troubleshooting, check the specific layer where the issue occurs:
| Symptom | Layer to Inspect | Action to Take |
|---|---|---|
| Browser cannot connect | Container networking | Confirm --host 0.0.0.0 is set, check the 5173:5173 port mapping, and ensure the host port is open. |
| File edits save, but HMR does not trigger | Filesystem events | Check the bind mount. If using WSL2, enable Vite file polling. |
| Pulled dependency is missing at runtime | Dependency volume | Run docker compose run --rm frontend npm ci. |
| npm creates files with root ownership | Host vs. container user IDs | Match the container user and group IDs with your host user ID. |
| Native package fails to install | Architecture / C library | Verify platform support for the package and check your base Node image. |
File watching can occasionally stall on Windows WSL2 setups. If changes aren't detected, enable polling in vite.config.ts by setting server.watch.usePolling: true. Polling checks the filesystem periodically, which increases CPU usage slightly. See the Vite server-watch documentation for options.
Understand Docker's boundary
This workflow standardizes Node.js versions, npm settings, and application dependencies, but underlying host systems still behave differently:
- The
Dockerfilesets the Linux environment and system packages. package-lock.jsonfixes the exact dependency tree.- Linux hosts share the host kernel directly with containers, while Docker Desktop on macOS and Windows runs containers inside a lightweight Linux virtual machine.
Native npm packages compile machine code specifically for the operating system where npm runs. A package compiled on macOS cannot execute inside a Linux container. Storing node_modules inside the Linux Docker volume ensures that all native binaries match the Linux environment running Vite. For more on container architecture, see the Docker containers and virtual machines guide.
To test production builds locally, use vite preview. Deploying the final build to production still requires your standard hosting setup for static assets, CDNs, reverse proxies, and caching. See the Vite static deployment guide for options.
Conclusion
Keeping your source code on the host, dependencies inside a Docker volume, and runtime settings in version control gives you the best of both worlds: native file editing in your local IDE alongside a consistent, isolated Node.js environment.
When you switch computers or pull team updates, running Compose gets you back to work immediately without environment drift.
References and further reading
Open the complete reference catalog
Primary Sources
- Docker Docs: What is a container?
- Docker Docs: Optimize cache usage in builds
- Docker Docs: Build context and .dockerignore files
- Docker Docs: Understanding Volumes
- npm Docs: npm ci command
- Node.js: Node.js Releases
- Vite Docs: Server Options
- Vite Docs: TypeScript Transpile Only
- Vite Docs: Deploying a Static Site
Related Site Guides
- Making the Most of Git Worktrees and Dev Containers – Extend environment isolation to parallel branches and development tasks.
- SPA, MPA, and Hybrid Navigation Architectures – How browser documents and navigation lifecycles work.
- From HTML to Pixels – How browsers turn code into pixels on screen.
- Choosing HTML, SVG, Canvas, or WebGL for Real-Time Graphics – Rendering approaches for dynamic dashboards.