Writing
Microservices Frontend Projects with Docker

A little story from my own experience: how do you manage more than one frontend project when they need to live side by side? Maybe a marketing site built with Vue, a dashboard built with React, and one API behind them - each owned by different people, each deployed on its own schedule. Docker is a neat answer. Here is the approach in its simplest form:
- there are 2 or more frontend projects
- each project gets dockerized
- each container serves its build with its own nginx config
- the containers connect through a Docker network
I will walk through each step with a working setup.
Why Docker for a frontend at all?
Fair question - a built frontend is just static files. Dockerizing it buys you three things: parity (the exact same nginx image runs on your laptop and on the server), isolation (each app pins its own Node version for the build step, so upgrading one project never breaks the other), and one deploy story (the same docker compose up works whether the app is Vue, React or Next.js).
The honest 2026 caveat: if your frontend is purely static, a CDN/edge host (Vercel, Netlify, Cloudflare Pages) is usually less work. The Docker approach earns its keep when you are self-hosting, running several apps on one VM, or you need the web server in the middle - which is exactly the scenario below.
Step 1: Dockerize each frontend
The pattern for every app is a multi-stage build: stage one builds with Node, stage two copies only the static output into a tiny nginx image. The final image never contains your source or node_modules.
Dockerfile (works for any Vite app - Vue, React, Svelte):
# stage 1 - build
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# stage 2 - serve
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
nginx.conf - the part people forget is the SPA fallback, so deep links like /settings/profile do not 404 on refresh:
server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /assets/ {
root /usr/share/nginx/html;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Step 2: Wire the projects together with Compose
docker-compose.yml at the repo root defines both apps and - the "microservices" part - their shared network:
services:
marketing:
build: ./marketing-vue-app
ports:
- "8081:80"
dashboard:
build: ./dashboard-react-app
ports:
- "8082:80"
api:
image: my-api:latest
expose:
- "3000"
networks:
default:
name: frontend-net
Compose puts every service on frontend-net automatically. That network is what makes the services talk to each other by service name: the dashboard calls the API at http://api:3000, not http://localhost:3000. Only marketing and dashboard publish ports to your machine; api stays internal to the network (expose vs ports).
Step 3 (optional): one nginx in front of them all
Two open ports are fine for development, but production usually wants a single entry point - one domain, path-based routing:
server {
listen 80;
server_name example.com;
location /dashboard/ {
proxy_pass http://dashboard:8082/;
}
location / {
proxy_pass http://marketing:8081/;
}
}
Drop this into another nginx container (or your existing reverse proxy) and it forwards to the containers over the same Docker network. Watch the trailing slashes in proxy_pass - they control whether the path prefix is stripped, and getting it wrong is the classic "my app loads but assets 404" bug.
Run it all:
docker compose up --build -d
When not to do this
To keep the story honest: if your frontend is static and you control nothing else, skip the containers entirely and put it on a CDN host - you get global caching, previews per pull request, and zero servers to patch. Docker for frontends shines when apps share a VM with other services, when compliance or data residency rules keep you on your own infrastructure, or when the nginx layer itself is part of the product (routing, auth gates, legacy rewrites).
That's the whole approach - each frontend is a small self-contained service, glued together by a network instead of a monorepo build. Thanks for reading!