Foundationপ্রথম নীতি থেকে
LEVEL 12লেসন ১০/২২অ্যাডভান্সড৫৫ মিনিট

Container Runtime ও OCI — docker run থেকে kernel syscall পর্যন্ত সম্পূর্ণ পথ

Container Runtime and OCI: From docker run to the Kernel Syscalls

এই module-এর আগের লেসনগুলো namespace, cgroup, আর overlay filesystem-এর ভেতরের kernel mechanics আলাদা আলাদাভাবে দেখিয়েছে — এই লেসন সেগুলোকে একসাথে জোড়া দিয়ে module-এর কেন্দ্রীয় প্রশ্নের উত্তর দেয়: docker run লিখলে ঠিক কোন সফটওয়্যার-স্তর, কোন ক্রমে, কোন syscall চালায়। OCI-এর Image আর Runtime spec-এর বিভাজন, runc-এর one-shot প্রকৃতি, আর containerd/containerd-shim-এর persistent daemon architecture — এই তিনটা মিলেই সেই সম্পূর্ণ উত্তর, একটা হাতে-চালানো Python simulation দিয়ে verify করা।

এই লেসন শেষে আপনি পারবেন

  • OCI (Open Container Initiative)-এর দুইটা আলাদা spec — Image Specification আর Runtime Specification — এর মধ্যে সঠিক বিভাজনরেখা টেনে, প্রতিটা ঠিক কী standardize করে তা ব্যাখ্যা করতে পারবেন।
  • runc ঠিক কী কাজ করে (config.json পড়া, namespace/cgroup/mount syscall চালানো, execve() দিয়ে নিজের প্রসেস-image প্রতিস্থাপন) আর কেন এটা একটা one-shot প্রসেস-লঞ্চার, daemon না — তা প্রমাণ করে বলতে পারবেন।
  • containerd কেন runc-এর ওপরে একটা persistent daemon হিসেবে দরকার — container lifecycle management, image pull/unpack, docker logs/docker exec, restart policy — যুক্তিসহ ব্যাখ্যা করতে পারবেন, আর containerd-shim ঠিক কোন সমস্যাটা সমাধান করে তা বলতে পারবেন।
  • docker run থেকে শুরু করে dockerd → containerd → containerd-shim → runc → kernel পর্যন্ত সম্পূর্ণ trace, exact syscall নামসহ (clone(), pivot_root(), execve()), ধাপে ধাপে বর্ণনা করতে পারবেন — এই module-এর কেন্দ্রীয় প্রশ্নের সরাসরি উত্তর হিসেবে।
  • একটা toy OCI config.json-জাতীয় dict থেকে বাস্তব runtime যে syscall sequence চালাবে তা হাতে-কলমে অনুমান ও simulate করতে পারবেন, আর কেন OCI-র ঐতিহাসিক standardization পুরো container ecosystem-এর জন্য গুরুত্বপূর্ণ ছিল তা যুক্তি দিতে পারবেন।

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

এই module-এর প্রতিটা আগের লেসন একটা নির্দিষ্ট, সংকীর্ণ প্রশ্নে গভীরে গিয়েছে। virtualization-cloud/linux-namespaces-deep-dive দেখিয়েছে সাতটা namespace-এর exact clone() flag, PID 1-এর সিগন্যাল quirk, pivot_root()-এর নির্ভুল mechanics। virtualization-cloud/cgroups-v2-resource-control দেখিয়েছে resource limit ঠিক কোন ফাইলে লেখা হয়, kernel সেই সীমা কীভাবে বাস্তবে প্রয়োগ করে। virtualization-cloud/overlay-filesystems-image-layers দেখিয়েছে একটা container image আসলে কী — স্তরে স্তরে সাজানো read-only layer, উপরে একটা copy-on-write layer, OverlayFS দিয়ে একটা একক দৃশ্যমান filesystem-এ মিশিয়ে দেওয়া।

তিনটা লেসন, তিনটা ভিন্ন যন্ত্র — কিন্তু একটাও এখনো এই সবচেয়ে সহজ, সবচেয়ে ঘন ঘন জিজ্ঞাসিত প্রশ্নের সরাসরি উত্তর দেয়নি:

docker run লিখলে kernel-এ ঠিক কোন কোন syscall হয়?

এটাই এই পুরো module-এর চালিকা-প্রশ্ন (driving question), আর এই লেসনেই তার সরাসরি, ধাপে-ধাপে উত্তর আসবে। উত্তরটা এক লাইনে না — এটা পাঁচটা আলাদা সফটওয়্যার-স্তরের মধ্য দিয়ে যাওয়া একটা পথ (docker CLI → dockerd → containerd → containerd-shim → runc), যার শেষ ধাপেই কেবল আগের তিনটা লেসনের namespace/cgroup/mount syscall-গুলো বাস্তবে ঘটে। এই লেসনের কাজ সেই পুরো পথটা একসাথে জোড়া দেওয়া — কোন স্তর কী দায়িত্ব নেয়, কেন এতগুলো স্তর দরকার (একটা একক প্রোগ্রাম দিয়ে কেন কাজটা সারা যায় না), আর কীভাবে এই পুরো ব্যবস্থাটা একটা standard-এর ওপর দাঁড়িয়ে আছে যাকে বলা হয় OCI — Open Container Initiative।

মূল ধারণা

OCI — একটা শব্দ, দুইটা সম্পূর্ণ আলাদা স্পেসিফিকেশন

OCI (Open Container Initiative) নামটা প্রায়ই একটা একক জিনিস মনে হয়, কিন্তু এটা আসলে দুইটা সম্পূর্ণ আলাদা, স্বাধীনভাবে ভার্সন-করা স্পেসিফিকেশনের ছাতা-নাম:

Image SpecificationRuntime Specification
প্রশ্ন যার উত্তর দেয়container image-এ কী আছে, কীভাবে প্যাক করাএকটা container কীভাবে চালানো হয়
মূল artifactlayer tarball + manifest.json + image config JSONconfig.json (একটা “bundle”-এর অংশ)
প্রামাণ্য উৎসoverlay-filesystems-image-layers লেসনের ভিত্তি — এই লেসন সেটা ধরে নেয়এই লেসনের কেন্দ্রীয় বিষয়
CLI contractনেই — এটা শুধু একটা ফাইল-ফরম্যাটcreate / start / kill / delete / state — বাধ্যতামূলক
কে বাস্তবায়ন করেregistry, docker build, image-toolingrunc, crun, runsc (gVisor), kata-runtime

এই বিভাজনটা ইচ্ছাকৃত আর গুরুত্বপূর্ণ, কারণ দুইটা প্রশ্ন সত্যিই স্বাধীন। একটা image “কী” তা জানা (কোন layer, কোন entrypoint কমান্ড, কোন environment variable) কোনোভাবেই বলে দেয় না সেটা “কীভাবে” চালাতে হবে (কোন namespace, কোন cgroup সীমা) — বিপরীতটাও সত্যি। overlay-filesystems-image-layers লেসন যা বানিয়েছে — layer tarball-এর স্তর, OverlayFS-এ মিশে যাওয়া একটা rootfs — সেটাই এই লেসনের Runtime Spec-এর config.json-এ root.path হিসেবে point করা হয়। দুইটা spec একে অপরের input/output, একে অপরের প্রতিস্থাপন না।

কেন এই standardization ঐতিহাসিকভাবে গুরুত্বপূর্ণ ছিল

২০১৩-১৪ সালে Docker-এর জনপ্রিয়তা বিস্ফোরিত হওয়ার সময়, “container চালানো” মানে ব্যবহারিকভাবে “Docker-এর নিজস্ব অভ্যন্তরীণ runtime (libcontainer) ব্যবহার করা” — কোনো প্রতিদ্বন্দ্বী বাস্তবায়নের সাথে compatibility নিশ্চিত করার কোনো প্রামাণ্য উপায় ছিল না। ২০১৪-১৫ সালে CoreOS তাদের নিজস্ব বিকল্প container টুল (rkt) আর নিজস্ব image ফরম্যাট প্রস্তাব (appc) নিয়ে আসে, যুক্তি দিয়ে যে container ecosystem-এর একজন vendor-এর ওপর সম্পূর্ণ নির্ভরশীল হওয়া উচিত না। ২০১৫ সালে এই উত্তেজনার সমাধান হিসেবে Docker তাদের libcontainer কোড আর একটা নতুন, vendor-নিরপেক্ষ স্পেসিফিকেশন Linux Foundation-এর অধীনে একটা নতুন সংস্থা — Open Container Initiative-কে দান করে। সেই দান করা কোডটাই, পুনর্গঠিত হয়ে, হয়ে ওঠে runc।

standardization-এর ফলাফলটা এখনও দৃশ্যমান, আর এটাই সবচেয়ে বড় ব্যবহারিক প্রমাণ যে এটা কাজ করেছে: আজ docker, podman, containerd, Kubernetes-এর kubelet — এদের কেউই একে অপরের অভ্যন্তরীণ কোড না জেনেই একই কাজ করতে পারে, কারণ সবাই একই Runtime Spec CLI contract-এর সাপেক্ষে কথা বলে। একটা runtime বদলে ফেলা (runc থেকে gVisor-এর runsc-এ, বা Kata Containers-এ) উচ্চ-স্তরের tooling-এ কোনো কোড পরিবর্তন ছাড়াই সম্ভব — শুধু একটা কনফিগারেশন লাইন বদলালেই চলে, কারণ সবাই একই contract মেনে চলে। এই লেসনের শেষে (realworld সেকশনে) এই বিকল্পগুলো ফিরে আসবে।

runc — Runtime Spec-এর reference implementation, একটা one-shot লঞ্চার

runc হলো Runtime Spec-এর reference implementation — একটা ছোট, নিম্ন-স্তরের CLI টুল যার পুরো কাজ একটাই বাক্যে বলা যায়: একটা config.json পড়া, তাতে বর্ণিত namespace/cgroup/mount বাস্তবে তৈরি করা, আর সেই bundle-এর process.args-এ বলা entrypoint কমান্ড চালিয়ে দেওয়া। এটা Runtime Spec-এর CLI contract বাস্তবায়ন করে:

  • runc create <container-id> — bundle থেকে namespace, cgroup, mount সবকিছু তৈরি করে, কিন্তু entrypoint এখনো চালায় না — container একটা created অবস্থায় থামে
  • runc start <container-id> — সেই থেমে-থাকা container-এর entrypoint অবশেষে execve() করে চালায়
  • runc kill <container-id> — চলমান container-এ সিগন্যাল পাঠায়
  • runc delete <container-id> — বন্ধ হয়ে যাওয়া container-এর অবশিষ্ট state/cgroup পরিষ্কার করে

create আর start কেন আলাদা ধাপ — এটা একটা ইচ্ছাকৃত, সূক্ষ্ম ডিজাইন সিদ্ধান্ত যার ব্যবহারিক গুরুত্ব বিশাল। create-এর পরে network namespace ইতিমধ্যে তৈরি হয়ে গেছে, কিন্তু container-এর ভেতরের প্রোগ্রাম তখনো চলা শুরু করেনি। ঠিক এই ফাঁকা জানালায় একটা higher-level orchestration টুল (CNI — Container Network Interface — plugin) সেই network namespace-এ একটা veth interface ঢুকিয়ে, IP address বসিয়ে, route কনফিগার করে দিতে পারে — এন্ট্রিপয়েন্ট প্রোগ্রাম চালু হওয়ার আগেই। যদি create+start একটাই ধাপ হতো, container-এর ভেতরের প্রোগ্রাম (যদি এটা সাথে সাথেই একটা নেটওয়ার্ক কল করার চেষ্টা করত) network প্রস্তুত হওয়ার আগেই চলা শুরু করার race condition-এ পড়ত। এই ফাঁকটা এই লেসনের শেষে ইচ্ছাকৃতভাবে খোলা রাখা হচ্ছে — network namespace-এর বাইরের জগতের সাথে সংযোগের সম্পূর্ণ mechanics পরের লেসনের বিষয়।

ভেতরে কী ঘটছে

যদি runc-ই যথেষ্ট হতো, containerd কেন লাগে?

আগের সেকশনের শেষ Callout-টা একটা তাৎক্ষণিক প্রশ্ন তৈরি করে: যদি runc start-এর পর runc-এর নিজের কোনো অস্তিত্ব না থাকে, তাহলে docker logs <container> চালালে কে সেই লগ ধরে রাখে? docker exec চালালে কে চলমান container-এর namespace-এ ঢোকার ব্যবস্থা করে? container ক্র্যাশ করলে কে সেটা restart policy অনুযায়ী আবার চালু করে? এই প্রশ্নগুলোর প্রতিটার একটাই উত্তর — এই কাজগুলোর জন্য কাউকে persistently চলতে হবে, আর সেই persistently-চলা প্রসেসটাই containerd।

runc-এর দায়িত্ব একবারের, bounded — namespace/cgroup/mount তৈরি করে entrypoint চালিয়ে দেওয়া। এটা শেষ হওয়ার সাথে সাথেই তার কাজ সম্পূর্ণ। কিন্তু একটা container চালু থাকা মানে সেটার সাথে জড়িত অনেকগুলো চলমান, সময়ের-সাথে-বিস্তৃত দায়িত্ব তৈরি হয়:

  • Image pull ও unpack — একটা নতুন image দরকার হলে registry থেকে layer download করা, overlay-filesystems-image-layers লেসনের বর্ণিত snapshotter দিয়ে সেই layer-গুলো একটা নতুন rootfs-এ unpack ও mount করা — এটা runc-এর কাজ না, runc শুধু একটা ইতিমধ্যে-প্রস্তুত rootfs-এর path পায়।
  • Container lifecycle-এর সম্পূর্ণ ইতিহাস ট্র্যাক করা — কোন container চলছে, কোনটা বন্ধ, কোনটার exit code কী ছিল — এটা এমন একটা তথ্য যা কোনো না কোনো প্রসেসকে সময়ের সাথে ধরে রাখতে হয়, runc-এর মতো একবারের invocation দিয়ে সম্ভব না।
  • docker logs/docker exec-এর মতো ইন্টারঅ্যাকশন — চলমান একটা container-এর stdout/stderr stream করে দেখানো, বা তার namespace-এর ভেতরে একটা নতুন প্রসেস (exec) ঢুকিয়ে দেওয়া — দুইটাই এমন অপারেশন যেগুলো container চালু হওয়ার অনেক পরেও ঘটতে পারে, তাই কাউকে সেই মুহূর্তে চলমান থাকতেই হবে সেই request সার্ভ করার জন্য।
  • Restart policy বাস্তবায়ন — container ক্র্যাশ করলে --restart=always-এর মতো নীতি অনুযায়ী স্বয়ংক্রিয়ভাবে আবার চালানো, যা মানে exit event-এর জন্য অপেক্ষা করা এবং তার প্রতিক্রিয়ায় কাজ করা — একটা persistently-চলা প্রসেস ছাড়া অসম্ভব।

containerd হলো ঠিক এই দায়িত্বগুলো পালন করার জন্য বানানো একটা persistent daemon, runc-এর ওপরের একটা স্তর। এটা runc-কে replace করে না — বরং প্রতিটা container শুরু করার জন্য runc-কেই invoke করে, কিন্তু তার চারপাশের পুরো lifecycle management নিজে হাতে নেয়।

containerd-shim — কেন containerd নিজেই সরাসরি container-এর parent হয় না

এখানে একটা আরও সূক্ষ্ম প্রশ্ন থেকে যায়: যদি containerd-ই persistent daemon যে container ট্র্যাক করে, তাহলে containerd নিজেই কেন সরাসরি container প্রসেসের parent হয় না? উত্তরটা একটা বাস্তব অপারেশনাল সমস্যা থেকে আসে — containerd নিজেকেই মাঝে মাঝে upgrade/restart করতে হয় (একটা নতুন version deploy করা, একটা bug-fix আপডেট)। যদি containerd সরাসরি প্রতিটা container-এর parent process হতো, containerd restart করার মুহূর্তে তার সব child (সব চলমান container) orphan হয়ে যেত, আর অনেক প্ল্যাটফর্মে parent মারা গেলে child-ও সিগন্যাল পেয়ে মারা যায় বা কমপক্ষে তাদের exit-status ট্র্যাক করার সংযোগ হারিয়ে যায়।

এই সমস্যার সমাধান containerd-shim — প্রতিটা চলমান container-এর জন্য একটা আলাদা, ছোট, দীর্ঘজীবী প্রসেস, যেটাই আসলে container-এর প্রকৃত parent (runc-কে invoke করার পর runc নিজে execve()-এ রূপান্তরিত হয়ে যায়, কিন্তু shim সেই সম্পূর্ণ প্রসেস-গ্রুপের বাইরের anchor হিসেবে থেকে যায়)। shim-এর কাজ ইচ্ছাকৃতভাবে ন্যূনতম:

  1. container-এর stdout/stderr pipe খোলা রাখা, এমনকি containerd দূরে থাকলেও
  2. container exit করলে তার exit code capture করে রাখা
  3. containerd আবার সংযুক্ত হলে (restart-এর পর) সেই তথ্য ফিরিয়ে দেওয়া

এই ডিজাইনের ফলে containerd নিজে সম্পূর্ণ বন্ধ হয়ে আবার চালু হলেও, ইতিমধ্যে চলমান container-গুলো একবারও থামে না — প্রতিটার নিজস্ব shim তাদের বাঁচিয়ে রেখেছে, আর containerd শুধু সেই shim-গুলোর সাথে পুনরায় সংযুক্ত হয়ে (reconnect) আগের অবস্থা ফিরে পায়। এটাকে ইন্ডাস্ট্রিতে প্রায়ই “live restore” ক্ষমতা বলা হয়।

উদাহরণ

সম্পূর্ণ trace — module-এর চালিকা-প্রশ্নের পূর্ণ উত্তর

এখন সব টুকরো একসাথে জোড়া দেওয়া যাক। নিচের trace-টাই এই module-এর শুরু থেকে করা প্রশ্নের সরাসরি, ধাপে-ধাপে উত্তর — docker run লিখলে ঠিক কোন স্তর, কোন ক্রমে, কোন syscall চালায়।

docker run alpine echo hi — CLI থেকে kernel syscall পর্যন্ত
  1. docker (CLI, thin client)আর্গুমেন্ট পার্স করে dockerd-এর কাছে একটা REST API request পাঠায় — নিজে কোনো namespace/cgroup সম্পর্কে কিছুই জানে না
  2. dockerd (Docker daemon)high-level container object তৈরি করে, image resolve/pull করার প্রয়োজন যাচাই করে, containerd-কে একটা gRPC কল পাঠায়
  3. containerd — Create/Start task (gRPC)image আগে থেকে unpack করা না থাকলে snapshotter দিয়ে overlay layer প্রস্তুত করে (overlay-filesystems-image-layers লেসনের বিষয়), তারপর একটা নতুন containerd-shim spawn করে
  4. containerd-shim spawnএই container-এর জন্য একটা আলাদা, দীর্ঘজীবী anchor প্রসেস — stdio pipe তৈরি করে, runc invoke করার প্রস্তুতি নেয়
  5. shim → runc create <id>bundle path (rootfs + config.json) দিয়ে runc-কে ডাকে — namespace/cgroup/mount তৈরি হয়, entrypoint এখনো চলে না
  6. runc: clone(CLONE_NEWPID|NEWNS|NEWNET|NEWUTS|NEWIPC|NEWUSER)হেক্স ০x৭C০২০০০০ — linux-namespaces-deep-dive লেসনের ঠিক এই bitmask, একটাই syscall-এ ছয়টা namespace
  7. runc: uid_map/gid_map লেখা, mount() কল-গুলো, cgroup.procs/cpu.max/memory.max/pids.max লেখাcgroups-v2-resource-control লেসনের বর্ণিত ঠিক এই ফাইলগুলো — namespace visibility, cgroup ব্যবহারের সীমা
  8. runc: pivot_root() + umount2(MNT_DETACH)overlay-filesystems-image-layers লেসনের তৈরি করা merged rootfs-কে এই container-এর "/" বানানো, পুরনো root সম্পূর্ণ unmount
  9. shim → runc start <id>দ্বিতীয় ধাপ — এখন entrypoint সত্যিই চালু হবে
  10. runc: execve("/bin/echo", ["echo", "hi"], envp)runc নিজের প্রসেস-image প্রতিস্থাপিত হয় — container-এর entrypoint-ই এখন এই PID, runc আর অস্তিত্বে নেই

লক্ষ করুন here চিহ্নিত ধাপটা — containerd-এর task creation — কারণ এখান থেকেই OCI Runtime Spec-এর জগত শুরু হয়। এর আগের দুইটা ধাপ (docker, dockerd) সম্পূর্ণ Docker-নির্দিষ্ট, non-standard API। এর পরের প্রতিটা ধাপ (shim, runc, config.json) কোনো Docker-নির্দিষ্ট কিছু না — এটা একই OCI Runtime Spec contract যা Kubernetes-এর kubelet বা podman-ও ব্যবহার করে, dockerd-কে সম্পূর্ণ বাদ দিয়ে।

ছবিতে

        Image Spec জগত                          Runtime Spec জগত
        ────────────────                          ─────────────────

  registry-এ layer tarball                    bundle/
   + manifest.json                              ├── rootfs/       ← Image Spec-এর
   + image config JSON                          │                    layer unpack করে বানানো
        │                                        │                    (overlay-filesystems লেসন)
        │ pull + unpack (containerd)             │
        ▼                                        └── config.json   ← Runtime Spec-এর
  স্থানীয় unpacked rootfs  ───────────────────────►                    namespaces/cgroup/mounts/
  (overlay layer, RO+RW)                                              process.args বর্ণনা করে
                                                       │
                                                       │ runc create/start
                                                       ▼
                                              চলমান container প্রসেস
                                          (kernel-এ শুধু namespace+cgroup
                                           pointer-সহ একটা সাধারণ task_struct)
চিত্র ১OCI-এর দুইটা স্বাধীন স্পেসিফিকেশন কীভাবে মিলিত হয়ে একটা চলমান container তৈরি করে
আগে (containerd v1 চলছে):

  containerd (দীর্ঘ জীবনকাল)
    ├── containerd-shim (container A-এর জীবনকাল)
    │     └── container A entrypoint প্রসেস (runc execve()-এ পরিণত হয়েছিল)
    └── containerd-shim (container B-এর জীবনকাল)
          └── container B entrypoint প্রসেস

containerd upgrade/restart-এর মুহূর্তে:

  containerd (v1) ── SIGTERM/exit ──✕
    ├── containerd-shim (container A)  ← বেঁচে থাকে, orphan হয়ে যায় সাময়িকভাবে
    │     └── container A entrypoint   ← একবারও থামেনি, চলতেই থাকে
    └── containerd-shim (container B)  ← একইভাবে বেঁচে থাকে
          └── container B entrypoint   ← চলতেই থাকে

পরে (containerd v2 চালু হয়েছে):

  containerd (v2, নতুন প্রসেস)
    ├── (পুনরায় সংযুক্ত) containerd-shim (container A) ── container A অক্ষত
    └── (পুনরায় সংযুক্ত) containerd-shim (container B) ── container B অক্ষত
চিত্র ২তিনটা প্রসেস-স্তরের ভিন্ন ভিন্ন জীবনকাল — containerd restart হলেও container বাঁচে কেন

নিজে চালিয়ে দেখুন

EXPERIMENT

একটা honest পরীক্ষা — এই sandbox-এ সত্যিকার runc/containerd invocation সম্ভব কি?

এই sandbox: macOS (Darwin), root/namespace-permission ছাড়া, docker daemon চলমান না· ৩ মিনিট

সততার সাথে বলা দরকার: এই লেসনটা যেই পরিবেশে লেখা ও পরীক্ষা করা হচ্ছে সেটা macOS, আর যদিও এখানে docker CLI বাইনারি ইনস্টল করা আছে, প্রকৃত runc/containerd-এর কোনো চলমান instance নেই — Docker Desktop সাধারণত এই টুলগুলো একটা লুকানো Linux VM-এর ভেতরে চালায় (নিচের realworld সেকশনে এই ব্যাপারটাই ফিরে আসবে), আর সেই VM এই মুহূর্তে চালু নেই। নিচের কোডটা এই সীমাবদ্ধতা সরাসরি, verifiably দেখায়:

import shutil, subprocess, sys

print("platform:", sys.platform)
print("runc on PATH:", shutil.which("runc"))
print("docker CLI on PATH:", shutil.which("docker"))

result = subprocess.run(
    ["docker", "info", "--format", "{{.ServerVersion}}"],
    capture_output=True, text=True
)
print("docker info exit code:", result.returncode)
print(
    "docker info stderr (first line):",
    result.stderr.strip().splitlines()[0] if result.stderr.strip() else "(none)"
)

প্রকৃত, এই sandbox-এ চালিয়ে পাওয়া output:

platform: darwin
runc on PATH: None
docker CLI on PATH: /usr/local/bin/docker
docker info exit code: 1
docker info stderr (first line): Cannot connect to the Docker daemon at unix:///Users/360infotech/.docker/run/docker.sock. Is the docker daemon running?

দুইটা তথ্যই স্পষ্ট প্রমাণ। প্রথম, runc বাইনারিই এই মেশিনে নেই (None) — এটা একটা Linux-নির্দিষ্ট টুল, macOS-এর জন্য কোনো native build অর্থবহ না, কারণ এটা যে syscall (clone() namespace flag-সহ) চালায় সেগুলো macOS-এর XNU kernel-এ নেই। দ্বিতীয়, docker CLI বাইনারি থাকলেও (এটা শুধু একটা thin client), সেটা যে daemon-কে API কল পাঠাতে চায় (dockerd, যা আসলে ভেতরে containerd-কে ডাকে) — সেই daemon চলমান না, তাই সংযোগই ব্যর্থ হচ্ছে। এই ব্যর্থতা কোনো bug না — এটাই নিশ্চিত করে যে এই লেসনের বাকি অংশে বর্ণিত পুরো chain সত্যিই আলাদা আলাদা, চলমান প্রসেসের ওপর নির্ভরশীল, নিছক একটা লাইব্রেরি কল না।

এটা কী প্রমাণ করে

এই লেসনের LayerTrace-এ বর্ণিত পুরো chain (dockerd → containerd → shim → runc) বাস্তবে চালানোর জন্য একটা Linux kernel আর একটা চলমান daemon দরকার — নিছক docker CLI বাইনারি ইনস্টল থাকলেই যথেষ্ট না।

নিজে বানান

BUILD IT

MiniRuntime — একটা toy OCI config থেকে runc-এর exact syscall sequence বের করা

Python · ●●●●○
  1. একটা toy OCI Runtime Spec-শৈলীর dict বানান — namespaces list, uidMappings, mounts list, cgroup resources, আর process.args — ঠিক config.json-এর গঠন অনুসরণ করে
  2. namespace type থেকে exact CLONE_NEW* hex flag বের করার একটা lookup table বানান — linux-namespaces-deep-dive লেসনের টেবিল অনুসরণ করে
  3. namespaces list-এর সবগুলো flag bitwise-OR করে একটাই combined clone() flag word বের করুন
  4. একে একে uid_map লেখা, mount() কল-গুলো, cgroup resource ফাইল লেখা, sethostname(), pivot_root()+umount2(), আর সবশেষে execve()-এর জন্য একটা "action" print করুন — ঠিক সেই ক্রমে যা runc বাস্তবে অনুসরণ করে
  5. শেষ ধাপের পর স্পষ্টভাবে বলুন runc প্রসেসটা আর অস্তিত্বে নেই — execve() তার প্রসেস-image প্রতিস্থাপন করেছে

এই সিমুলেশনটা কোনো syscall সত্যিই চালায় না (এই sandbox macOS, clone()-এর namespace flag এখানে অর্থহীন) — বরং runc-এর C/Go কোড যে সিদ্ধান্ত-প্রক্রিয়া অনুসরণ করে (config.json পার্স করে, প্রতিটা ধাপ ক্রমান্বয়ে বাস্তবায়ন করে), সেই একই যুক্তি একটা ছোট, স্বাধীনভাবে-যাচাইযোগ্য Python প্রোগ্রামে পুনর্নির্মাণ করে — যাতে আগের example সেকশনের LayerTrace বিমূর্ত prose না থেকে, একটা যান্ত্রিক, চালানো প্রোগ্রামে পরিণত হয়।

"""
mini_runtime.py -- a toy OCI-runtime simulator.

Takes a config.json-shaped dict (the OCI Runtime Specification's format --
namespaces, cgroup resource limits, mounts, and the process entrypoint) and
prints the EXACT ordered sequence of syscalls/actions a real OCI runtime
(runc) performs to turn that JSON into a running container. Nothing here
actually calls clone()/pivot_root()/execve() -- this is macOS, those Linux
syscalls don't exist here -- it mechanically walks the same decision logic
runc's C/Go code walks, so the ORDER and the CONTENT of each step is real.
"""

CLONE_NEW = {
    "mount":   ("CLONE_NEWNS",      0x00020000),
    "cgroup":  ("CLONE_NEWCGROUP",  0x02000000),
    "uts":     ("CLONE_NEWUTS",     0x04000000),
    "ipc":     ("CLONE_NEWIPC",     0x08000000),
    "user":    ("CLONE_NEWUSER",    0x10000000),
    "pid":     ("CLONE_NEWPID",     0x20000000),
    "network": ("CLONE_NEWNET",     0x40000000),
}


def run(config):
    step = 1

    def emit(syscall, detail):
        nonlocal step
        print(f"[{step:02d}] {syscall}")
        print(f"      {detail}")
        step += 1

    # 1. Compute the combined clone() flag word from config.linux.namespaces
    ns_types = [n["type"] for n in config["linux"]["namespaces"]]
    flag_bits = 0
    flag_names = []
    for t in ns_types:
        name, bit = CLONE_NEW[t]
        flag_bits |= bit
        flag_names.append(name)
    emit(
        f"clone(flags={'|'.join(flag_names)}, child_stack=..., SIGCHLD)",
        f"combined flag word = 0x{flag_bits:08X} -- one syscall, "
        f"{len(ns_types)} namespaces created together",
    )

    # 2. uid/gid mapping, written by the PARENT into the child's /proc entry
    if "uidMappings" in config["linux"]:
        for m in config["linux"]["uidMappings"]:
            emit(
                f"write(/proc/{{child_pid}}/uid_map, \"{m['containerID']} {m['hostID']} {m['size']}\")",
                "parent writes the child's UID map -- the child cannot write its own",
            )

    # 3. mounts, in config.json order
    for m in config["mounts"]:
        emit(
            f"mount(\"{m['source']}\", \"{m['destination']}\", \"{m['type']}\", ...)",
            f"mount namespace: attach {m['type']} at {m['destination']}",
        )

    # 4. cgroup resource limits -- one write per controller, before execve
    cg_path = f"/sys/fs/cgroup/{config['hostname']}"
    res = config["linux"].get("resources", {})
    if "memory" in res:
        emit(
            f"write({cg_path}/memory.max, \"{res['memory']['limit']}\")",
            "cgroup v2: hard memory ceiling in bytes",
        )
    if "cpu" in res:
        quota, period = res["cpu"]["quota"], res["cpu"]["period"]
        emit(
            f"write({cg_path}/cpu.max, \"{quota} {period}\")",
            f"cgroup v2: {quota}/{period} = {quota/period:.2f} CPU cores max",
        )
    if "pids" in res:
        emit(
            f"write({cg_path}/pids.max, \"{res['pids']['limit']}\")",
            "cgroup v2: max simultaneous PIDs inside this container",
        )
    emit(
        f"write({cg_path}/cgroup.procs, \"{{child_pid}}\")",
        "join the child process to the cgroup -- limits now enforced",
    )

    # 5. hostname -- UTS namespace
    emit(
        f'sethostname("{config["hostname"]}", {len(config["hostname"])})',
        "UTS namespace: set container-local hostname",
    )

    # 6. root filesystem swap
    emit(
        'mount(NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL)',
        "detach mount propagation before pivoting",
    )
    emit(
        f'pivot_root("{config["root"]["path"]}", "{config["root"]["path"]}/.old_root")',
        "swap which mounted filesystem is root",
    )
    emit(
        'umount2("/.old_root", MNT_DETACH)',
        "detach the old root -- no fd-based escape possible afterward",
    )

    # 7. hand off to the entrypoint
    args = config["process"]["args"]
    emit(
        f"execve(\"{args[0]}\", {args!r}, envp)",
        "runc's own process image is replaced by the container's entrypoint -- "
        "runc itself no longer exists as a process from this point on",
    )

    print()
    print(f"total steps: {step - 1}")
    print("runc process lifetime: ends here (execve replaced it) -- no daemon left behind")


if __name__ == "__main__":
    config = {
        "ociVersion": "1.0.2",
        "hostname": "mini-container",
        "root": {"path": "rootfs", "readonly": False},
        "process": {
            "args": ["/bin/echo", "hello from inside the container"],
            "cwd": "/",
        },
        "mounts": [
            {"destination": "/proc", "type": "proc", "source": "proc"},
            {"destination": "/dev", "type": "tmpfs", "source": "tmpfs"},
        ],
        "linux": {
            "namespaces": [
                {"type": "pid"},
                {"type": "network"},
                {"type": "mount"},
                {"type": "uts"},
                {"type": "ipc"},
                {"type": "user"},
            ],
            "uidMappings": [{"containerID": 0, "hostID": 1000, "size": 1}],
            "resources": {
                "memory": {"limit": 268435456},
                "cpu": {"quota": 50000, "period": 100000},
                "pids": {"limit": 128},
            },
        },
    }

    run(config)

এই sandbox-এ সত্যিই চালিয়ে পাওয়া output (macOS-এ Python 3.9.6, সম্পূর্ণ portable কোড — কোনো Linux syscall লাগেনি):

[01] clone(flags=CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWUSER, child_stack=..., SIGCHLD)
      combined flag word = 0x7C020000 -- one syscall, 6 namespaces created together
[02] write(/proc/{child_pid}/uid_map, "0 1000 1")
      parent writes the child's UID map -- the child cannot write its own
[03] mount("proc", "/proc", "proc", ...)
      mount namespace: attach proc at /proc
[04] mount("tmpfs", "/dev", "tmpfs", ...)
      mount namespace: attach tmpfs at /dev
[05] write(/sys/fs/cgroup/mini-container/memory.max, "268435456")
      cgroup v2: hard memory ceiling in bytes
[06] write(/sys/fs/cgroup/mini-container/cpu.max, "50000 100000")
      cgroup v2: 50000/100000 = 0.50 CPU cores max
[07] write(/sys/fs/cgroup/mini-container/pids.max, "128")
      cgroup v2: max simultaneous PIDs inside this container
[08] write(/sys/fs/cgroup/mini-container/cgroup.procs, "{child_pid}")
      join the child process to the cgroup -- limits now enforced
[09] sethostname("mini-container", 14)
      UTS namespace: set container-local hostname
[10] mount(NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL)
      detach mount propagation before pivoting
[11] pivot_root("rootfs", "rootfs/.old_root")
      swap which mounted filesystem is root
[12] umount2("/.old_root", MNT_DETACH)
      detach the old root -- no fd-based escape possible afterward
[13] execve("/bin/echo", ['/bin/echo', 'hello from inside the container'], envp)
      runc's own process image is replaced by the container's entrypoint -- runc itself no longer exists as a process from this point on

total steps: 13
runc process lifetime: ends here (execve replaced it) -- no daemon left behind

লক্ষ্য করুন ধাপ [01]-এর 0x7C020000 — এটা হুবহু linux-namespaces-deep-dive লেসনের সেই একই combined flag value (CLONE_NEWPID|NEWNS|NEWNET|NEWUTS|NEWIPC|NEWUSER-এর bitwise OR), এখানে স্বাধীনভাবে, একটা lookup table থেকে namespace type string-এর ভিত্তিতে recompute করা হয়েছে, হার্ডকোড করা না — দুইটা লেসনের সংখ্যা মিলে যাওয়াটাই প্রমাণ করে দুইটাই একই underlying kernel সত্যকে বর্ণনা করছে। ধাপ [13]-এর পরের মন্তব্যটাই এই লেসনের কেন্দ্রীয় দাবির যান্ত্রিক প্রমাণ — প্রোগ্রামটা নিজেই বলছে runc-এর জীবনকাল এখানেই শেষ, কোনো অবশিষ্ট daemon প্রসেস নেই।

বাস্তব সিস্টেমে

বাস্তব সিস্টেমে — এই স্তরগুলো ঠিক কোথায় চলছে

  • এই লেসন যেখানে লেখা হচ্ছে, ঠিক সেখানেই — এই sandbox-এর docker info output-এ Context: desktop-linux দেখা যায়, যা প্রমাণ করে Docker Desktop for Mac আসলে macOS-এ সরাসরি dockerd/containerd/runc চালায় না (এগুলো Linux-নির্দিষ্ট), বরং পটভূমিতে একটা লুকানো, লাইটওয়েট Linux VM চালায় (এই module-এর containers-vs-vms লেসনের সরাসরি প্রয়োগ — এখানে container-কে VM-এর ভেতরে পাওয়া যাচ্ছে), আর সেই VM-এর ভেতরেই আসল dockerd+containerd+runc চলে। macOS-এর docker CLI শুধু একটা thin client যা সেই VM-এর ভেতরের dockerd-কে API কল পাঠায়।
  • Kubernetes-এর CRI (Container Runtime Interface) — kubelet সরাসরি containerd-কে (বা CRI-O-কে) backend হিসেবে কল করে, dockerd-কে সম্পূর্ণ বাদ দিয়ে — প্রমাণ করে dockerd কোনো বাধ্যতামূলক স্তর না, এটা শুধু একটা user-facing convenience layer।
  • crun — runc-এর একটা বিকল্প বাস্তবায়ন, C-তে লেখা (Go-এর বদলে), দ্রুততর startup আর কম memory overhead দাবি করে — কিন্তু একই OCI Runtime Spec contract মেনে চলে, তাই containerd-এর কাছে সম্পূর্ণ অদল-বদলযোগ্য।
  • gVisor-এর runsc — একই CLI contract বাস্তবায়ন করে, কিন্তু ভেতরে clone()-এর namespace flag সরাসরি ব্যবহার না করে, একটা userspace-এ বাস্তবায়িত kernel (Sentry) দিয়ে container-এর syscall intercept ও পুনরায় বাস্তবায়ন করে — containers-vs-vms লেসনের আলোচনার সরাসরি সম্প্রসারণ, যেখানে namespace-ভিত্তিক isolation যথেষ্ট মনে না হলে এই বিকল্প ব্যবহৃত হয়।
  • Kata Containers-এর kata-runtimeও একই contract বাস্তবায়ন করে, কিন্তু ভেতরে namespace/cgroup তৈরি না করে একটা সম্পূর্ণ lightweight VM (microVM) চালু করে — containerd-এর দৃষ্টিতে এটাও শুধু আরেকটা OCI-compliant runtime, কোনো বিশেষ কোড ছাড়াই plug হয়ে যায়।

যে ভুলগুলো সবাই করে

“Docker নিজেই container runtime — docker run মানে Docker সরাসরি namespace/cgroup তৈরি করছে।”

ভুল। docker CLI একটা thin client মাত্র — এটা কোনো namespace/cgroup syscall সরাসরি চালায় না। প্রকৃত namespace/cgroup তৈরি হয় runc-এ, যেটা dockerd নিজে সরাসরি ডাকে না বরং containerd-এর মধ্য দিয়ে (gRPC কল, তারপর containerd-shim, তারপর সেই shim runc-কে ডাকে)। “Docker container চালাচ্ছে” — এই বাক্যটা ব্যবহারিকভাবে ঠিক, কিন্তু টেকনিক্যালি এটা পাঁচটা আলাদা স্তরের একটা সংক্ষিপ্তকরণ, যার মধ্যে docker/dockerd নিজে namespace-এর কোনো সিদ্ধান্তই নেয় না — সেটা সম্পূর্ণভাবে runc-এর কাজ।

“runc একটা container চলাকালীন ব্যাকগ্রাউন্ডে চলতে থাকে, তাকে supervise করে।”

উল্টো। runc start-এ execve() সফল হওয়ার সাথে সাথেই runc প্রসেসটা নিজেই container-এর entrypoint-এ রূপান্তরিত হয়ে যায় — এরপর runc নামের আলাদা কোনো প্রসেস অস্তিত্বেই থাকে না। container চলাকালীন যেকোনো “supervision” (log ধরে রাখা, exit status ট্র্যাক করা) runc-এর কাজ না — এটা containerd-shim-এর কাজ, যেটা runc-এর বাইরের একটা সম্পূর্ণ আলাদা, দীর্ঘজীবী প্রসেস।

“containerd আর runc আসলে একই কাজ করে, শুধু containerd একটা 'বড়' বা 'উন্নত' সংস্করণের runc।”

ভুল — এই দুইটার দায়িত্ব orthogonal, একই কাজের দুইটা মাত্রা না। runc-এর দায়িত্ব bounded, one-shot: একটা config.json পড়ে namespace/cgroup/mount তৈরি করে entrypoint চালিয়ে exit হয়ে যাওয়া। containerd-এর দায়িত্ব persistent, stateful: হাজারো container-এর lifecycle সময়ের সাথে ট্র্যাক করা, image pull/unpack করা, log/exec serve করা। containerd নিজে কখনো namespace/cgroup syscall সরাসরি চালায় না — সেটার জন্য এটা প্রতিবারই runc-কে (বা যেকোনো OCI-compliant runtime-কে) ডাকে।

“OCI Image Specification আর OCI Runtime Specification একই জিনিস, শুধু ভিন্ন নাম।”

ভুল, আর এই লেসনের প্রথম সেকশনের কেন্দ্রীয় বিভাজন। Image Spec সংজ্ঞায়িত করে একটা container image কী — layer tarball, manifest, image config JSON। Runtime Spec সংজ্ঞায়িত করে একটা container কীভাবে চালানো হয় — config.json-এর namespace/cgroup/mount বর্ণনা, আর create/start/kill/delete CLI contract। দুইটা সম্পূর্ণ স্বাধীন, আলাদাভাবে ভার্সন-করা স্পেসিফিকেশন — একটা image বানানোর টুল Runtime Spec সম্পর্কে কিছুই জানার দরকার নেই, আর runc-এর মতো একটা runtime Image Spec সম্পর্কে কিছুই জানে না (এটা শুধু একটা আগে-থেকে-unpack-করা rootfs path পায়)।

বুঝেছেন কি না দেখুন

1

OCI স্পেসিফিকেশন ঠিক দুইটা আলাদা স্পেসিফিকেশনে ভাগ করা। নাম বলুন, আর প্রতিটা ঠিক কী define করে?

স্মরণ

Image Specification — একটা container image-এ কী আছে তা define করে: layer tarball, manifest.json, আর image config JSON। Runtime Specification — একটা container কীভাবে চালানো হয় তা define করে: config.json-এর namespace/cgroup/mount বর্ণনা, আর create/start/kill/delete-এর মতো একটা বাধ্যতামূলক CLI contract। প্রথমটা “কী”, দ্বিতীয়টা “কীভাবে চালানো” — দুইটা সম্পূর্ণ স্বাধীন।

2

কেন runc-কে daemon বানানো হয়নি, অথচ containerd-কে বানানো হয়েছে একটা persistent daemon হিসেবে? দুইটার দায়িত্বের কোন পার্থক্যটা এই ডিজাইন সিদ্ধান্ত নির্ধারণ করে?

যুক্তি

runc-এর দায়িত্ব — namespace/cgroup/mount তৈরি করে entrypoint চালিয়ে দেওয়া — একটা bounded, একবারের অপারেশন। এটা শেষ হয়ে গেলে বাকি সবকিছু (namespace-এর visibility, cgroup-এর resource সীমা) kernel নিজেই বাস্তবায়ন করে, runc-এর আর কিছু “supervise” করার থাকে না। বিপরীতে containerd-এর দায়িত্ব — শত শত container-এর lifecycle সময়ের সাথে ট্র্যাক করা, docker logs/docker exec-এর মতো যেকোনো মুহূর্তে আসা request সার্ভ করা, restart policy বাস্তবায়ন করা — সবই এমন কাজ যা “যেকোনো সময়ে ঘটতে পারে”, তাই কাউকে সবসময় চলমান থাকতেই হয় সেই request handle করার জন্য। একটা bounded অপারেশনকে daemon বানানো অপচয়, একটা inherently persistent দায়িত্বকে one-shot টুল বানানো অসম্ভব।

3

একটা config.json-এর linux.namespaces list-এ শুধু চারটা এন্ট্রি আছে: pid, network, mount, user (uts আর ipc বাদ)। combined clone() flag-এর hex value কত হবে?

প্রয়োগ

CLONE_NEWPID (0x20000000) | CLONE_NEWNET (0x40000000) | CLONE_NEWNS (0x00020000) | CLONE_NEWUSER (0x10000000) — bitwise OR করলে 0x70020000। প্রতিটা bit আলাদা অবস্থানে থাকায় (linux-namespaces-deep-dive লেসনের bitmask ব্যাখ্যা), শুধু চারটা flag-এর OR নিলেই হয়, uts/ipc বাদ দেওয়া মানে সেই দুইটা bit (0x04000000, 0x08000000) মূল ছয়-namespace value (0x7C020000) থেকে বাদ পড়ে।

4

একজন সহকর্মী বলছেন containerd আপগ্রেড করার আগে সব চলমান container বন্ধ করে দিতে হবে, নাহলে containerd restart করলে container-ও মারা যাবে। এই ধারণা ঠিক না ভুল, আর কেন?

যুক্তি

ভুল, containerd-shim আর্কিটেকচারের কারণে। প্রতিটা চলমান container-এর নিজস্ব একটা shim প্রসেস থাকে যেটাই container-এর প্রকৃত parent, containerd নিজে না — তাই containerd restart হলে shim-গুলো orphan হয়ে যায় সাময়িকভাবে, কিন্তু তারা চলতেই থাকে (তাদের জীবনকাল container-এর জীবনকালের সাথে বাঁধা, containerd-এর জীবনকালের সাথে না)। নতুন containerd চালু হয়ে সেই shim-গুলোর সাথে পুনরায় সংযুক্ত হয় (“live restore”), আর কোনো চলমান container একবারও থামে না।

5

runc create আর runc start-এর মাঝখানে ঠিক কোন সুযোগটা তৈরি হয়, আর কোন real-world tool-শ্রেণী এই সুযোগ ব্যবহার করে?

প্রয়োগ

create-এর পরে network namespace ইতিমধ্যে তৈরি হয়ে গেছে, কিন্তু container-এর entrypoint প্রোগ্রাম এখনো চলা শুরু করেনি (start না হওয়া পর্যন্ত)। এই ফাঁকা জানালায় একটা CNI (Container Network Interface) plugin সেই network namespace-এ veth interface ঢুকিয়ে, IP address ও route কনফিগার করে দিতে পারে — এন্ট্রিপয়েন্ট চালু হওয়ার আগেই নেটওয়ার্ক প্রস্তুত থাকে, একটা race condition এড়িয়ে। যদি create+start একটাই ধাপ হতো, এই সুযোগটা থাকত না।

6

আপনাকে একটা নতুন, পরীক্ষামূলক container runtime লিখতে বলা হয়েছে যেটা gVisor-এর মতো একটা userspace kernel দিয়ে syscall intercept করে অতিরিক্ত নিরাপত্তা দেয়। শর্ত হলো: বিদ্যমান dockerd/containerd/kubelet স্ট্যাকে কোনো কোড পরিবর্তন ছাড়াই এটা প্লাগ-ইন হতে হবে। কীভাবে ডিজাইন করবেন, আর কেন এটাই OCI standardization-এর পুরো উদ্দেশ্য প্রমাণ করে?

ডিজাইন

এই নতুন runtime-কে একটা স্বতন্ত্র বাইনারি হিসেবে বানাতে হবে যেটা ঠিক runc-এর মতো OCI Runtime Specification-এর CLI contract বাস্তবায়ন করে — একই config.json ইনপুট নেয়, একই create/start/kill/delete/state কমান্ড সমর্থন করে, একই exit-code ও error-reporting convention মেনে চলে। ভেতরে namespace/cgroup syscall সরাসরি চালানোর বদলে এই বাইনারি container-এর syscall নিজের userspace kernel-এ intercept ও পুনরায় বাস্তবায়ন করবে — কিন্তু containerd-এর দৃষ্টিতে এটা পার্থক্যহীন, কারণ containerd কখনো runtime-এর ভেতরের বাস্তবায়ন জানে না, শুধু CLI contract-এর সাথে কথা বলে। containerd-এর কনফিগারেশনে শুধু runtime handler-এর নাম এই নতুন বাইনারির path-এ বদলে দিলেই (ঠিক যেভাবে gVisor-এর runsc বা Kata-এর kata-runtime আজ কাজ করে) পুরো স্ট্যাক — dockerd, containerd, kubelet — কোনো কোড পরিবর্তন ছাড়াই এই নতুন runtime ব্যবহার শুরু করে। এটাই সরাসরি প্রমাণ করে OCI standardization-এর আসল ফসল কী — বাস্তবায়ন বদলানো যায়, contract অপরিবর্তিত থাকলে।

এরপর কী

আমরা এখন module-এর চালিকা-প্রশ্নের সম্পূর্ণ উত্তর পেয়ে গেছি — docker run লিখলে docker CLI থেকে dockerd, containerd, containerd-shim, runc হয়ে অবশেষে kernel-এর clone()/pivot_root()/execve() পর্যন্ত পুরো পথ, প্রতিটা ধাপের দায়িত্ব সহ। namespace-এর সাতটা type, cgroup-এর resource সীমা, overlay filesystem-এর image layer, আর এখন OCI-এর runtime/lifecycle স্তর — এই চারটা লেসন মিলিয়ে একটা “container” শব্দের পেছনের প্রতিটা kernel primitive আর প্রতিটা userspace daemon এখন পরিচিত।

কিন্তু trace-টা একটা জায়গায় ইচ্ছাকৃতভাবে অসম্পূর্ণ রাখা হয়েছে। ধাপ ছয়ে আমরা দেখেছি clone(CLONE_NEWNET|...) একটা নতুন, সম্পূর্ণ বিচ্ছিন্ন network namespace তৈরি করে — নিজস্ব interface তালিকা, নিজস্ব routing table, নিজস্ব port space, linux-namespaces-deep-dive লেসনের বর্ণনা অনুযায়ী। কিন্তু একটা সম্পূর্ণ বিচ্ছিন্ন network namespace, যার ভেতরে শুধু একটা নিষ্ক্রিয় loopback আছে, কারো সাথেই কথা বলতে পারে না — না host-এর সাথে, না অন্য কোনো container-এর সাথে, না ইন্টারনেটের সাথে। একটা container যদি সত্যিই এমন সম্পূর্ণ বিচ্ছিন্ন থাকত, curl, docker -p, বা kubectl port-forward-এর মতো প্রতিদিনকার প্রতিটা ব্যবহার অসম্ভব হয়ে যেত।

পরের লেসন, virtualization-cloud/container-networking, ঠিক এই ফাঁকটা বন্ধ করবে — একটা বিচ্ছিন্ন network namespace কীভাবে veth pair, bridge, আর iptables/nftables-এর NAT rule দিয়ে বাইরের জগতের সাথে সংযুক্ত হয়, আর কীভাবে এই মেকানিজমই docker run -p-এর মতো কমান্ডকে সম্ভব করে তোলে।

আরও পড়ুন

  • Open Container Initiative Runtime Specification — Open Container Initiative · config.json-এর প্রামাণ্য schema, আর create/start/kill/delete CLI contract-এর নির্ভুল সংজ্ঞা
  • Open Container Initiative Image Specification — Open Container Initiative · container image-এর layer tarball + manifest + config ফরম্যাটের প্রামাণ্য উৎস — prerequisite overlay-filesystems-image-layers লেসনের ভিত্তি
  • runc — Open Container Initiative · OCI Runtime Specification-এর reference implementation — Docker-এর দান করা মূল libcontainer কোড থেকে ২০১৫ সালে জন্ম
  • containerd — an industry-standard container runtime — containerd project / CNCF · containerd-এর architecture, shim design, ও lifecycle-management দায়িত্বের প্রামাণ্য ডকুমেন্টেশন