<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sohana Akbar</title>
    <description>The latest articles on DEV Community by Sohana Akbar (@sohanaakbar7).</description>
    <link>https://dev.to/sohanaakbar7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3878706%2Fb026acb6-e832-44ba-9999-2c0e44f18218.jpg</url>
      <title>DEV Community: Sohana Akbar</title>
      <link>https://dev.to/sohanaakbar7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sohanaakbar7"/>
    <language>en</language>
    <item>
      <title>Docker Caching Strategies That Actually Work with npm ci</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Tue, 26 May 2026 07:24:31 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/docker-caching-strategies-that-actually-work-with-npm-ci-4ja4</link>
      <guid>https://dev.to/sohanaakbar7/docker-caching-strategies-that-actually-work-with-npm-ci-4ja4</guid>
      <description>&lt;p&gt;If you’ve ever waited 10 minutes for npm ci to run inside a Docker build, you know the pain.&lt;/p&gt;

&lt;p&gt;You add a single line of code, rebuild, and… Docker re-installs everything. 🔥&lt;/p&gt;

&lt;p&gt;Here’s the truth: npm ci is fantastic for CI/CD—but its strict nature breaks naive Docker caching. Let’s fix that for good.&lt;/p&gt;

&lt;p&gt;Why npm ci breaks your cache&lt;br&gt;
npm ci installs directly from package-lock.json. If the lockfile changes at all, Docker’s build cache invalidates from that layer onward.&lt;/p&gt;

&lt;p&gt;But Docker doesn’t just check the content of package-lock.json—it checks the file’s timestamp too. A fresh git checkout? New timestamp → cache miss.&lt;/p&gt;

&lt;p&gt;Strategy 1: Copy only the lockfile first&lt;br&gt;
❌ Don’t do this:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm ci&lt;br&gt;
✅ Do this:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm ci&lt;br&gt;
COPY . .&lt;br&gt;
This way, npm ci only reruns if package.json or package-lock.json actually changes.&lt;/p&gt;

&lt;p&gt;Strategy 2: Leverage Docker’s --mount=type=cache (modern hero)&lt;br&gt;
For Docker 18.09+ with BuildKit:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  syntax=docker/dockerfile:1.4
&lt;/h1&gt;

&lt;p&gt;FROM node:20-slim&lt;/p&gt;

&lt;p&gt;WORKDIR /app&lt;/p&gt;

&lt;p&gt;COPY package*.json ./&lt;/p&gt;

&lt;p&gt;RUN --mount=type=cache,target=/root/.npm \&lt;br&gt;
    npm ci --only=production&lt;/p&gt;

&lt;p&gt;COPY . .&lt;br&gt;
The --mount=cache keeps npm’s global cache between builds. Combine with --only=production for smaller images.&lt;/p&gt;

&lt;p&gt;Strategy 3: Separate devDependencies from production&lt;br&gt;
dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 1: dev dependencies (for testing)
&lt;/h1&gt;

&lt;p&gt;FROM node:20-slim AS deps&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm ci&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 2: production-only
&lt;/h1&gt;

&lt;p&gt;FROM node:20-slim AS prod&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm ci --only=production&lt;br&gt;
COPY --from=deps /app/node_modules /app/node_modules&lt;br&gt;
COPY . .&lt;br&gt;
Why this works: --only=production creates a deterministic subset. Changes to dev dependencies don’t touch production layers.&lt;/p&gt;

&lt;p&gt;Strategy 4: Copy lockfile with preserved timestamps&lt;br&gt;
Some CI systems (looking at you, Jenkins) reset timestamps. Fix it:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY --chown=node:node package*.json ./&lt;/p&gt;

&lt;h1&gt;
  
  
  Force a known timestamp if needed:
&lt;/h1&gt;

&lt;h1&gt;
  
  
  RUN touch --date="2024-01-01 00:00:00" package-lock.json
&lt;/h1&gt;

&lt;p&gt;Better: Use --checksum with BuildKit (available in newer Docker):&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY --checksum=sha256:... package-lock.json .&lt;br&gt;
The ultimate recipe (for most projects)&lt;br&gt;
dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  syntax=docker/dockerfile:1.4
&lt;/h1&gt;

&lt;p&gt;FROM node:20-slim AS builder&lt;/p&gt;

&lt;p&gt;WORKDIR /app&lt;br&gt;
COPY package*.json ./&lt;/p&gt;

&lt;p&gt;RUN --mount=type=cache,target=/root/.npm \&lt;br&gt;
    npm ci&lt;/p&gt;

&lt;p&gt;COPY . .&lt;/p&gt;

&lt;h1&gt;
  
  
  Final stage
&lt;/h1&gt;

&lt;p&gt;FROM node:20-slim&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY --from=builder /app/node_modules ./node_modules&lt;br&gt;
COPY . .&lt;br&gt;
CMD ["node", "index.js"]&lt;br&gt;
Real-world results&lt;br&gt;
Before: 3–5 min installs on every build&lt;/p&gt;

&lt;p&gt;After: 10–20 secs (cache hit), 2 min (full rebuild)&lt;/p&gt;

&lt;p&gt;Gotchas to remember&lt;br&gt;
npm ci deletes node_modules before install. That’s intentional—don’t fight it.&lt;/p&gt;

&lt;p&gt;Private registries? Mount your .npmrc separately:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY .npmrc ./&lt;br&gt;
RUN --mount=type=secret,id=npmrc cat /run/secrets/npmrc &amp;gt; .npmrc&lt;br&gt;
Always run npm ci (not npm install) in CI. You want exact lockfile compliance.&lt;/p&gt;

&lt;p&gt;Bottom line&lt;br&gt;
Treat package-lock.json as the source of truth. Copy it first. Use BuildKit’s cache mounts. Separate dev from prod. Your build times will thank you.&lt;/p&gt;

&lt;p&gt;Have a faster trick? Let me know in the comments. 🚀&lt;/p&gt;

</description>
      <category>docker</category>
      <category>npm</category>
      <category>node</category>
      <category>caching</category>
    </item>
    <item>
      <title>Running Playwright Tests Inside a Docker Container</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Sun, 24 May 2026 09:47:35 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/running-playwright-tests-inside-a-docker-container-56o5</link>
      <guid>https://dev.to/sohanaakbar7/running-playwright-tests-inside-a-docker-container-56o5</guid>
      <description>&lt;p&gt;Playwright is a fantastic tool for end-to-end testing of modern web apps. But if you’ve ever tried to run your Playwright test suite on a CI server (or a colleague’s machine), you’ve probably run into the classic: “But it works on my machine!”&lt;/p&gt;

&lt;p&gt;The solution? Docker.&lt;/p&gt;

&lt;p&gt;Containers give you a consistent, isolated, and reproducible environment for your tests. No more Chrome version mismatches or missing system dependencies.&lt;/p&gt;

&lt;p&gt;Let’s walk through how to containerize your Playwright tests.&lt;/p&gt;

&lt;p&gt;Why Docker for Playwright?&lt;br&gt;
Consistency – Run tests exactly the same way locally, on staging, or in CI.&lt;/p&gt;

&lt;p&gt;Speed – No need to set up browsers on the host machine. Docker images come with everything pre-installed.&lt;/p&gt;

&lt;p&gt;Parallelism – Easily spin up multiple containers to shard your test suite.&lt;/p&gt;

&lt;p&gt;Step 1: Your Project Structure&lt;br&gt;
Assume a simple Node.js project with Playwright:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
my-playwright-tests/&lt;br&gt;
├── tests/&lt;br&gt;
│   └── example.spec.js&lt;br&gt;
├── package.json&lt;br&gt;
├── playwright.config.js&lt;br&gt;
└── Dockerfile&lt;br&gt;
Step 2: The Dockerfile&lt;br&gt;
Playwright maintains official Docker images. Here’s a minimal, production-ready Dockerfile:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  Use the official Playwright image
&lt;/h1&gt;

&lt;p&gt;FROM mcr.microsoft.com/playwright:latest&lt;/p&gt;

&lt;p&gt;WORKDIR /app&lt;/p&gt;

&lt;h1&gt;
  
  
  Copy package files and install dependencies
&lt;/h1&gt;

&lt;p&gt;COPY package*.json ./&lt;br&gt;
RUN npm ci&lt;/p&gt;

&lt;h1&gt;
  
  
  Copy the rest of your test code
&lt;/h1&gt;

&lt;p&gt;COPY . .&lt;/p&gt;

&lt;h1&gt;
  
  
  Run the tests
&lt;/h1&gt;

&lt;p&gt;CMD ["npx", "playwright", "test"]&lt;br&gt;
💡 The playwright:latest image already includes Chromium, Firefox, WebKit, and all system dependencies.&lt;/p&gt;

&lt;p&gt;Step 3: Build &amp;amp; Run&lt;br&gt;
Build the image:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
docker build -t playwright-tests .&lt;br&gt;
Run once:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
docker run --rm playwright-tests&lt;br&gt;
Run with mounted reports (to see HTML report locally):&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
docker run --rm -v $(pwd)/test-results:/app/test-results playwright-tests&lt;br&gt;
Step 4: Running specific browsers or headed mode&lt;br&gt;
Want to see the browser window? Pass environment variables or override the command:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
docker run --rm -e BROWSER=firefox playwright-tests npx playwright test --headed&lt;br&gt;
Note: Headed mode requires --ipc=host on Linux or special X11 handling. For CI, stick to headless.&lt;/p&gt;

&lt;p&gt;Step 5: Using in CI (GitHub Actions example)&lt;br&gt;
yaml&lt;br&gt;
name: Playwright tests&lt;br&gt;
on: push&lt;br&gt;
jobs:&lt;br&gt;
  test:&lt;br&gt;
    runs-on: ubuntu-latest&lt;br&gt;
    container:&lt;br&gt;
      image: mcr.microsoft.com/playwright:latest&lt;br&gt;
    steps:&lt;br&gt;
      - uses: actions/checkout@v4&lt;br&gt;
      - run: npm ci&lt;br&gt;
      - run: npx playwright test&lt;br&gt;
      - uses: actions/upload-artifact@v4&lt;br&gt;
        if: always()&lt;br&gt;
        with:&lt;br&gt;
          name: playwright-report&lt;br&gt;
          path: playwright-report/&lt;br&gt;
Common pitfalls &amp;amp; fixes&lt;br&gt;
Problem Solution&lt;br&gt;
Tests fail because of missing fonts Use mcr.microsoft.com/playwright:focal for better system fonts.&lt;br&gt;
Docker cache bloats Use --mount=type=cache for ~/.cache/ms-playwright if you build frequently.&lt;br&gt;
Slow test startup   Pre-build the image and reuse layers.&lt;br&gt;
To sum up&lt;br&gt;
Containerizing your Playwright tests is a small upfront effort that pays off in reliability. You get:&lt;/p&gt;

&lt;p&gt;✅ No more “works on my machine”&lt;/p&gt;

&lt;p&gt;✅ Painless CI setup&lt;/p&gt;

&lt;p&gt;✅ Easy scaling across multiple containers&lt;/p&gt;

&lt;p&gt;Try it out. Your future self (and your teammates) will thank you.&lt;/p&gt;

&lt;p&gt;Have you tried running Playwright in Docker? What was your biggest blocker? Let me know in the comments.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>docker</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Docker Compose for Local Frontend + API + DB — Sample Repo</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Sat, 23 May 2026 10:38:34 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/docker-compose-for-local-frontend-api-db-sample-repo-4gba</link>
      <guid>https://dev.to/sohanaakbar7/docker-compose-for-local-frontend-api-db-sample-repo-4gba</guid>
      <description>&lt;p&gt;Stop "It Works on My Machine" Forever&lt;br&gt;
Spinning up a full-stack app locally usually means:&lt;/p&gt;

&lt;p&gt;npm install in 3 terminals&lt;/p&gt;

&lt;p&gt;A local database needing manual setup&lt;/p&gt;

&lt;p&gt;Environment variables getting lost&lt;/p&gt;

&lt;p&gt;Docker Compose fixes all of that. One command, three services, zero local dependencies.&lt;/p&gt;

&lt;p&gt;The Stack (Simple &amp;amp; Realistic)&lt;br&gt;
Frontend: React/Vite (port 3000)&lt;/p&gt;

&lt;p&gt;API: Node.js/Express (port 5000)&lt;/p&gt;

&lt;p&gt;Database: PostgreSQL (port 5432)&lt;/p&gt;

&lt;p&gt;The Project Structure&lt;br&gt;
text&lt;br&gt;
my-app/&lt;br&gt;
├── frontend/&lt;br&gt;
│   ├── Dockerfile&lt;br&gt;
│   └── (React app)&lt;br&gt;
├── backend/&lt;br&gt;
│   ├── Dockerfile&lt;br&gt;
│   └── (Express app)&lt;br&gt;
└── docker-compose.yml&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Compose File (docker-compose.yml)
yaml
version: '3.8'&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;services:&lt;br&gt;
  db:&lt;br&gt;
    image: postgres:15&lt;br&gt;
    environment:&lt;br&gt;
      POSTGRES_USER: devuser&lt;br&gt;
      POSTGRES_PASSWORD: devpass&lt;br&gt;
      POSTGRES_DB: appdb&lt;br&gt;
    ports:&lt;br&gt;
      - "5432:5432"&lt;br&gt;
    volumes:&lt;br&gt;
      - pgdata:/var/lib/postgresql/data&lt;/p&gt;

&lt;p&gt;backend:&lt;br&gt;
    build: ./backend&lt;br&gt;
    ports:&lt;br&gt;
      - "5000:5000"&lt;br&gt;
    environment:&lt;br&gt;
      DATABASE_URL: postgresql://devuser:devpass@db:5432/appdb&lt;br&gt;
    depends_on:&lt;br&gt;
      - db&lt;br&gt;
    volumes:&lt;br&gt;
      - ./backend:/app&lt;br&gt;
      - /app/node_modules&lt;/p&gt;

&lt;p&gt;frontend:&lt;br&gt;
    build: ./frontend&lt;br&gt;
    ports:&lt;br&gt;
      - "3000:3000"&lt;br&gt;
    environment:&lt;br&gt;
      VITE_API_URL: &lt;a href="http://localhost:5000" rel="noopener noreferrer"&gt;http://localhost:5000&lt;/a&gt;&lt;br&gt;
    depends_on:&lt;br&gt;
      - backend&lt;br&gt;
    volumes:&lt;br&gt;
      - ./frontend:/app&lt;br&gt;
      - /app/node_modules&lt;/p&gt;

&lt;p&gt;volumes:&lt;br&gt;
  pgdata:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Frontend Dockerfile (frontend/Dockerfile)
dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]&lt;/li&gt;
&lt;li&gt;Backend Dockerfile (backend/Dockerfile)
dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "run", "dev"]
Note: Using npm run dev with --watch or nodemon enables hot reloading thanks to the volume mounts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running Everything&lt;br&gt;
bash&lt;br&gt;
docker-compose up --build&lt;br&gt;
That's it. Your entire stack is running:&lt;/p&gt;

&lt;p&gt;Frontend → &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;API → &lt;a href="http://localhost:5000" rel="noopener noreferrer"&gt;http://localhost:5000&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL → localhost:5432&lt;/p&gt;

&lt;p&gt;Why This Rocks for Local Dev&lt;br&gt;
✅ Zero local installs — No Node, no Postgres on your host&lt;br&gt;
✅ Perfect isolation — Different Node versions? No problem&lt;br&gt;
✅ Instant onboarding — New teammate runs one command&lt;br&gt;
✅ Hot reload — Code changes rebuild instantly (volume mounts)&lt;br&gt;
✅ Clean teardown — docker-compose down -v nukes everything&lt;/p&gt;

&lt;p&gt;The Sample Repo&lt;br&gt;
👉 github.com/yourusername/fullstack-docker-compose&lt;/p&gt;

&lt;p&gt;(Clone it, run docker-compose up, and you're live in 30 seconds)&lt;/p&gt;

&lt;p&gt;Next Steps&lt;br&gt;
Add a .env file for secrets&lt;/p&gt;

&lt;p&gt;Use profiles for dev vs prod&lt;/p&gt;

&lt;p&gt;Throw in Redis or Nginx if needed&lt;/p&gt;

&lt;p&gt;Stop wrestling with local setup. Ship code instead.&lt;/p&gt;

&lt;p&gt;Have you tried Docker Compose for full-stack dev? Drop your tips below. 🐳&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why You Shouldn't Run npm install in Production Containers</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Fri, 22 May 2026 12:34:18 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/why-you-shouldnt-run-npm-install-in-production-containers-5048</link>
      <guid>https://dev.to/sohanaakbar7/why-you-shouldnt-run-npm-install-in-production-containers-5048</guid>
      <description>&lt;p&gt;You’ve built your Docker image, and everything works fine locally. But when you deploy to production, things break. Or worse — they seem fine, until a dependency silently changes and your app crashes at 3 AM.&lt;/p&gt;

&lt;p&gt;The culprit? Often, it's running npm install inside your production container.&lt;/p&gt;

&lt;p&gt;Let’s break down why this is risky and what to do instead.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Non-Deterministic Builds
npm install fetches the latest compatible versions based on your ^ or ~ ranges. Today, that might mean &lt;a href="mailto:lodash@4.17.20"&gt;lodash@4.17.20&lt;/a&gt;. Tomorrow, it could be 4.17.21 — with a subtle breaking change or a vulnerability fix that actually breaks your logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your container image becomes a time bomb. Rebuilding the same image a week later could produce a different result.&lt;/p&gt;

&lt;p&gt;Fix: Always use npm ci instead of npm install in production. It installs exactly what’s in your package-lock.json.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build Tools &amp;amp; Unnecessary Dependencies
npm install often pulls in devDependencies by default (unless you pass --omit=dev). That means:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TypeScript compiler&lt;/p&gt;

&lt;p&gt;Testing frameworks&lt;/p&gt;

&lt;p&gt;Linters&lt;/p&gt;

&lt;p&gt;Build tools like node-gyp&lt;/p&gt;

&lt;p&gt;These add size, attack surface, and potential runtime issues — none of which belong in production.&lt;/p&gt;

&lt;p&gt;Fix: Use npm ci --omit=dev or set NODE_ENV=production before install.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Slower, Larger, Less Secure Images
Running npm install inside your final container means:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No node_modules layer caching (unless carefully optimized)&lt;/p&gt;

&lt;p&gt;Larger image size (gigabytes vs. megabytes)&lt;/p&gt;

&lt;p&gt;Extra time during deployment&lt;/p&gt;

&lt;p&gt;More importantly, every npm install re-runs scripts from packages. Malicious or compromised packages can execute arbitrary code during install — in your production environment.&lt;/p&gt;

&lt;p&gt;Fix: Build node_modules in a separate build stage and copy only what’s needed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Better Pattern: Multi-Stage Builds
Here’s the safe, fast, reproducible way:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  Build stage
&lt;/h1&gt;

&lt;p&gt;FROM node:20-alpine AS builder&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm ci --omit=dev&lt;/p&gt;

&lt;h1&gt;
  
  
  Production stage
&lt;/h1&gt;

&lt;p&gt;FROM node:20-alpine&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY --from=builder /app/node_modules ./node_modules&lt;br&gt;
COPY . .&lt;br&gt;
CMD ["node", "index.js"]&lt;br&gt;
No network calls during production container start. No surprises. Small image.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exceptions to the Rule
Sometimes you do need to run npm install in production:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You’re installing private packages that require runtime authentication&lt;/p&gt;

&lt;p&gt;You’re deploying to an environment without build capabilities (e.g., some PaaS)&lt;/p&gt;

&lt;p&gt;Even then, prefer npm ci and avoid running it repeatedly.&lt;/p&gt;

&lt;p&gt;Summary&lt;br&gt;
Practice    Deterministic   Fast    Secure  Production Ready&lt;br&gt;
npm install (default)   ❌ ❌ ❌ ❌&lt;br&gt;
npm ci --omit=dev   ✅ ✅ ⚠️ (still runs scripts) ⚠️&lt;br&gt;
Pre-built + copy    ✅ ✅✅  ✅ ✅&lt;br&gt;
Don’t let your production container phone home for dependencies at runtime. Build once, copy carefully, and deploy with confidence.&lt;/p&gt;

&lt;p&gt;Want to catch these issues before they reach production? Add npm ci to your CI pipeline and test against a locked package-lock.json.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>node</category>
      <category>npm</category>
    </item>
    <item>
      <title>Multi-Stage Builds for a Next.js App — Reduce Image Size by 70%</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Thu, 21 May 2026 14:04:52 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/multi-stage-builds-for-a-nextjs-app-reduce-image-size-by-70-4nni</link>
      <guid>https://dev.to/sohanaakbar7/multi-stage-builds-for-a-nextjs-app-reduce-image-size-by-70-4nni</guid>
      <description>&lt;p&gt;Next.js apps are heavy. A standard Docker build often exceeds 1 GB, wasting bandwidth, disk space, and deployment time.&lt;/p&gt;

&lt;p&gt;The fix? Multi-stage builds. With a simple Dockerfile change, you can shrink that image by 70% or more.&lt;/p&gt;

&lt;p&gt;Why is the default image so big?&lt;br&gt;
A typical single-stage Dockerfile:&lt;/p&gt;

&lt;p&gt;Installs node_modules (including devDependencies like TypeScript, ESLint, testing tools).&lt;/p&gt;

&lt;p&gt;Keeps build caches and source maps.&lt;/p&gt;

&lt;p&gt;Includes the entire build toolchain inside the final image.&lt;/p&gt;

&lt;p&gt;You don’t need any of that in production.&lt;/p&gt;

&lt;p&gt;The multi-stage approach&lt;br&gt;
Split the build into three stages:&lt;/p&gt;

&lt;p&gt;Dependencies – Install everything.&lt;/p&gt;

&lt;p&gt;Builder – Run next build.&lt;/p&gt;

&lt;p&gt;Production – Copy only the bare essentials.&lt;/p&gt;

&lt;p&gt;Example Dockerfile&lt;br&gt;
dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 1: Dependencies
&lt;/h1&gt;

&lt;p&gt;FROM node:18-alpine AS deps&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY package.json package-lock.json ./&lt;br&gt;
RUN npm ci --only=production&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 2: Builder
&lt;/h1&gt;

&lt;p&gt;FROM node:18-alpine AS builder&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY --from=deps /app/node_modules ./node_modules&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm run build&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 3: Production
&lt;/h1&gt;

&lt;p&gt;FROM node:18-alpine AS runner&lt;br&gt;
WORKDIR /app&lt;/p&gt;

&lt;h1&gt;
  
  
  Copy standalone output (Next.js 12+)
&lt;/h1&gt;

&lt;p&gt;COPY --from=builder /app/.next/standalone ./&lt;br&gt;
COPY --from=builder /app/.next/static ./.next/static&lt;br&gt;
COPY --from=builder /app/public ./public&lt;/p&gt;

&lt;p&gt;EXPOSE 3000&lt;br&gt;
CMD ["node", "server.js"]&lt;br&gt;
Key line:&lt;br&gt;
COPY --from=builder /app/.next/standalone ./ — This is the magic. It grabs only the compiled production output.&lt;/p&gt;

&lt;p&gt;Results&lt;br&gt;
Metric  Single-stage    Multi-stage Reduction&lt;br&gt;
Image size  1.2 GB  280 MB  77%&lt;br&gt;
Node modules    Full (300 MB)   None in final   100%&lt;br&gt;
Build artifacts Kept    Removed –&lt;br&gt;
Pro tips for even smaller images&lt;br&gt;
Enable standalone output – In next.config.js:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
module.exports = {&lt;br&gt;
  output: 'standalone',&lt;br&gt;
}&lt;br&gt;
This creates a self-contained server.js with minimal dependencies.&lt;/p&gt;

&lt;p&gt;Use Alpine Linux – node:18-alpine is ~50 MB vs node:18 (~1 GB).&lt;/p&gt;

&lt;p&gt;Add .dockerignore – Exclude .git, .next, node_modules, Dockerfile.&lt;/p&gt;

&lt;p&gt;Run as non-root – Add RUN addgroup --system --gid 1001 nodejs + USER nodejs for security.&lt;/p&gt;

&lt;p&gt;What about caching?&lt;br&gt;
Multi-stage still caches well. Docker caches each stage independently. Your CI will rebuild only changed layers.&lt;/p&gt;

&lt;p&gt;The bottom line&lt;br&gt;
Switching to multi-stage builds is 15 minutes of work for a 70%+ size reduction. Smaller images mean:&lt;/p&gt;

&lt;p&gt;Faster deployments&lt;/p&gt;

&lt;p&gt;Lower storage costs&lt;/p&gt;

&lt;p&gt;Quicker container pulls (especially on Kubernetes)&lt;/p&gt;

&lt;p&gt;Try it on your next Next.js project. Your DevOps team will thank you.&lt;/p&gt;

&lt;p&gt;Need a working example? Check out the official Next.js Docker example in their GitHub repo.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>nextjs</category>
      <category>performance</category>
    </item>
    <item>
      <title>5 Dockerfile mistakes frontend devs make</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Wed, 20 May 2026 14:02:23 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/5-dockerfile-mistakes-frontend-devs-make-22jn</link>
      <guid>https://dev.to/sohanaakbar7/5-dockerfile-mistakes-frontend-devs-make-22jn</guid>
      <description>&lt;p&gt;Dockerizing frontend apps sounds simple — until node_modules disappears or your image is 1.2GB. Here are the most common mistakes I've seen (and made myself).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copying everything, then installing dependencies
❌ Wrong:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm install&lt;br&gt;
RUN npm run build&lt;br&gt;
Every code change invalidates the cache, forcing npm install to re-run. Slow.&lt;/p&gt;

&lt;p&gt;✅ Right:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm install&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm run build&lt;br&gt;
Docker caches the RUN npm install layer unless package.json changes. Much faster rebuilds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using node:latest as the base image for production
Your 900MB image includes compilers, npm, and Python — none of which your nginx server needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✅ Right: Multi-stage builds&lt;/p&gt;

&lt;p&gt;dockerfile&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 1: build
&lt;/h1&gt;

&lt;p&gt;FROM node:20-alpine AS builder&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY package*.json ./&lt;br&gt;
RUN npm ci&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm run build&lt;/p&gt;

&lt;h1&gt;
  
  
  Stage 2: serve
&lt;/h1&gt;

&lt;p&gt;FROM nginx:alpine&lt;br&gt;
COPY --from=builder /app/dist /usr/share/nginx/html&lt;br&gt;
EXPOSE 80&lt;br&gt;
Final image: ~25MB instead of 900MB.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Running as root in the container
Your container runs as root by default. If someone sneaks into your container, they own the host's kernel namespaces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✅ Right:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
RUN addgroup -g 1001 -S nodejs &amp;amp;&amp;amp; \&lt;br&gt;
    adduser -S nextjs -u 1001&lt;br&gt;
USER nextjs&lt;br&gt;
For nginx images, they already have a nginx user — use it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ignoring npm ci in favor of npm install
npm install can update lock files and install different versions than your package-lock.json defines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✅ Right:&lt;/p&gt;

&lt;p&gt;dockerfile&lt;br&gt;
COPY package*.json package-lock.json ./&lt;br&gt;
RUN npm ci&lt;br&gt;
npm ci:&lt;/p&gt;

&lt;p&gt;Fails if lock file is out of sync&lt;/p&gt;

&lt;p&gt;Installs exactly what's in the lock file&lt;/p&gt;

&lt;p&gt;Is faster for CI/CD&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leaving node_modules in the final stage
You don't need dependencies in production if you're serving static files. But sometimes people do:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;dockerfile&lt;br&gt;
FROM node:20-alpine&lt;br&gt;
COPY . .&lt;br&gt;
RUN npm ci &amp;amp;&amp;amp; npm run build&lt;br&gt;
CMD ["npm", "run", "preview"]  # still has dev dependencies!&lt;br&gt;
✅ Right: Build once, discard everything except the output (see multi-stage example above). Only the dist or .next folder goes to production.&lt;/p&gt;

&lt;p&gt;Quick checklist before you ship&lt;br&gt;
Multi-stage build with static server&lt;/p&gt;

&lt;p&gt;COPY package.json before RUN npm ci&lt;/p&gt;

&lt;p&gt;Non-root user&lt;/p&gt;

&lt;p&gt;npm ci not npm install&lt;/p&gt;

&lt;p&gt;No dev dependencies in final image&lt;/p&gt;

&lt;p&gt;Your frontend image doesn't need Node.js at runtime. Treat your Dockerfile like you treat your bundle — ship only what's necessary.&lt;/p&gt;

&lt;p&gt;What mistake would you add? Drop it in the comments.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>frontend</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>Canary Deployments for Frontends: Overkill or Necessary?</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Tue, 19 May 2026 13:38:40 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/canary-deployments-for-frontends-overkill-or-necessary-10og</link>
      <guid>https://dev.to/sohanaakbar7/canary-deployments-for-frontends-overkill-or-necessary-10og</guid>
      <description>&lt;p&gt;Let's settle this debate right away: Canary deployments for frontends are not overkill—they're underused.&lt;/p&gt;

&lt;p&gt;But I get the skepticism. Backend canaries? Essential. Frontends? Isn't that just... showing some users the new button?&lt;/p&gt;

&lt;p&gt;Here's why it's worth your time.&lt;/p&gt;

&lt;p&gt;The "It's Just HTML/CSS/JS" Fallacy&lt;br&gt;
Frontends break in production all the time:&lt;/p&gt;

&lt;p&gt;JavaScript errors that ruin core functionality&lt;/p&gt;

&lt;p&gt;Layout shifts breaking checkout flows&lt;/p&gt;

&lt;p&gt;API contract mismatches nobody caught in staging&lt;/p&gt;

&lt;p&gt;Third-party script failures cascading into outages&lt;/p&gt;

&lt;p&gt;Dark mode + browser + OS version combos your QA never saw&lt;/p&gt;

&lt;p&gt;Your frontend is the face of your product. When it breaks, customers feel it immediately.&lt;/p&gt;

&lt;p&gt;What Makes Frontend Canaries Different&lt;br&gt;
Backend canaries route traffic at the request level. Frontends need a different approach:&lt;/p&gt;

&lt;p&gt;Feature flags (simplest) – Toggle visibility for 1% of users, monitor errors, ramp up.&lt;/p&gt;

&lt;p&gt;Edge/CDN routing – Serve different bundle versions based on cookie/header (CloudFlare, Fastly, Vercel Edge Config).&lt;/p&gt;

&lt;p&gt;Build-time variants – Deploy both versions, use load balancer to split traffic (overkill for most SPAs).&lt;/p&gt;

&lt;p&gt;When It's Actually Overkill&lt;br&gt;
You probably don't need frontend canaries if:&lt;/p&gt;

&lt;p&gt;Your team is &amp;lt; 3 people&lt;/p&gt;

&lt;p&gt;You deploy &amp;lt; 5 times per week&lt;/p&gt;

&lt;p&gt;Your app has &amp;lt; 1,000 daily active users&lt;/p&gt;

&lt;p&gt;Breaking things means "oops, fix in 10 minutes"&lt;/p&gt;

&lt;p&gt;In those cases, better monitoring + faster rollbacks win.&lt;/p&gt;

&lt;p&gt;When It's Necessary&lt;br&gt;
You should implement frontend canaries when:&lt;/p&gt;

&lt;p&gt;Revenue flows through your UI (e-commerce, SaaS checkout)&lt;/p&gt;

&lt;p&gt;You have &amp;gt; 10K DAU – 1% of users finding a bug = 100 angry people&lt;/p&gt;

&lt;p&gt;Your frontend is mission-critical (banking, healthcare, dashboards)&lt;/p&gt;

&lt;p&gt;Teams deploy independently – micro-frontends especially benefit&lt;/p&gt;

&lt;p&gt;Regulatory requirements demand gradual rollouts&lt;/p&gt;

&lt;p&gt;The Practical Middle Ground&lt;br&gt;
Start minimal. You don't need Kubernetes and 12 services.&lt;/p&gt;

&lt;p&gt;Step 1: Add error tracking (Sentry, Bugsnag) and RUM (Datadog, LogRocket).&lt;/p&gt;

&lt;p&gt;Step 2: Use a simple feature flag for any non-trivial UI change.&lt;/p&gt;

&lt;p&gt;Step 3: Graduate to CDN-based routing for major releases.&lt;/p&gt;

&lt;p&gt;Step 4: Automate rollback on error budget violation.&lt;/p&gt;

&lt;p&gt;Real Talk&lt;br&gt;
Most frontend teams skip canaries because they're "too complex." But complexity is just unfamiliarity.&lt;/p&gt;

&lt;p&gt;Modern platforms (Vercel, Netlify, Cloudflare) have made this shockingly easy. You can have production canary deploys in an afternoon.&lt;/p&gt;

&lt;p&gt;The real question isn't "overkill or necessary?"—it's "can we afford the lost customers when our next deploy breaks for everyone?"&lt;/p&gt;

&lt;p&gt;For most production apps with real users, the answer is no.&lt;/p&gt;

&lt;p&gt;Start with 1%. Watch the metrics. Ramp if green. Rollback if red. Your users will thank you.&lt;/p&gt;

&lt;p&gt;What's your take? Have you tried frontend canaries, or are you still deploying to 100% and praying? Drop your experience below.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Test a Frontend Build Inside a Docker Container Before Deploy</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Mon, 18 May 2026 13:18:49 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/how-to-test-a-frontend-build-inside-a-docker-container-before-deploy-en7</link>
      <guid>https://dev.to/sohanaakbar7/how-to-test-a-frontend-build-inside-a-docker-container-before-deploy-en7</guid>
      <description>&lt;p&gt;How to Test a Frontend Build Inside a Docker Container Before Deploy&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Pipeline as YAML — Good or Bad? 🤔</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Sun, 17 May 2026 13:00:35 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/pipeline-as-yaml-good-or-bad-3hl0</link>
      <guid>https://dev.to/sohanaakbar7/pipeline-as-yaml-good-or-bad-3hl0</guid>
      <description>&lt;p&gt;Let’s settle this once and for all.&lt;/p&gt;

&lt;p&gt;If you’ve worked with CI/CD tools like GitHub Actions, GitLab CI, or Jenkins, you’ve met Pipeline as YAML. Some devs love it. Others loathe it.&lt;/p&gt;

&lt;p&gt;So which is it? Good or bad?&lt;/p&gt;

&lt;p&gt;Spoiler: It’s both. Let me explain.&lt;/p&gt;

&lt;p&gt;✅ The Good&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Version control everything&lt;br&gt;
Your pipeline lives right next to your code. No more clicking through a UI to update a build step. Change → commit → review → done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reproducible builds&lt;br&gt;
That pipeline that worked six months ago? Still works. No “but it worked on Sarah’s machine” nonsense.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reviewable like code&lt;br&gt;
Pull requests for pipeline changes mean your team can catch mistakes before they break main.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Portable (mostly)&lt;br&gt;
Once you learn one YAML CI syntax, switching between GitHub Actions, GitLab, and Bitbucket feels familiar — just different keywords.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;❌ The Bad&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;YAML is… YAML&lt;br&gt;
Indentation mistakes, invisible trailing spaces, and the dreaded “boolean vs string” trap.&lt;br&gt;
on: yes → suddenly your pipeline runs only on Tuesday. Good luck debugging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy-paste hell&lt;br&gt;
Want the same job for dev, staging, and prod? Some tools offer anchors/references. Many devs just copy-paste 50 lines. Three times.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited logic&lt;br&gt;
No loops. No functions. No real programming constructs. You end up writing shell scripts inside YAML strings — which are also untyped and untested.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debugging pain&lt;br&gt;
“Invalid pipeline” — thanks, tool. Which line? What’s wrong? Go fish.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🧠 The Verdict&lt;br&gt;
Pipeline as YAML isn’t inherently bad. It’s a minimum viable interface for defining workflows. The real problem is when your pipeline needs programming, but YAML gives you data serialization.&lt;/p&gt;

&lt;p&gt;Use YAML for simple workflows.&lt;br&gt;
For complex ones, generate your YAML from a real language (Python, TypeScript, etc.) using a CDK-like tool.&lt;/p&gt;

&lt;p&gt;✅ Good for: small teams, standard builds, deploy-on-push workflows&lt;br&gt;
⚠️ Bad for: complex branching logic, reusable components, non-developers&lt;/p&gt;

&lt;p&gt;💡 What I’d like to see&lt;br&gt;
Built-in YAML validation in every CI tool&lt;/p&gt;

&lt;p&gt;More !include or extends without workarounds&lt;/p&gt;

&lt;p&gt;Optional scripting languages inside pipeline definitions (no more Bash string soup)&lt;/p&gt;

&lt;p&gt;Until then? YAML pipelines are like JavaScript — the worst form of CI/CD, except for all the others.&lt;/p&gt;

&lt;p&gt;What’s your take? Love it? Hate it? Built your own YAML generator to escape the madness? Drop your thoughts below. 👇&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Build Once, Deploy Many: Artifact Sharing Between Frontend &amp; Backend</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Sat, 16 May 2026 12:30:17 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/build-once-deploy-many-artifact-sharing-between-frontend-backend-1n</link>
      <guid>https://dev.to/sohanaakbar7/build-once-deploy-many-artifact-sharing-between-frontend-backend-1n</guid>
      <description>&lt;p&gt;Stop rebuilding. Start sharing.&lt;/p&gt;

&lt;p&gt;We’ve all been there. You fix a small UI bug, push the frontend, and your CI dutifully rebuilds the entire backend too. Or vice versa. Hours wasted. Caches invalidated. Patience gone.&lt;/p&gt;

&lt;p&gt;But what if you could build once and deploy anywhere?&lt;/p&gt;

&lt;p&gt;The Problem&lt;br&gt;
Most monorepos treat frontend and backend as separate build pipelines. Each deployment triggers:&lt;/p&gt;

&lt;p&gt;Fresh npm install&lt;/p&gt;

&lt;p&gt;Full compilation&lt;/p&gt;

&lt;p&gt;Asset generation&lt;/p&gt;

&lt;p&gt;Testing suites&lt;/p&gt;

&lt;p&gt;For a typical Next.js + Node backend, that’s 2–4 minutes per deploy. Multiply by daily commits → you’re burning 10+ hours monthly on redundant builds.&lt;/p&gt;

&lt;p&gt;The Solution: Shared Artifacts&lt;br&gt;
Artifact sharing means building a deployable package once, then promoting that same artifact across environments (dev, staging, production) and even across services.&lt;/p&gt;

&lt;p&gt;How It Works (Simple Example)&lt;br&gt;
yaml&lt;/p&gt;

&lt;h1&gt;
  
  
  .github/workflows/build.yml
&lt;/h1&gt;

&lt;p&gt;name: Build&lt;br&gt;
on: push&lt;/p&gt;

&lt;p&gt;jobs:&lt;br&gt;
  build:&lt;br&gt;
    runs-on: ubuntu-latest&lt;br&gt;
    steps:&lt;br&gt;
      - uses: actions/checkout@v3&lt;br&gt;
      - run: npm ci&lt;br&gt;
      - run: npm run build&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  # Upload shared artifact
  - uses: actions/upload-artifact@v3
    with:
      name: app-artifact
      path: |
        dist/
        node_modules/
        package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then in your deploy jobs:&lt;/p&gt;

&lt;p&gt;yaml&lt;br&gt;
deploy-frontend:&lt;br&gt;
  needs: build&lt;br&gt;
  uses: ./.github/workflows/deploy.yml&lt;br&gt;
  with:&lt;br&gt;
    artifact-name: app-artifact&lt;br&gt;
    target: frontend&lt;/p&gt;

&lt;p&gt;deploy-backend:&lt;br&gt;
  needs: build&lt;br&gt;
  uses: ./.github/workflows/deploy.yml&lt;br&gt;
  with:&lt;br&gt;
    artifact-name: app-artifact&lt;br&gt;
    target: backend&lt;br&gt;
Real Benefits&lt;br&gt;
✅ Faster CI — Build once in 90 seconds instead of twice in 4 minutes&lt;br&gt;
✅ Consistency — Same code hits every environment&lt;br&gt;
✅ Traceability — One commit hash = one artifact version&lt;br&gt;
✅ Atomic deploys — Frontend/backend always in sync&lt;/p&gt;

&lt;p&gt;When It Gets Tricky&lt;br&gt;
Environment-specific config — Don’t bake API keys into artifacts. Use runtime env vars or a config service.&lt;/p&gt;

&lt;p&gt;Different runtimes — Works best when frontend (Node) and backend share the same runtime. For static frontends, just share the asset manifest.&lt;/p&gt;

&lt;p&gt;Large artifacts — Cache node_modules separately. Upload only dist/ and a package.json.&lt;/p&gt;

&lt;p&gt;Pro Tips&lt;br&gt;
Use tarballs — npm pack your shared code into a .tgz file&lt;/p&gt;

&lt;p&gt;Leverage Docker layers — Build once, tag with commit SHA, deploy same image everywhere&lt;/p&gt;

&lt;p&gt;Artifact registry — Store builds in S3/Nexus/GitHub Packages for permanent reference&lt;/p&gt;

&lt;p&gt;Real-World Example&lt;br&gt;
At [Company X], we moved from 12 separate pipelines to 4 shared artifacts. Deployment time dropped from 22 minutes to 6 minutes. Developer frustration? Zero.&lt;/p&gt;

&lt;p&gt;Start small. Pick one shared module (UI components? validation logic?). Build once. Deploy everywhere.&lt;/p&gt;

&lt;p&gt;Your CI runner will thank you.&lt;/p&gt;

&lt;p&gt;What’s your biggest build bottleneck? Let me know in the comments. 👇&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Auto-cancel Redundant GitHub Actions — Save 40% Runner Time</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Fri, 15 May 2026 12:43:37 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/auto-cancel-redundant-github-actions-save-40-runner-time-2pnl</link>
      <guid>https://dev.to/sohanaakbar7/auto-cancel-redundant-github-actions-save-40-runner-time-2pnl</guid>
      <description>&lt;p&gt;If you’re paying for GitHub Actions minutes, you’re probably wasting money. Every time you push new commits while a previous workflow is still running, those old runs become pointless. But they keep burning through your runner time anyway.&lt;/p&gt;

&lt;p&gt;The fix is simple: auto-cancel redundant workflows.&lt;/p&gt;

&lt;p&gt;The Problem&lt;br&gt;
Picture this: You push a commit. Tests start running. Before they finish, you push a second commit with a tiny typo fix. Now two workflows are running simultaneously — one on an outdated commit you no longer care about.&lt;/p&gt;

&lt;p&gt;That first run is garbage. But GitHub still charges you for every minute of it.&lt;/p&gt;

&lt;p&gt;Multiply this across your team. Across multiple PRs. Across the entire day. You're easily looking at 30–50% wasted runner time.&lt;/p&gt;

&lt;p&gt;The Solution&lt;br&gt;
One YAML block inside your GitHub Actions workflow:&lt;/p&gt;

&lt;p&gt;yaml&lt;br&gt;
concurrency:&lt;br&gt;
  group: ${{ github.workflow }}-${{ github.ref }}&lt;br&gt;
  cancel-in-progress: true&lt;br&gt;
That’s it.&lt;/p&gt;

&lt;p&gt;group — creates a named queue per workflow + branch. Only one run from this group runs at a time.&lt;/p&gt;

&lt;p&gt;cancel-in-progress — kills the older run immediately when a new one arrives.&lt;/p&gt;

&lt;p&gt;Real-World Results&lt;br&gt;
At a previous company, we added this to all PR-triggered workflows. Our monthly runner minutes dropped from ~45,000 to ~27,000.&lt;/p&gt;

&lt;p&gt;40% saved. Zero effort after setup.&lt;/p&gt;

&lt;p&gt;Where to Use This&lt;br&gt;
✅ Pull request checks (test, lint, build)&lt;/p&gt;

&lt;p&gt;✅ Push-triggered workflows on the same branch&lt;/p&gt;

&lt;p&gt;✅ Deployment jobs where only the latest matters&lt;/p&gt;

&lt;p&gt;Where NOT to Use It&lt;br&gt;
❌ Release workflows (you want each to complete)&lt;/p&gt;

&lt;p&gt;❌ Scheduled workflows with overlapping logic&lt;/p&gt;

&lt;p&gt;❌ Matrix builds you actually need in parallel&lt;/p&gt;

&lt;p&gt;Pro Tip&lt;br&gt;
Combine with branch‑level rules:&lt;/p&gt;

&lt;p&gt;yaml&lt;br&gt;
concurrency:&lt;br&gt;
  group: ${{ github.workflow }}-${{ github.ref_name }}&lt;br&gt;
  cancel-in-progress: ${{ github.ref_name != 'main' }}&lt;br&gt;
This cancels on feature branches but protects your main branch builds.&lt;br&gt;
A three-line change. Five minutes of work. Potentially thousands of saved runner minutes per month.&lt;/p&gt;

&lt;p&gt;If you're paying for GitHub Actions, this is the lowest‑hanging fruit in your CI bill.&lt;/p&gt;

&lt;p&gt;Go add concurrency to your workflows today.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>github</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Zero-Downtime Deployments for a React + Node App</title>
      <dc:creator>Sohana Akbar</dc:creator>
      <pubDate>Thu, 14 May 2026 12:51:09 +0000</pubDate>
      <link>https://dev.to/sohanaakbar7/zero-downtime-deployments-for-a-react-node-app-o9b</link>
      <guid>https://dev.to/sohanaakbar7/zero-downtime-deployments-for-a-react-node-app-o9b</guid>
      <description>&lt;p&gt;Deploying a new version of your app without kicking users out or causing errors sounds like a dream, right? Let's make it real.&lt;/p&gt;

&lt;p&gt;The Problem&lt;br&gt;
You git push, run your build, restart the server... and for 5–30 seconds, your users see:&lt;/p&gt;

&lt;p&gt;Cannot GET /&lt;/p&gt;

&lt;p&gt;502 Bad Gateway&lt;/p&gt;

&lt;p&gt;Or just a blank white screen of sadness&lt;/p&gt;

&lt;p&gt;That's downtime. In 2025, it's not acceptable.&lt;/p&gt;

&lt;p&gt;The Setup&lt;br&gt;
We'll assume:&lt;/p&gt;

&lt;p&gt;Frontend: React (built with Vite or CRA)&lt;/p&gt;

&lt;p&gt;Backend: Node.js + Express&lt;/p&gt;

&lt;p&gt;Server: Linux (Ubuntu)&lt;/p&gt;

&lt;p&gt;Process manager: PM2&lt;/p&gt;

&lt;p&gt;The Strategy&lt;br&gt;
The goal: Old version keeps running until new version is ready.&lt;/p&gt;

&lt;p&gt;Here's the plan:&lt;/p&gt;

&lt;p&gt;Build the new React bundle&lt;/p&gt;

&lt;p&gt;Prepare the new Node backend&lt;/p&gt;

&lt;p&gt;Switch traffic atomically&lt;/p&gt;

&lt;p&gt;Handle in-flight requests gracefully&lt;/p&gt;

&lt;p&gt;Step 1: Build in a Staging Directory&lt;br&gt;
Don't overwrite your live files directly.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Clone fresh or pull latest
&lt;/h1&gt;

&lt;p&gt;git pull origin main&lt;/p&gt;

&lt;h1&gt;
  
  
  Build frontend to a timestamped folder
&lt;/h1&gt;

&lt;p&gt;TIMESTAMP=$(date +%s)&lt;br&gt;
npm run build -- --dest /var/www/builds/$TIMESTAMP&lt;/p&gt;

&lt;h1&gt;
  
  
  Copy backend files to a versioned folder
&lt;/h1&gt;

&lt;p&gt;cp -r backend /var/www/backends/$TIMESTAMP&lt;br&gt;
Step 2: Use PM2 Clustering (Zero-Downtime Reload)&lt;br&gt;
PM2 can reload your Node app one worker at a time.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Start with max CPU cores
&lt;/h1&gt;

&lt;p&gt;pm2 start backend/server.js -i max --name my-api&lt;/p&gt;

&lt;h1&gt;
  
  
  Later, to reload zero-downtime:
&lt;/h1&gt;

&lt;p&gt;pm2 reload my-api&lt;br&gt;
This keeps at least one process alive while others restart.&lt;/p&gt;

&lt;p&gt;Step 3: Atomic Symlink Swap for React&lt;br&gt;
Serve your React build via Nginx. Keep a current symlink.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Initial
&lt;/h1&gt;

&lt;p&gt;ln -sfn /var/www/builds/old /var/www/current&lt;/p&gt;

&lt;h1&gt;
  
  
  After new build ready
&lt;/h1&gt;

&lt;p&gt;ln -sfn /var/www/builds/$TIMESTAMP /var/www/current&lt;br&gt;
Nginx config:&lt;/p&gt;

&lt;p&gt;nginx&lt;br&gt;
location / {&lt;br&gt;
    root /var/www/current;&lt;br&gt;
    try_files $uri $uri/ /index.html;&lt;br&gt;
}&lt;br&gt;
The symlink change is instantaneous — no copying, no downtime.&lt;/p&gt;

&lt;p&gt;Step 4: Graceful Shutdown in Node&lt;br&gt;
Your Express app must handle SIGINT properly:&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
const server = app.listen(PORT);&lt;/p&gt;

&lt;p&gt;process.on('SIGINT', () =&amp;gt; {&lt;br&gt;
  console.log('Closing gracefully...');&lt;br&gt;
  server.close(() =&amp;gt; {&lt;br&gt;
    console.log('All connections closed');&lt;br&gt;
    process.exit(0);&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;// Force close after 10s&lt;br&gt;
  setTimeout(() =&amp;gt; process.exit(1), 10000);&lt;br&gt;
});&lt;br&gt;
Now PM2's reload won't kill active requests.&lt;/p&gt;

&lt;p&gt;Step 5: Put It All Together (Deploy Script)&lt;br&gt;
bash&lt;/p&gt;

&lt;h1&gt;
  
  
  !/bin/bash
&lt;/h1&gt;

&lt;p&gt;set -e&lt;/p&gt;

&lt;p&gt;TIMESTAMP=$(date +%s)&lt;/p&gt;

&lt;h1&gt;
  
  
  Pull code
&lt;/h1&gt;

&lt;p&gt;git pull origin main&lt;/p&gt;

&lt;h1&gt;
  
  
  Build backend
&lt;/h1&gt;

&lt;p&gt;cp -r backend /var/www/backends/$TIMESTAMP&lt;br&gt;
pm2 reload my-api --update-env&lt;/p&gt;

&lt;h1&gt;
  
  
  Build frontend
&lt;/h1&gt;

&lt;p&gt;npm run build&lt;br&gt;
cp -r dist /var/www/builds/$TIMESTAMP&lt;/p&gt;

&lt;h1&gt;
  
  
  Atomic swap
&lt;/h1&gt;

&lt;p&gt;ln -sfn /var/www/builds/$TIMESTAMP /var/www/current&lt;/p&gt;

&lt;h1&gt;
  
  
  Clean old builds (keep last 5)
&lt;/h1&gt;

&lt;p&gt;ls -1 /var/www/builds | head -n -5 | xargs -I {} rm -rf /var/www/builds/{}&lt;br&gt;
ls -1 /var/www/backends | head -n -5 | xargs -I {} rm -rf /var/www/backends/{}&lt;br&gt;
Step 6: Handle Database Migrations&lt;br&gt;
Migrations are tricky. Two safe approaches:&lt;/p&gt;

&lt;p&gt;Approach A (safer): Run migrations before deploying new code, ensuring backward compatibility.&lt;/p&gt;

&lt;p&gt;Approach B: Run migrations during a low-traffic window with a lock file.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// Before starting new version&lt;br&gt;
await runMigrations();&lt;br&gt;
Step 7: Session &amp;amp; Auth Gotcha&lt;br&gt;
If you store sessions in memory, users will be logged out after reload.&lt;/p&gt;

&lt;p&gt;Fix: Use Redis for sessions.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
const session = require('express-session');&lt;br&gt;
const RedisStore = require('connect-redis')(session);&lt;/p&gt;

&lt;p&gt;app.use(session({&lt;br&gt;
  store: new RedisStore({ host: 'localhost' }),&lt;br&gt;
  secret: 'keyboard cat',&lt;br&gt;
  resave: false&lt;br&gt;
}));&lt;br&gt;
Testing Zero-Downtime Locally&lt;br&gt;
Before shipping to prod:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Terminal 1: run app
&lt;/h1&gt;

&lt;p&gt;pm2 start server.js -i 2&lt;br&gt;
pm2 logs&lt;/p&gt;

&lt;h1&gt;
  
  
  Terminal 2: send constant traffic
&lt;/h1&gt;

&lt;p&gt;while true; do curl &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;; sleep 0.1; done&lt;/p&gt;

&lt;h1&gt;
  
  
  Terminal 3: reload
&lt;/h1&gt;

&lt;p&gt;pm2 reload server&lt;br&gt;
You should see zero errors and zero connection resets.&lt;/p&gt;

&lt;p&gt;Common Pitfalls&lt;br&gt;
Problem Solution&lt;br&gt;
WebSocket connections drop  Reconnect on client, or use Socket.IO with Redis adapter&lt;br&gt;
File uploads in progress    Increase graceful shutdown timeout&lt;br&gt;
React app cache issues  Use chunk hashing (Vite/CRA does this automatically)&lt;br&gt;
Environment variables not updated   Use pm2 reload --update-env&lt;br&gt;
The Result&lt;br&gt;
✅ Users stay logged in&lt;br&gt;
✅ No 502 errors&lt;br&gt;
✅ Deploy at 3 PM on a Tuesday&lt;br&gt;
✅ Sleep better at night&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;br&gt;
Zero-downtime isn't magic — it's just careful swapping of assets and graceful process handling.&lt;/p&gt;

&lt;p&gt;Start with the symlink trick for your React build + PM2 reload for Node. That covers 90% of apps.&lt;/p&gt;

&lt;p&gt;For larger systems, add a load balancer (like HAProxy or Nginx upstreams) and do blue-green deployments. But that's a story for another day.&lt;/p&gt;

&lt;p&gt;Have you tried this approach? What's your zero-downtime setup like? Drop a comment below!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>node</category>
      <category>react</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
