Docker ইমেজ ও Dockerfile
এই পাঠে যা শিখবেন
- Docker ইমেজ আসলে কী এবং কেন এটি অপরিবর্তনীয় (immutable)
- একটি Dockerfile-এর সাধারণ ইনস্ট্রাকশনসমূহ এবং প্রতিটি কীভাবে একটি নতুন লেয়ার তৈরি করে
- লেয়ার ক্যাশিং কীভাবে কাজ করে এবং কেন ইনস্ট্রাকশনের ক্রম গুরুত্বপূর্ণ
- ইমেজ ও কন্টেইনারের সম্পর্ক — Python দিয়ে একটি লেয়ার-ক্যাশ ইনভ্যালিডেশন সিমুলেশন
১ · ইমেজ কী
ইমেজDocker Imageএকটি অপরিবর্তনীয়, লেয়ারড টেমপ্লেট — অ্যাপ্লিকেশন কোড, রানটাইম, লাইব্রেরি ও কনফিগারেশন সবকিছু একসাথে ধারণ করে, যা থেকে কন্টেইনার চালু করা যায়। একবার তৈরি হওয়ার পর একটি ইমেজ পরিবর্তন করা যায় না (immutable) — কোনো পরিবর্তন দরকার হলে একটি নতুন ইমেজ বিল্ড করতে হয়। এই immutability-ই L22-এর "বিল্ড ওয়ান্স, রান এনিহোয়ার" গ্যারান্টি দেয় — একই ইমেজ যেকোনো জায়গায় চালালে ঠিক একই ফলাফল আসবে, কারণ এটি বদলাতে পারে না।
২ · Dockerfile — ইমেজ বিল্ড করার রেসিপি
DockerfileDockerfileএকটি টেক্সট ফাইল যাতে ধাপে ধাপে ইনস্ট্রাকশন লেখা থাকে — কোন বেস ইমেজ ব্যবহার হবে, কোন ফাইল কপি হবে, কী ইনস্টল হবে, এবং কন্টেইনার শুরু হলে কী কমান্ড চলবে। একটি সাধারণ Dockerfile-এর সাধারণ ইনস্ট্রাকশনগুলো —
বেস ইমেজ নির্ধারণ করে (যেমন একটি নির্দিষ্ট ভাষার রানটাইম) — সবসময় প্রথম ইনস্ট্রাকশন।
হোস্ট মেশিন থেকে ফাইল/ফোল্ডার ইমেজের ভেতরে কপি করে।
ইমেজ বিল্ড করার সময় একটি কমান্ড চালায় (যেমন ডিপেন্ডেন্সি ইনস্টল করা) — ফলাফল লেয়ারে সংরক্ষিত হয়।
কন্টেইনার চালু হওয়ার সময় কোন কমান্ড চলবে তা নির্ধারণ করে — বিল্ড টাইমে নয়, রানটাইমে চলে।
Dockerfile-এর প্রতিটি লাইন একটি স্বতন্ত্র, স্ট্যাক করা লেয়ার তৈরি করে — এবং প্রতিটি লেয়ার তার আগের সব লেয়ারের উপর নির্ভরশীল। Docker রিবিল্ডের সময় প্রতিটি ইনস্ট্রাকশনের একটি "কনটেন্ট হ্যাশ" হিসাব করে — যদি একটি ইনস্ট্রাকশন এবং তার আগের সব ইনস্ট্রাকশন অপরিবর্তিত থাকে, Docker সেই লেয়ারের পুরনো ফলাফল ক্যাশ থেকে পুনর্ব্যবহার করে, নতুন করে না চালিয়ে। কিন্তু যদি কোনো একটি আগের ইনস্ট্রাকশন বদলে যায়, তার পরের সব লেয়ার (এমনকি সেগুলো নিজেরা অপরিবর্তিত থাকলেও) ইনভ্যালিড হয়ে যায় — কারণ তাদের ফলাফল সেই বদলে যাওয়া লেয়ারের উপর নির্ভরশীল।
এই কারণেই ইনস্ট্রাকশন সাজানোর ক্রম গুরুত্বপূর্ণ একটি অপ্টিমাইজেশন — যে জিনিস সবচেয়ে কম ঘনঘন বদলায় (যেমন ডিপেন্ডেন্সি ইনস্টলেশন) তা আগে রাখা উচিত, আর যা সবচেয়ে বেশি ঘনঘন বদলায় (যেমন অ্যাপ্লিকেশনের নিজস্ব সোর্স কোড) তা সবার শেষে — যাতে সাধারণ কোড পরিবর্তনের সময় শুধু শেষের কয়েকটি লেয়ার রিবিল্ড হয়, পুরো ডিপেন্ডেন্সি ইনস্টলেশন আবার না চালাতে হয় (L25-এ আরও অপ্টিমাইজেশন কৌশল দেখব)।
৩ · ইমেজ বনাম কন্টেইনার — ক্লাস বনাম অবজেক্ট
একটি ইমেজ একটি স্ট্যাটিক টেমপ্লেট মাত্র — এটি নিজে থেকে কিছু "করে না"। একটি কন্টেইনার হলো সেই ইমেজের একটি চলমান ইনস্ট্যান্স — প্রোগ্রামিং-এ ক্লাস ও অবজেক্টের সম্পর্কের সাথে হুবহু তুলনীয়: একটি ইমেজ (ক্লাস) থেকে একসাথে একাধিক স্বতন্ত্র কন্টেইনার (অবজেক্ট) চালানো যায়, প্রতিটির নিজস্ব রানটাইম স্টেট (প্রসেস, মেমরি, ফাইলসিস্টেম পরিবর্তন) থাকে, কিন্তু সবাই একই অপরিবর্তনীয় বেস থেকে শুরু হয়।
# Dockerfile লেয়ার-ক্যাশিং সিমুলেশন — প্রতিটি ইনস্ট্রাকশনের একটি "content hash" থাকে
def build(instructions, cache):
"""cache: {layer_index: (hash_at_that_point, result)} — আগের বিল্ড থেকে সংরক্ষিত।"""
results = []
prefix_hash = ""
cache_valid = True # একবার একটি লেয়ার ইনভ্যালিড হলে, তার পরের সব লেয়ারও ইনভ্যালিড
for i, instr in enumerate(instructions):
prefix_hash += instr["hash"] # ক্রমবর্ধমান হ্যাশ — এই পয়েন্ট পর্যন্ত সব ইনস্ট্রাকশনের সমষ্টি
cached = cache.get(i)
if cache_valid and cached is not None and cached[0] == prefix_hash:
results.append((instr["name"], "CACHE থেকে পুনর্ব্যবহৃত"))
else:
cache_valid = False # এই লেয়ার থেকে বাকি সব রিবিল্ড হবে
results.append((instr["name"], "নতুন করে বিল্ড হলো"))
cache[i] = (prefix_hash, instr["name"])
return results
base_instructions = [
{"name": "FROM python:3.12-slim", "hash": "h1"},
{"name": "RUN pip install -r requirements.txt", "hash": "h2"},
{"name": "COPY app/ /app/", "hash": "h3"},
{"name": "CMD [\"python\", \"main.py\"]", "hash": "h4"},
]
cache = {}
print("== কোল্ড বিল্ড (কোনো ক্যাশ নেই) ==")
for name, status in build(base_instructions, cache):
print(f" {name:38}{status}")
# পরিস্থিতি ১: শুধু শেষ ইনস্ট্রাকশন বদলেছে (নতুন CMD) — আগের লেয়ারগুলো পুনর্ব্যবহৃত হওয়া উচিত
changed_last = [dict(i) for i in base_instructions]
changed_last[3] = {"name": "CMD [\"python\", \"main.py\", \"--prod\"]", "hash": "h4b"}
print("\n== রিবিল্ড: শুধু শেষ ইনস্ট্রাকশন বদলেছে ==")
for name, status in build(changed_last, dict(cache)):
print(f" {name:38}{status}")
# পরিস্থিতি ২: একটি প্রথম দিকের ইনস্ট্রাকশন বদলেছে (নতুন requirements) — এর পরের সব ইনভ্যালিড হওয়া উচিত
changed_early = [dict(i) for i in base_instructions]
changed_early[1] = {"name": "RUN pip install -r requirements.txt", "hash": "h2b"}
print("\n== রিবিল্ড: প্রথম দিকের একটি ইনস্ট্রাকশন বদলেছে ==")
for name, status in build(changed_early, dict(cache)):
print(f" {name:38}{status}")
ইমেজ = অপরিবর্তনীয় টেমপ্লেট, Dockerfile = সেই টেমপ্লেট বিল্ড করার রেসিপি, কন্টেইনার = সেই টেমপ্লেটের একটি চলমান ইনস্ট্যান্স। লেয়ার ক্যাশিং ইনস্ট্রাকশনের ক্রমের উপর নির্ভরশীল — কম-পরিবর্তনশীল ইনস্ট্রাকশন আগে, বেশি-পরিবর্তনশীল ইনস্ট্রাকশন পরে রাখাই দ্রুত রিবিল্ডের মূল কৌশল।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি Dockerfile-এ যদি অ্যাপ্লিকেশনের সোর্স কোড কপি করার ইনস্ট্রাকশন (COPY) ডিপেন্ডেন্সি ইনস্টল করার (RUN pip install) আগে রাখা হয়, তাহলে ক্যাশিং-এর দিক থেকে কী সমস্যা হবে?
সোর্স কোড সবচেয়ে ঘনঘন বদলায় (প্রতিটি ছোট ফিচার বা বাগফিক্সে)। যদি COPY ডিপেন্ডেন্সি ইনস্টলের আগে থাকে, তাহলে প্রতিবার কোড বদলানোর সাথে সাথে COPY-র হ্যাশ বদলে যাবে, এবং cascade invalidation নিয়ম অনুযায়ী তার পরের RUN pip install লেয়ারও ইনভ্যালিড হয়ে যাবে — অর্থাৎ প্রতিটি ছোট কোড পরিবর্তনে সম্পূর্ণ ডিপেন্ডেন্সি ইনস্টলেশন আবার চালাতে হবে, যা রিবিল্ড সময় অপ্রয়োজনীয়ভাবে অনেক বাড়িয়ে দেবে।
প্র ০২ একটি ইমেজ "অপরিবর্তনীয়" (immutable) — এই বৈশিষ্ট্যটি L20-এর idempotency ধারণার সাথে কীভাবে সম্পর্কিত?
একটি অপরিবর্তনীয় ইমেজ থেকে চালানো একটি কন্টেইনার সবসময় ঠিক একই প্রারম্ভিক অবস্থা থেকে শুরু হবে, প্রতিবার — এটি একধরনের কাঠামোগত idempotency দেয়: একই ইমেজ থেকে বারবার ডিপ্লয় করলে ঠিক একই ফলাফল আসবে, বিদ্যমান অবস্থার উপর নির্ভর করে ফলাফল বদলাবে না। এটি L39-এর "immutable infrastructure" দর্শনের ভিত্তি — সার্ভার ম্যানুয়ালি প্যাচ করার বদলে একটি নতুন ইমেজ বিল্ড করে পুরনো ইনস্ট্যান্স প্রতিস্থাপন করা।
প্র ০৩ একই ইমেজ থেকে একই সময়ে ১০টি কন্টেইনার চালু করলে সেগুলো কি একে অপরের ফাইলসিস্টেম পরিবর্তন দেখতে পাবে? কেন বা কেন নয়?
না — প্রতিটি কন্টেইনার তার নিজস্ব আলাদা লেখার-যোগ্য ফাইলসিস্টেম লেয়ার পায় (ইমেজের অপরিবর্তনীয় লেয়ারের উপরে একটি পাতলা লেখার-যোগ্য লেয়ার যোগ হয়, প্রতিটি কন্টেইনারের জন্য আলাদা)। ফলে একটি কন্টেইনারের ভেতরে একটি ফাইল বদলালে বা তৈরি করলে তা শুধু সেই নির্দিষ্ট কন্টেইনারের মধ্যেই থাকে — বাকি ৯টি একই মূল ইমেজ শেয়ার করলেও, একে অপরের রানটাইম পরিবর্তন দেখতে পায় না। এটিই L24-এ দেখা "কন্টেইনারের ফাইলসিস্টেম পরিবর্তন ক্ষণস্থায়ী" ধারণার ভিত্তি।
অনুশীলন
-
চিন্তা করুন: আপনার পরিচিত একটি অ্যাপ্লিকেশনের জন্য একটি কাল্পনিক Dockerfile-এর ইনস্ট্রাকশন ক্রম লিখুন (FROM, RUN, COPY, CMD) — সবচেয়ে কম-পরিবর্তনশীল থেকে সবচেয়ে বেশি-পরিবর্তনশীল ক্রমে সাজিয়ে।
একটি সাধারণ ক্রম হবে — FROM (বেস ইমেজ, প্রায় কখনো বদলায় না) → RUN (সিস্টেম প্যাকেজ/ডিপেন্ডেন্সি ইনস্টল, মাঝে মাঝে বদলায়) → COPY (অ্যাপ্লিকেশন সোর্স কোড, সবচেয়ে ঘনঘন বদলায়) → CMD (স্টার্টআপ কমান্ড, প্রায় স্থির)। মূল নীতি হলো: যা কম বদলায় তা উপরে, যা বেশি বদলায় তা নিচে।
-
পরীক্ষা করুন: উপরের কোড সেলে একটি চতুর্থ পরিস্থিতি যোগ করুন যেখানে একদম প্রথম ইনস্ট্রাকশনও (FROM) বদলায় — এবং যাচাই করুন এতে কি সবকটি লেয়ারই রিবিল্ড হয়।
হ্যাঁ — যেহেতু prefix_hash ক্রমবর্ধমান (প্রতিটি লেয়ারের হ্যাশ তার আগের সব ইনস্ট্রাকশনের হ্যাশের উপর নির্ভরশীল), FROM বদলালে প্রথম লেয়ার থেকেই hash mismatch হবে, এবং cache_valid সাথে সাথে False হয়ে যাবে — ফলে RUN, COPY, CMD সবকটিই "নতুন করে বিল্ড হলো" দেখাবে, একটিও ক্যাশ থেকে পুনর্ব্যবহৃত হবে না। এটি সবচেয়ে খারাপ কেস — বেস ইমেজ পরিবর্তন সবসময় একটি সম্পূর্ণ কোল্ড রিবিল্ড দাবি করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ পরবর্তী পাঠ — Docker কন্টেইনার লাইফসাইকেল ও নেটওয়ার্কিং — এই মডিউলেই যুক্ত।
- Cybersecurity & Ethical Hacking কোর্স সঙ্গী কোর্স ইমেজ সিকিউরিটি স্ক্যানিং বিস্তারিত জানতে L26 ও এই কোর্স দেখুন।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity ও Cloud Computing & DevOps — সব এক জায়গায়।