Skip to main content
Technology & EngineeringMicro Frontend379 lines

Deployment

Independent deployment patterns for micro-frontends including CI/CD pipelines, versioning, rollback, and environment strategies.

Quick Summary24 lines
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 lines
Paste into your CLAUDE.md or agent config

Deployment — 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-TimeRuntime
Integration pointnpm install + webpack buildBrowser fetches remote at page load
Deploy independenceNo — shell must rebuildYes — deploy remote, shell picks it up
Version controlpackage.json lockImport map, manifest, or Module Federation
RollbackRedeploy shell with old dep versionUpdate pointer to old remote version

Runtime integration is required for true independent deployment.

The Remote Entry / Manifest Pattern

Each micro-frontend build produces:

  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.

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

  1. 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.
  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.

Common Pitfalls

  • CDN cache serving stale entry points — if remoteEntry.js is 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.js or importmap.json is 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

Get CLI access →