Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
Docker

Reduce Docker Image Sizes by 10x with Multi-Stage Builds

- 01.02.26 | 01.02.26 - ErcanOPAK

Your Docker image is 1.2GB when it should be 120MB? Multi-stage builds eliminate build dependencies from your final image.

The Problem – Single Stage Build:

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install  # Installs 300MB of dev dependencies
COPY . .
RUN npm run build  # Creates 10MB production build
CMD ["node", "dist/server.js"]

# Result: 1.2GB image (Node base + dependencies + source + build artifacts)

The Solution – Multi-Stage Build:

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

# Stage 2: Production
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --production  # Only production dependencies
CMD ["node", "dist/server.js"]

# Result: 180MB image (Slim base + production deps + compiled code)

Why This Works:
Multi-stage builds use multiple FROM statements. Each FROM starts a new build stage. COPY –from=STAGE lets you selectively copy artifacts from earlier stages. Everything from the ‘builder’ stage (source code, dev dependencies, intermediate files) is discarded in the final image.

Real Example – C# Application:

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]

# Result: 220MB vs 1.8GB with SDK image

The Math:
.NET SDK image: 1.8GB (includes compilers, dev tools, debug symbols)
ASP.NET runtime image: 180MB (just the runtime)
Your app: 40MB (compiled DLLs)
Final image: 220MB = 87% size reduction

Pro Tip – Use Alpine for Even Smaller Images:

FROM node:18-alpine  # 175MB instead of 1GB
FROM python:3.11-alpine  # 50MB instead of 900MB
FROM nginx:alpine  # 23MB instead of 135MB

Alpine Linux uses musl libc instead of glibc, cutting base OS size by 90%. Only downside: some packages have compatibility issues with musl.

Related posts:

Why Docker Containers Randomly Slow Down After Days (Even With Low Traffic)

Docker Compose: Launch Full Stack Apps with One Command (Node + Redis + Postgres)

Why Docker Containers Get Slower Over Time (Even Without Traffic)

Post Views: 3

Post navigation

Debug Kubernetes Pods That Keep Crashing Before Logs Disappear
AI Prompt: Generate Production-Ready API Documentation from Code Comments

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

February 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
232425262728  
« Jan    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)
  • Add Constraint to SQL Table to ensure email contains @ (572)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (552)
  • Average of all values in a column that are not zero in SQL (517)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (473)
  • Find numbers with more than two decimal places in SQL (436)

Recent Posts

  • C#: Use init Accessor to Create Immutable Objects Without Constructor Boilerplate
  • C#: Use Index and Range Operators for Cleaner Array Slicing
  • C#: Use Null-Coalescing Assignment to Simplify Lazy Initialization
  • SQL: Use CHAR Instead of VARCHAR for Fixed-Length Columns to Save Space
  • SQL: Use CROSS APPLY Instead of Subqueries for Better Performance
  • .NET Core: Use Required Modifier to Force Property Initialization
  • .NET Core: Use Global Using Directives to Avoid Repeating Imports
  • Git: Use git restore to Unstage Files Without Losing Changes
  • Git: Use git bisect to Find Which Commit Introduced a Bug
  • AJAX: Use Fetch with Signal to Cancel Requests When User Navigates Away

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (934)
  • How to add default value for Entity Framework migrations for DateTime and Bool (830)
  • Get the First and Last Word from a String or Sentence in SQL (822)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (708)

Recent Posts

  • C#: Use init Accessor to Create Immutable Objects Without Constructor Boilerplate
  • C#: Use Index and Range Operators for Cleaner Array Slicing
  • C#: Use Null-Coalescing Assignment to Simplify Lazy Initialization
  • SQL: Use CHAR Instead of VARCHAR for Fixed-Length Columns to Save Space
  • SQL: Use CROSS APPLY Instead of Subqueries for Better Performance

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com