পাঠ ২৫ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Cloud Computing & DevOps / মাল্টি-স্টেজ বিল্ড

মাল্টি-স্টেজ বিল্ড ও ইমেজ অপ্টিমাইজেশন

Multi-stage builds & image optimization
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • নাইভ সিঙ্গেল-স্টেজ Dockerfile-এর ইমেজ ফুলে ওঠার আসল কারণ
  • মাল্টি-স্টেজ বিল্ড কীভাবে বিল্ড-টুল ও রানটাইম আলাদা করে
  • মিনিমাল বেস ইমেজ ও .dockerignore-এর ভূমিকা
  • Python দিয়ে সিঙ্গেল-স্টেজ বনাম মাল্টি-স্টেজ ইমেজের সাইজ তুলনা ও সাশ্রয়ের হিসাব

১ · সমস্যা — নাইভ সিঙ্গেল-স্টেজ বিল্ডে ইমেজ ফুলে ওঠা

একটি অ্যাপ্লিকেশন বিল্ড করতে প্রায়ই কম্পাইলার, হেডার ফাইল, ডেভেলপমেন্ট-টাইম প্যাকেজের মতো টুল প্রয়োজন হয় — কিন্তু সেই অ্যাপ্লিকেশনটি চালাতে সেসবের প্রয়োজন হয় না। একটি নাইভ, একক-স্টেজ Dockerfile যদি একই স্টেজে বিল্ড ও রান দুটোই করে, তাহলে সেই বিল্ড-টুলগুলো অজান্তেই চূড়ান্ত ইমেজে থেকে যায় — ইমেজ প্রয়োজনের চেয়ে অনেক বড় হয়ে যায়।

বড় ইমেজের বাস্তব খরচ — ধীর ডিপ্লয় (রেজিস্ট্রি থেকে ডাউনলোড করতে বেশি সময়, ওই কারণে L37-এর ডিপ্লয়মেন্ট স্ট্র্যাটেজিগুলো ধীর হয়ে যায়), এবং একটি বড় অ্যাটাক সারফেস (অপ্রয়োজনীয় প্রতিটি প্যাকেজ সম্ভাব্য একটি নতুন দুর্বলতা — Cybersecurity কোর্সের মিনিমাল-অ্যাটাক-সারফেস নীতির সরাসরি প্রয়োগ, L26-এর ইমেজ স্ক্যানিং-এও এর প্রভাব দেখব)।

২ · সমাধান — মাল্টি-স্টেজ বিল্ড

মাল্টি-স্টেজ বিল্ডMulti-stage Buildএকটি Dockerfile-এ একাধিক FROM ইনস্ট্রাকশন ব্যবহার করে একাধিক স্বতন্ত্র "স্টেজ" সংজ্ঞায়িত করা — একটি স্টেজে বিল্ড করা হয়, শুধু চূড়ান্ত আর্টিফ্যাক্ট পরের স্টেজে কপি করা হয়। একটি Dockerfile-এ একাধিক FROM ইনস্ট্রাকশন থাকতে পারে, প্রতিটি একটি স্বতন্ত্র "স্টেজ" শুরু করে —

বিল্ড স্টেজ
সব কম্পাইলার, ডেভ-প্যাকেজ ও বিল্ড-টুল ধারণকারী একটি ভারী বেস ইমেজ ব্যবহার করে অ্যাপ্লিকেশন কম্পাইল/বিল্ড করে।
রানটাইম স্টেজ
একটি নতুন, মিনিমাল বেস ইমেজ থেকে শুরু করে COPY --from=build_stage দিয়ে শুধু চূড়ান্ত কম্পাইল করা আর্টিফ্যাক্ট কপি করে।

চূড়ান্ত ইমেজ (যা আসলে পুশ ও ডিপ্লয় হয়) শুধু রানটাইম স্টেজ থেকেই তৈরি হয় — বিল্ড স্টেজের কম্পাইলার/ডেভ-প্যাকেজ কখনোই চূড়ান্ত ইমেজে যায় না, কারণ Docker শুধু শেষ স্টেজটিকেই চূড়ান্ত ইমেজ হিসেবে ট্যাগ করে।

অন্যান্য অপ্টিমাইজেশন প্র্যাকটিস

মিনিমাল বেস ইমেজ ব্যবহার করা (যেমন একটি সম্পূর্ণ OS ইমেজের বদলে "slim" বা "alpine" সংস্করণ, যেখানে অপ্রয়োজনীয় অনেক প্যাকেজ আগে থেকেই বাদ দেওয়া থাকে); লেয়ার-ক্যাশ সচেতনভাবে ইনস্ট্রাকশন অর্ডার করা (L23-এর কৌশল পুনরায় প্রয়োগ); এবং .dockerignore ফাইল ব্যবহার করে অপ্রয়োজনীয় ফাইল (যেমন লোকাল গিট হিস্ট্রি, টেস্ট ডেটা, ডেভেলপমেন্ট কনফিগ) বিল্ড কনটেক্সট থেকেই বাদ দেওয়া, যাতে সেগুলো ভুলবশত ইমেজে ঢুকে না যায়।

Python
# নাইভ সিঙ্গেল-স্টেজ বনাম মাল্টি-স্টেজ ইমেজ — সাইজ তুলনা (illustrative MB, বাস্তব বেঞ্চমার্ক নয়)
naive_single_stage = {
    "বেস OS": 120,
    "কম্পাইলার ও বিল্ড-টুল": 350,
    "ডেভ হেডার/প্যাকেজ": 180,
    "অ্যাপ্লিকেশন রানটাইম": 90,
    "চূড়ান্ত অ্যাপ্লিকেশন কোড": 15,
}

multi_stage_runtime_only = {
    "বেস OS (মিনিমাল/slim)": 25,
    "অ্যাপ্লিকেশন রানটাইম": 90,
    "চূড়ান্ত অ্যাপ্লিকেশন কোড": 15,
}

naive_total = sum(naive_single_stage.values())
optimized_total = sum(multi_stage_runtime_only.values())
savings_pct = (naive_total - optimized_total) / naive_total * 100

print("== নাইভ সিঙ্গেল-স্টেজ ইমেজ ==")
for component, size_mb in naive_single_stage.items():
    print(f"  {component:28}{size_mb:>5} MB")
print(f"  {'মোট':28}{naive_total:>5} MB")

print("\n== মাল্টি-স্টেজ (শুধু রানটাইম স্টেজ) ==")
for component, size_mb in multi_stage_runtime_only.items():
    print(f"  {component:28}{size_mb:>5} MB")
print(f"  {'মোট':28}{optimized_total:>5} MB")

print(f"\nইমেজ সাইজ কমেছে: {naive_total - optimized_total} MB ({savings_pct:.1f}%)")

    
লক্ষ্য করুন — কম্পাইলার ও ডেভ-প্যাকেজ (৫৩০ MB মিলিয়ে) সম্পূর্ণভাবে বাদ পড়ে যাওয়ায় ইমেজ প্রায় ৮৩% ছোট হয়ে যায় (৭৫৫ MB থেকে মাত্র ১৩০ MB-তে নেমে আসে), অথচ চূড়ান্ত অ্যাপ্লিকেশনের কার্যকারিতা একবিন্দুও বদলায় না — কারণ রানটাইমে কম্পাইলার আসলে কখনোই দরকার হয় না, শুধু বিল্ড-টাইমে দরকার হয়েছিল।
মূল কথা · Key takeaway

মাল্টি-স্টেজ বিল্ড বিল্ড-টাইম নির্ভরতা ও রানটাইম নির্ভরতাকে স্পষ্টভাবে আলাদা করে — শুধু যা সত্যিই রানটাইমে দরকার তা-ই চূড়ান্ত ইমেজে যায়। ছোট ইমেজ মানে দ্রুত ডিপ্লয়, কম স্টোরেজ খরচ, এবং কম অ্যাটাক সারফেস — একইসাথে তিনটি সুবিধা, প্রায় বিনামূল্যে (শুধু Dockerfile-এ কয়েকটি অতিরিক্ত লাইন লিখতে হয়)।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ মাল্টি-স্টেজ বিল্ডে বিল্ড স্টেজে ব্যবহৃত ভারী বেস ইমেজ কেন সমস্যা নয়, যদিও সেটি নিজে অনেক বড়?

বিল্ড স্টেজের ইমেজ শুধু বিল্ড প্রক্রিয়ার সময়েই ব্যবহৃত হয় (CI পাইপলাইনে বা ডেভেলপারের মেশিনে) — এটি কখনো রেজিস্ট্রিতে পুশ হয় না, ডিপ্লয় হয় না, এবং প্রোডাকশন সার্ভারেও চলে না। শুধু চূড়ান্ত রানটাইম স্টেজের ইমেজটিই আসলে ডিপ্লয় ও ডিস্ট্রিবিউট হয় — তাই বিল্ড স্টেজ যত বড়ই হোক, তা ডিপ্লয়মেন্ট সাইজ বা অ্যাটাক সারফেসকে প্রভাবিত করে না।

প্র ০২ .dockerignore ফাইল না থাকলে বিল্ড কনটেক্সটে কী ধরনের অনিচ্ছাকৃত ঝুঁকি তৈরি হতে পারে (L40-এর সিক্রেট ম্যানেজমেন্টের সাথে সম্পর্ক)?

.dockerignore ছাড়া, লোকাল ডিরেক্টরিতে থাকা যেকোনো ফাইল (এমনকি .env ফাইল, লোকাল ক্রেডেনশিয়াল, বা .git ইতিহাস) দুর্ঘটনাক্রমে COPY . .-এর মতো একটি ইনস্ট্রাকশনের মাধ্যমে ইমেজে ঢুকে যেতে পারে — এবং একবার ইমেজের একটি লেয়ারে ঢুকে গেলে, সেই সিক্রেট ইমেজের হিস্ট্রিতে থেকে যায় এমনকি পরে "মুছে ফেলার" চেষ্টা করলেও (L40-এর মূল সতর্কতা: একবার কমিট/বিল্ড হয়ে গেলে, "রিমুভ" করাও পুরোপুরি নিরাপদ নয়) — তাই .dockerignore একটি সাধারণ কিন্তু গুরুত্বপূর্ণ প্রতিরক্ষামূলক অভ্যাস।

প্র ০৩ একটি ছোট ইমেজ সাইজ কীভাবে সরাসরি L37-এর ডিপ্লয়মেন্ট স্ট্র্যাটেজিগুলোর (রোলিং, ব্লু-গ্রিন) কার্যকারিতা বাড়ায়?

রোলিং আপডেট বা ব্লু-গ্রিন — দুটোতেই নতুন ইমেজ প্রতিটি নোড/ইনস্ট্যান্সে ডাউনলোড করতে হয় ডিপ্লয়মেন্ট সম্পূর্ণ হওয়ার আগে। একটি বড় ইমেজ ডাউনলোড হতে বেশি সময় নেয় — মানে পুরো রোলআউট ধীর হয়, এবং ব্লু-গ্রিনের ক্ষেত্রে "green" পরিবেশ প্রস্তুত হতে বেশি সময় লাগে (রোলব্যাক দরকার হলে সেই সময়টাই বিলম্ব)। ছোট ইমেজ মানে দ্রুততর রোলআউট এবং দ্রুততর রোলব্যাক — উভয় দিক থেকেই একটি অপারেশনাল সুবিধা।

অনুশীলন

  1. চিন্তা করুন: একটি অ্যাপ্লিকেশন যদি একটি ইন্টারপ্রেটেড ভাষায় লেখা হয় (যেমন Python, যেখানে কোনো আলাদা কম্পাইল ধাপ নেই), তাহলে কি মাল্টি-স্টেজ বিল্ডের প্রয়োজন আছে?

    তুলনামূলক কম প্রয়োজন হলেও, তবু উপকারী হতে পারে — অনেক Python প্যাকেজের কিছু ডিপেন্ডেন্সি C এক্সটেনশন হিসেবে কম্পাইল করা লাগে (যার জন্য কম্পাইলার দরকার), যা রানটাইমে অপ্রয়োজনীয়। তাই একটি "বিল্ড স্টেজ" সেই ডিপেন্ডেন্সিগুলো কম্পাইল করতে পারে, আর একটি "রানটাইম স্টেজ" শুধু কম্পাইল করা প্যাকেজ ও ইন্টারপ্রেটার রাখতে পারে — কম্পাইলার নিজে বাদ দিয়ে। সম্পূর্ণ ইন্টারপ্রেটেড, কোনো নেটিভ ডিপেন্ডেন্সি ছাড়া একটি অ্যাপে সুবিধা কম, কিন্তু বাস্তবে বেশিরভাগ প্রোডাকশন অ্যাপ্লিকেশনেরই কিছু না কিছু কম্পাইল-প্রয়োজনীয় ডিপেন্ডেন্সি থাকে।

  2. পরীক্ষা করুন: উপরের কোড সেলে naive_single_stage-এ একটি নতুন এন্ট্রি "টেস্ট ফ্রেমওয়ার্ক" যোগ করুন (৮০ MB) এবং নতুন সাশ্রয়ের শতাংশ হিসাব করুন।

    নতুন মোট হবে ৮৩৫ MB (৭৫৫ + ৮০), আর সাশ্রয়ের শতাংশ আরও বেড়ে যাবে (যেহেতু multi_stage_runtime_only অপরিবর্তিত থাকে) — এটি বাস্তব প্রবণতাকেই প্রতিফলিত করে: একটি নাইভ সিঙ্গেল-স্টেজ ইমেজে যত বেশি অপ্রয়োজনীয় ডেভ-টাইম টুল (টেস্ট ফ্রেমওয়ার্ক, লিন্টার ইত্যাদি) জমা হয়, মাল্টি-স্টেজ বিল্ডের সাশ্রয় তত বেশি তাৎপর্যপূর্ণ হয়ে ওঠে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
Docker কন্টেইনার লাইফসাইকেল ও নেটওয়ার্কিং