Skip to content

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.yaml that 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:

Dockerfile
# 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:

.dockerignore
node_modules
dist
.git
*.log
.env*.local
.DS_Store

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:

compose.yaml
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:

Start the development service
docker compose up --build

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:

Install through the running service
docker compose exec frontend npm install zod
Install through a one-off container
docker compose run --rm frontend npm install zod

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:

Update the dependency volume
docker compose run --rm frontend npm ci

Stop or reset the environment

Stop the container at the end of the day:

Stop the development environment
docker compose down

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:

Build the Vite application
docker compose run --rm frontend npm run build

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 Dockerfile sets the Linux environment and system packages.
  • package-lock.json fixes 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