Deployment
Independent deployment patterns for micro-frontends including CI/CD pipelines, versioning, rollback, and environment strategies.
You are an expert in independent deployment patterns for micro-frontend architectures. You help teams set up CI/CD pipelines, versioning strategies, rollback mechanisms, and environment configurations that enable each micro-frontend to be deployed independently without coordinating with other teams. ## Key Points - Each micro-frontend has its own CI/CD pipeline. - Deploying one micro-frontend does not require rebuilding or redeploying others. - The shell dynamically discovers and loads the latest version of each micro-frontend at runtime. - Rollback of a single micro-frontend is possible without affecting others. 1. Hashed asset files (`catalog.a1b2c3.js`, `catalog.d4e5f6.css`). 2. A stable entry point (`remoteEntry.js`, `manifest.json`, or an import map entry) that points to the current hashed assets. 2. **Hash all assets, keep entry points stable** — content-hashed filenames enable aggressive caching. The entry point (remoteEntry.js, import map URL) is the only mutable reference. 3. **Short-cache entry points, long-cache assets** — the entry point should have a TTL of 30-60 seconds. Hashed assets should be cached for a year with `immutable`. 4. **Automate rollback** — store a history of deployed versions. Rollback should be a one-click operation that updates the entry point to the previous hash. 5. **Run integration smoke tests post-deploy** — after updating the import map, run a Playwright or Cypress test against the composed shell to verify the new version loads. 6. **Use environment promotion, not rebuild** — the same artifact (same JS bundle) should move from dev to staging to production. Only the import map entry changes per environment. 7. **Monitor each micro-frontend independently** — track bundle size, load time, error rate, and Core Web Vitals per micro-frontend. A regression in one should not be masked by averages. ## Quick Example ``` dev → staging → production ```
skilldb get micro-frontend-skills/DeploymentFull skill: 379 linesDeployment — Micro-Frontends
You are an expert in independent deployment patterns for micro-frontend architectures. You help teams set up CI/CD pipelines, versioning strategies, rollback mechanisms, and environment configurations that enable each micro-frontend to be deployed independently without coordinating with other teams.
Overview
Independent deployment is the primary operational benefit of micro-frontends. Each team can release on its own schedule, with its own pipeline, without waiting for other teams or a monolithic release train. However, achieving true independence requires careful design of the build, publish, and serving infrastructure.
Key requirements for independent deployment:
- Each micro-frontend has its own CI/CD pipeline.
- Deploying one micro-frontend does not require rebuilding or redeploying others.
- The shell dynamically discovers and loads the latest version of each micro-frontend at runtime.
- Rollback of a single micro-frontend is possible without affecting others.
Core Concepts
Build-Time vs Runtime Integration
| Build-Time | Runtime | |
|---|---|---|
| Integration point | npm install + webpack build | Browser fetches remote at page load |
| Deploy independence | No — shell must rebuild | Yes — deploy remote, shell picks it up |
| Version control | package.json lock | Import map, manifest, or Module Federation |
| Rollback | Redeploy shell with old dep version | Update pointer to old remote version |
Runtime integration is required for true independent deployment.
The Remote Entry / Manifest Pattern
Each micro-frontend build produces:
- Hashed asset files (
catalog.a1b2c3.js,catalog.d4e5f6.css). - A stable entry point (
remoteEntry.js,manifest.json, or an import map entry) that points to the current hashed assets.
The shell references only the stable entry point. When a team deploys, they update the entry point to reference new hashed assets. The shell's next page load picks up the change.
Environment Promotion
Micro-frontends follow the same promote-through-environments pattern as backend services:
dev → staging → production
Each environment has its own import map or manifest registry. Promoting a micro-frontend means updating the entry in the target environment's registry.
Implementation Patterns
CI/CD Pipeline per Micro-Frontend (GitHub Actions)
# .github/workflows/deploy.yml — in the catalog repo
name: Deploy Catalog MFE
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
- name: Upload to CDN (S3 + CloudFront)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
HASH=$(git rev-parse --short HEAD)
aws s3 sync dist/ "s3://mfe-assets/catalog/${HASH}/" \
--cache-control "public, max-age=31536000, immutable"
- name: Update import map
run: |
HASH=$(git rev-parse --short HEAD)
curl -X PATCH "${{ secrets.IMPORT_MAP_API }}/catalog" \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"url\": \"https://cdn.example.com/catalog/${HASH}/main.js\"}"
- name: Invalidate CDN cache for import map
run: |
aws cloudfront create-invalidation \
--distribution-id "${{ secrets.CF_DISTRIBUTION_ID }}" \
--paths "/importmap.json"
Import Map Deployer Service
// import-map-deployer — a small service that manages the live import map
// POST /api/map/:environment/:service
const express = require("express");
const { Storage } = require("@google-cloud/storage");
const app = express();
const storage = new Storage();
const bucket = storage.bucket("mfe-import-maps");
app.patch("/api/map/:env/:service", async (req, res) => {
const { env, service } = req.params;
const { url } = req.body;
// Load current import map
const file = bucket.file(`${env}/importmap.json`);
const [contents] = await file.download();
const importMap = JSON.parse(contents.toString());
// Store previous version for rollback
const previousUrl = importMap.imports[`@myorg/${service}`];
const historyFile = bucket.file(`${env}/history/${service}.json`);
const [historyContents] = await historyFile.download().catch(() => [Buffer.from("[]")]);
const history = JSON.parse(historyContents.toString());
history.push({ url: previousUrl, timestamp: new Date().toISOString() });
await historyFile.save(JSON.stringify(history));
// Update import map
importMap.imports[`@myorg/${service}`] = url;
await file.save(JSON.stringify(importMap, null, 2), {
contentType: "application/json",
metadata: { cacheControl: "public, max-age=30" }, // short TTL
});
res.json({ ok: true, service, url, previousUrl });
});
app.post("/api/rollback/:env/:service", async (req, res) => {
const { env, service } = req.params;
const historyFile = bucket.file(`${env}/history/${service}.json`);
const [contents] = await historyFile.download();
const history = JSON.parse(contents.toString());
const previous = history.pop();
if (!previous) return res.status(404).json({ error: "No previous version" });
// Recursively call the update endpoint
const response = await fetch(`http://localhost:3000/api/map/${env}/${service}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: previous.url }),
});
res.json({ ok: true, rolledBackTo: previous });
});
Import Map Structure
{
"imports": {
"@myorg/shell": "https://cdn.example.com/shell/a1b2c3d/main.js",
"@myorg/catalog": "https://cdn.example.com/catalog/e4f5g6h/main.js",
"@myorg/checkout": "https://cdn.example.com/checkout/i7j8k9l/main.js",
"@myorg/design-system": "https://cdn.example.com/design-system/v3.2.1/index.js",
"react": "https://cdn.jsdelivr.net/npm/react@18.2.0/+esm",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm"
}
}
Docker-Based Deployment with Nginx
# Dockerfile — each micro-frontend
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# nginx.conf — per micro-frontend
server {
listen 80;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /remoteEntry.js {
add_header Cache-Control "public, max-age=60";
}
location = /index.html {
add_header Cache-Control "no-cache";
}
}
Kubernetes Deployment with Independent Services
# k8s/catalog-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-mfe
labels:
app: catalog-mfe
spec:
replicas: 2
selector:
matchLabels:
app: catalog-mfe
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: catalog-mfe
spec:
containers:
- name: catalog-mfe
image: registry.example.com/catalog-mfe:${TAG}
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /remoteEntry.js
port: 80
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: catalog-mfe
spec:
selector:
app: catalog-mfe
ports:
- port: 80
targetPort: 80
Canary Deployment with Traffic Splitting
# Using Istio VirtualService for canary releases
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: catalog-mfe
spec:
hosts:
- catalog.mfe.example.com
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: catalog-mfe-canary
port:
number: 80
- route:
- destination:
host: catalog-mfe-stable
port:
number: 80
weight: 95
- destination:
host: catalog-mfe-canary
port:
number: 80
weight: 5
Smoke Tests Post-Deploy
// smoke-test.js — run after each micro-frontend deployment
import { test, expect } from "@playwright/test";
test("catalog micro-frontend loads in shell", async ({ page }) => {
await page.goto("https://staging.example.com/catalog");
// Verify the micro-frontend mounted
await expect(page.locator("[data-mfe='catalog']")).toBeVisible();
// Verify it renders content (not an error state)
await expect(page.locator(".product-list")).toBeVisible();
await expect(page.locator(".product-list .product-card")).toHaveCount.greaterThan(0);
});
test("checkout micro-frontend loads in shell", async ({ page }) => {
await page.goto("https://staging.example.com/checkout");
await expect(page.locator("[data-mfe='checkout']")).toBeVisible();
});
Asset Caching Strategy
# Caching rules summary:
# Hashed assets (JS, CSS, images) — immutable, long TTL
*.a1b2c3.js → Cache-Control: public, max-age=31536000, immutable
# Entry points (remoteEntry.js, manifest.json) — short TTL
remoteEntry.js → Cache-Control: public, max-age=60
manifest.json → Cache-Control: public, max-age=60
# Import map — very short TTL or no-cache
importmap.json → Cache-Control: public, max-age=30
# HTML shell — no cache (picks up latest import map)
index.html → Cache-Control: no-cache
Best Practices
- One repo, one pipeline, one deployable — each micro-frontend should be an independent repository (or at minimum an independent CI pipeline in a monorepo) that deploys without triggering other builds.
- Hash all assets, keep entry points stable — content-hashed filenames enable aggressive caching. The entry point (remoteEntry.js, import map URL) is the only mutable reference.
- Short-cache entry points, long-cache assets — the entry point should have a TTL of 30-60 seconds. Hashed assets should be cached for a year with
immutable. - Automate rollback — store a history of deployed versions. Rollback should be a one-click operation that updates the entry point to the previous hash.
- Run integration smoke tests post-deploy — after updating the import map, run a Playwright or Cypress test against the composed shell to verify the new version loads.
- Use environment promotion, not rebuild — the same artifact (same JS bundle) should move from dev to staging to production. Only the import map entry changes per environment.
- Monitor each micro-frontend independently — track bundle size, load time, error rate, and Core Web Vitals per micro-frontend. A regression in one should not be masked by averages.
Common Pitfalls
- CDN cache serving stale entry points — if
remoteEntry.jsis cached too long, users do not see the new deployment. Keep its TTL short or use cache invalidation on deploy. - Coordinated deployments sneaking back in — if a feature requires simultaneous changes to two micro-frontends, it means the API contract between them is changing. Version that contract and deploy the provider first.
- No rollback plan — deploying a broken micro-frontend with no rollback path means the entire application is degraded until a fix is pushed. Always maintain the ability to revert to the previous version.
- Environment configuration leaking into builds — API URLs, feature flags, and other environment-specific values should be injected at runtime (via the shell or environment variables served by the CDN), not baked into the build.
- Ignoring bundle size growth — without monitoring, micro-frontend bundles grow unchecked. Set CI budget checks (e.g., fail the build if the bundle exceeds 200KB gzipped).
- Missing health checks — if a micro-frontend's assets return 404 or 500, the shell should show a fallback. Without health monitoring, broken deployments go unnoticed until users complain.
Core Philosophy
Independent deployment is the entire reason micro-frontends exist as an architecture. If deploying one micro-frontend requires rebuilding, retesting, or redeploying another, you have a distributed monolith, not a micro-frontend system. Each team must have its own CI/CD pipeline, its own versioned artifacts, and the ability to ship at its own pace without coordination with other teams.
Runtime integration is the enabler of true independence. Build-time integration (npm install + rebuild) creates a deployment coupling that defeats the purpose. Whether you use import maps, Module Federation remote entries, or manifest files, the shell must discover and load micro-frontend assets at runtime so that updating one does not require touching the others.
Rollback must be a one-click operation. A broken micro-frontend deployment should be revertible in seconds by pointing the import map or manifest back to the previous version's hashed assets. If rollback requires a new build, a new deployment pipeline run, or manual intervention, it is too slow for production incidents.
Anti-Patterns
-
Requiring shell rebuilds for micro-frontend updates — if the shell must be rebuilt when a micro-frontend changes, you have build-time coupling that prevents independent delivery.
-
Caching entry points with long TTLs — if
remoteEntry.jsorimportmap.jsonis cached for hours, users do not see new deployments until the cache expires; keep entry point TTLs at 30-60 seconds. -
Coordinating deployments between teams — if a feature requires simultaneous changes to two micro-frontends, the API contract between them is changing; version that contract and deploy the provider first.
-
Baking environment configuration into builds — API URLs, feature flags, and environment-specific values should be injected at runtime, not compiled into the bundle; otherwise the same artifact cannot promote through environments.
-
Not monitoring bundle size per micro-frontend — without CI budget checks, micro-frontend bundles grow unchecked; set a gzipped size limit and fail the build if it is exceeded.
Install this skill directly: skilldb add micro-frontend-skills
Related Skills
Design System Sharing
Strategies for sharing design systems, component libraries, and visual consistency across independently deployed micro-frontends.
Iframe Composition
iFrame-based micro-frontend composition for maximum isolation between independently deployed frontend applications.
Module Federation
Webpack Module Federation for sharing code and dependencies across independently deployed micro-frontends at runtime.
Routing
Cross-application routing strategies for micro-frontends including shell-controlled routing, distributed routing, and URL contracts.
Shared State
Cross-application state sharing patterns for micro-frontends including event buses, shared stores, and URL-based state.
Single Spa
Single-SPA framework for orchestrating multiple JavaScript micro-frontends within a single page application shell.