“Works on my machine” is the sentence Docker exists to kill. For a front-end developer it is not about Kubernetes, but about two things: a reproducible build and an environment a new teammate starts with one command.
A multi-stage Dockerfile
The first stage builds the app on a Node image. The second takes only the finished dist folder and serves it with nginx. The final image contains neither Node nor node_modules — it weighs tens of megabytes instead of hundreds.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
Line order matters. We copy only package*.json first and install dependencies — Docker caches that layer, so changing just your code does not reinstall everything.
.dockerignore
Without it your local node_modules ends up in the image, the build takes longer and may differ from the one on CI.
node_modules
dist
.git
.env*
Compose for local work
For development we do not rebuild the image on every change. We mount the code as a volume and run the Vite dev server in a container. The second volume, /app/node_modules, keeps the container’s dependencies separate from the ones on your disk.
services:
web:
image: node:22-alpine
working_dir: /app
command: sh -c "npm ci && npm run dev -- --host"
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules
Three everyday commands
| Command | What it does |
|---|---|
docker compose up | starts the dev environment |
docker build -t app . | builds the production image |
docker run -p 8080:80 app | serves the build on localhost:8080 |
Watch out with SPAs Default nginx returns 404 for router paths after a refresh. Add your own config with try_files $uri /index.html;.
That is all. With these two files the build on CI, on your machine and on a colleague’s brand-new laptop gives the same result.
