Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
DevOpsJune 21, 20263 min read

Docker optimization: Mastering Multi-Stage Builds and GitHub Actions

Docker optimization doesn't have to be a headache. Learn how to use multi-stage builds and GitHub Actions remote caching to slash your deployment times.

DockerCI/CDGitHub ActionsDevOpsContainersLinux

My team’s CI pipeline was dragging. What started as a snappy three-minute build had ballooned into a twelve-minute slog, mostly because we were rebuilding our entire node_modules directory every single time a developer pushed a line of CSS. If you're tired of watching your deployment pipeline stall while Docker re-downloads half the internet, it’s time to take control of your build process.

The Problem with Naive Builds

When you build a Docker image, the engine processes your Dockerfile line by line. Each RUN, COPY, or ADD instruction creates a new layer. If you change a file early in the process—like your package.json or go.mod—Docker invalidates that layer and every layer that follows.

We initially tried to "fix" this by just throwing more compute at our GitHub Actions runners. It worked for about two weeks. Then our images grew, our dependencies multiplied, and we were back to square one, only now we were paying more for the privilege of waiting. Real Docker optimization isn't about faster hardware; it's about smarter layer management.

Implementing Multi-Stage Builds

The first step in any serious CI/CD performance strategy is the multi-stage build. Instead of shipping your build tools, compilers, and source code into your production image, you split the process.

Think of it like a construction site: you bring in the cranes and scaffolding to build the skyscraper, but you remove them before the tenants move in.

Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Production
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

By separating the build environment from the runtime environment, you drastically reduce the attack surface and the image size. More importantly, it keeps your production image clean and lightweight.

Scaling with Remote Layer Caching

Even with multi-stage builds, GitHub Actions runners are ephemeral. They start from scratch every time, meaning your local Docker cache is gone the moment the job finishes. You’re essentially cold-starting your build on every PR.

To fix this, you need to implement a container build cache that persists between runs. The docker/build-push-action in GitHub Actions supports this natively using the gha (GitHub Actions) cache backend.

Here is how we configure it in our workflow files:

YAML
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: my-app:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

The mode=max flag is the secret sauce. By default, Docker only caches the final image layers. Setting this to max tells Docker to cache intermediate layers from every stage of your build. This turned our average build time from 12 minutes down to roughly 2 minutes and 30 seconds.

Common Pitfalls to Avoid

Don't fall into the trap of blindly caching everything. If your build process relies on environment variables that change frequently, you might end up with stale cache hits that cause hard-to-debug runtime errors.

  1. Ordering matters: Always copy your dependency manifest files (package.json, go.mod, etc.) before your source code. This ensures that changing a line of business logic doesn't trigger a full dependency re-install.
  2. Layer Bloat: Be careful with RUN commands. Every RUN creates a layer. Use && to chain commands and clean up temporary files in the same layer.
  3. The mode=max tax: While caching every intermediate layer speeds up builds, it does consume more storage space on GitHub’s backend. Keep an eye on your repository's cache usage.

Is This Enough?

Honestly, this setup covers about 90% of the performance issues I see in production environments. If you’re still seeing slow builds after implementing multi-stage builds and remote caching, you might need to look at your base image selection. Switching from a full Ubuntu image to an Alpine or Distroless image can shave off another 100MB+ from your image size, which speeds up the push/pull part of your deployment cycle.

I’m still experimenting with BuildKit’s advanced features, like inline cache metadata, to see if we can get our deployment times even lower. It’s an ongoing process. Don’t strive for perfection; strive for a build pipeline that doesn't make you want to grab a coffee every time you push a hotfix.

Back to Blog

Similar Posts

DevOpsJune 21, 20264 min read

GitHub Actions Self-Hosted Runners: Scaling Ephemeral Docker Containers

GitHub Actions self-hosted runners don't have to be permanent servers. Learn how to build ephemeral, auto-scaling Docker runners to save time and money.

Read more
Close-up of colorful programming code on a computer screen, showcasing digital technology.
DevOpsJune 20, 2026
4 min read

Zero-downtime deploy with GitHub Actions: A Practical Guide

Achieve a zero-downtime deploy with GitHub Actions using blue-green strategies. Learn how to keep your services running seamlessly during every release.

Read more
Shipping containers and cranes at Hamburg port showcasing global trade.
DevOpsJune 20, 20265 min read

Docker for app developers: A mental model that sticks

Master Docker for app developers by shifting your mental model. Learn how containers actually work, why they aren't VMs, and how to build efficient images.

Read more