সরাসরি পর্বে যান
পর্ব ৩৯ · ৮১-এর মধ্যে · মডিউল ৭

Container চালানো — lifecycle, port, volume, network আর resource limit

Running Containers — Lifecycle, Ports, Volumes, Networks and Resource Limits

Taught in Bangla, this lesson follows the container lifecycle and reads it in docker inspect, interprets exit codes such as 137 and 143, uses logs, exec, top and stats, publishes ports, chooses volumes, bind mounts or tmpfs, connects containers by name on user-defined networks, measures memory and CPU limits, picks restart policies, drops privileges, and diagnoses crashes in a fixed order, with real Docker output and simulators.

এই পর্বে যা শিখবেন

  • Container-এর lifecycle আর docker inspect-এ তার প্রমাণ পড়া
  • Exit code 0, 1, 125, 126, 127, 137, 139, 143 দেখে কারণ আন্দাজ করা
  • Port publish, volume, bind mount আর tmpfs ঠিকভাবে বেছে নেওয়া
  • User-defined network-এ একটি container থেকে আরেকটিকে নাম ধরে ডাকা
  • Memory আর CPU সীমার আসল আচরণ মাপা, restart policy বেছে নেওয়া
  • Crash করা container-এর কারণ একটি নির্দিষ্ট ক্রমে খুঁজে বের করা

১ · একটি container-এর জীবন আর থামার ভাষা

পর্ব ৩৬ থেকে ৩৮-এ আপনি image বানাতে শিখেছেন। Dockerfile হলো রান্নাঘরের রেসিপি খাতা, image হলো সেই রেসিপিতে তৈরি প্যাকেট, আর container হলো প্যাকেট খুলে বসানো একটি আসর। এই পর্বের বিষয় সেই আসরটি চালানো: সে কখন শুরু হয়, কেন থামে, কোথায় ডেটা রাখে, কতটা জায়গা পায়।

একটি বিয়েবাড়ির ক্যাটারিং ভাবুন। হাঁড়ি-চুলা সাজানো হলো, কিন্তু আগুন জ্বলেনি: এটি created. রান্না চলছে: running. হঠাৎ load-shedding-এ সবাই হাত গুটিয়ে দাঁড়িয়ে আছেন, কিন্তু রান্না যেমন ছিল তেমনই আছে: paused. রান্না শেষ, হাঁড়ি এখনো চুলায়, চাইলে ভেতরে কী আছে দেখা যায়: exited. হাঁড়ি ধুয়ে তুলে রাখা হলো, কিছুই আর নেই: removed.

এই পর্বের transcript সম্পর্কে: নিচের সব Docker আউটপুট Docker Engine 29 (BuildKit)-এ একটি আসল run থেকে নেওয়া। যে লাইনে Docker শুধু নাম বা id ফেরত দেয়, সেগুলো অনেক জায়গায় বাদ দেওয়া হয়েছে; অন্য বাদ দেওয়া অংশে … আছে। Id, digest, size আর সময় আপনার মেশিনে আলাদা হবে। Docker ব্রাউজারে চলে না, তাই এগুলো শুধু পড়ার উদাহরণ। আপনি নিজে চালাতে চাইলে Docker থাকা একটি মেশিনে চালান; নিজের বানানো container শেষে মুছে ফেলুন।

নিজে চালাতে চাইলে: নিজের মেশিনে Docker-এ কোনো বিল আসে না। Cloud VM-এ চালালে VM আর disk-এর খরচ হতে পারে, তাই কাজ শেষে VM বন্ধ করুন। পরিষ্কার করতে সবসময় নাম ধরে মুছুন: docker rm -f abcl-dr-web, docker volume rm abcl-dr-data, docker network rm abcl-dr-net. একসাথে সব মোছার কমান্ড অন্য কারও কাজও মুছে দিতে পারে।

রোজ আপনি সাধারণত docker run লেখেন, যা একসাথে তিনটি কাজ করে: container তৈরি, চালু, আর তার আউটপুটের সাথে terminal জুড়ে দেওয়া। -d দিলে terminal জোড়ে না, container পেছনে চলে। --name মনে রাখার মতো নাম দেয়; না দিলে Docker একটি এলোমেলো নাম বসায়। --rm থামার সাথে সাথে container মুছে দেয়, যা একবারের কাজে ভালো, কিন্তু crash-এর প্রমাণও মুছে দেয়।

নিচের ধাপগুলোতে একটি nginx container-এর পুরো জীবন দেখুন। প্রতিটি ধাপের পরে docker inspect দিয়ে তার lifecycle-এর অবস্থা পড়া হয়েছে।

  1. createddocker createসাজানো, আগুন নেই
  2. runningdocker startprocess চলছে
  3. pauseddocker pauseজমে আছে
  4. exiteddocker stopথেমেছে, মোছেনি
  5. removeddocker rmআর কিছু নেই
docker inspect-এ যা দেখা গেল
Status
Pid > 0
ExitCode
Writable layer
Terminalid ছোট করে দেখানো
$ docker create --name abcl-dr-web --hostname demo nginx:1.27-alpine
9f7b87dd0f4a…
Terminalএবার process আছে
$ docker start abcl-dr-web
abcl-dr-web
TerminalStatus-এ Paused
$ docker pause abcl-dr-web
$ docker ps --filter name=abcl-dr-web --format 'table {{.Names}}\t{{.Status}}'
NAMES         STATUS
abcl-dr-web   Up Less than a second (Paused)
Terminalথেমেছে, তালিকায় এখনো আছে
$ docker unpause abcl-dr-web
$ docker stop abcl-dr-web
$ docker ps -a --filter name=abcl-dr-web --format 'table {{.Names}}\t{{.Status}}'
NAMES         STATUS
abcl-dr-web   Exited (0) Less than a second ago
Terminalকোনো চিহ্ন নেই
$ docker rm abcl-dr-web
$ docker inspect abcl-dr-web
[]
error: no such object: abcl-dr-web
  1. Created. docker create filesystem সাজিয়েছে আর image-এর ওপরে একটি writable layer বসিয়েছে। কিন্তু কোনো process নেই, তাই Pid 0.
  2. Running. docker start image-এর command চালাল। Status এখন running, Pid শূন্যের বেশি। docker run আসলে create আর start একসাথে।
  3. Paused. docker pause cgroup freezer দিয়ে সব process জমিয়ে দেয়। কোনো signal যায় না, memory-তে সব আগের মতোই থাকে। Pid তাই এখনো আছে।
  4. Exited. unpause-এর পরে docker stop চলল। nginx ভদ্রভাবে থামল, তাই ExitCode 0. Container মোছেনি: log, writable layer আর সব সেটিং এখনো আছে।
  5. Removed. docker rm-এর পরে writable layer আর log মুছে গেছে। Inspect বলছে no such object. Image আগের মতোই আছে, তাই আবার নতুন container বানানো যায়।

থামার ভাষা — exit code, SIGTERM আর 137

docker stop সরাসরি মেরে ফেলে না। সে প্রথমে PID 1-কে একটি signal পাঠায়, সাধারণত SIGTERM. তারপর একটি grace period অপেক্ষা করে। Process তখনও না থামলে পাঠায় SIGKILL, যা আটকানো যায় না। Docker-এর ডকুমেন্টেশন অনুযায়ী Linux container-এ default অপেক্ষা ১০ সেকেন্ড (Windows container-এ ৩০ সেকেন্ড)। Container তৈরির সময় --stop-timeout দিলে সেটিই তার নিজের default হয়, আর docker stop -t দিয়ে প্রতিবার আলাদা সময় দেওয়া যায়।

Exit code-এর নিয়মটি পর্ব ১১ (Service চালানো)-এর মতোই: signal-এ মারা গেলে কোড হয় 128 + signal নম্বর. তাই SIGKILL (9) থেকে 137, SIGTERM (15) থেকে 143, আর SIGSEGV (11) থেকে 139. নিচের প্রতিটি সংখ্যা আসল run-এ এসেছে।

Exit codeঅর্থএই run-এ যেভাবে পেলাম
0ভালোভাবে শেষsh -c 'exit 0'; nginx-এ docker stop
1, 3App নিজে ব্যর্থতা জানিয়েছেPython traceback থেকে 1; exit 3 থেকে 3
125Docker নিজেই চালাতে পারেনিdocker run --nosuchflag
126Command পাওয়া গেছে, চালানো যায়নিdocker run alpine:3.20 /etc
127Command পাওয়া যায়নিdocker run alpine:3.20 nosuchcmd
137SIGKILL (128 + 9)docker kill, stop-এর সময় পার, OOM
139SIGSEGV (128 + 11), ভুল memory-তে হাতctypes.string_at(0)
143SIGTERM (128 + 15)--init-সহ Python-এ docker stop

এবার একটি অবাক করা ফল। একই Python প্রোগ্রাম, একই docker stop. পার্থক্য শুধু --init:

$ docker run -d --name abcl-dr-py python:3.12-alpine \
    python -c 'import time; time.sleep(600)'
8ecbd42c5176…
$ time docker stop -t 3 abcl-dr-py
abcl-dr-py
real    0m3.286s
$ docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' abcl-dr-py
exited exit=137

$ docker run -d --init --name abcl-dr-i python:3.12-alpine \
    python -c 'import time; time.sleep(600)'
3afe0f8b50ad…
$ time docker stop abcl-dr-i
abcl-dr-i
real    0m0.274s
$ docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' abcl-dr-i
exited exit=143

প্রথম ক্ষেত্রে Python নিজেই PID 1. Kernel PID 1-কে বিশেষ সুরক্ষা দেয়: সে নিজে handler না বসালে SIGTERM-এর default কাজ (মরে যাওয়া) তার ওপর খাটে না। -t 3 দেওয়ায় পুরো ৩ সেকেন্ড অপেক্ষার পরে SIGKILL এলো (default হলে ১০ সেকেন্ড লাগত), ফল 137. দ্বিতীয় ক্ষেত্রে --init একটি ছোট init-কে PID 1 বানায়। সে signal-টি Python-এর কাছে পৌঁছে দেয়, Python সাথে সাথে মরে, আর exit code হয় 143. সময় ৩ সেকেন্ড থেকে নেমে ০.২৭ সেকেন্ড।

সব app একই signal পায় না। nginx image STOPSIGNAL SIGQUIT ঠিক করে রেখেছে (এই image-এ inspect করে দেখা), তাই docker stop-এ তার exit code ছিল 0. ধীর গতির shutdown মানেই সাধারণত আপনার app signal পাচ্ছে না। আর deploy-এর সময় কাজ অর্ধেক ফেলে মরা মানে ব্যবহারকারীর অনুরোধ হারানো। Exec form CMD-এর কারণ পর্ব ৩৮-এ আছে।

২ · চলন্ত container-এর ভেতরে দেখা — logs, exec, top, stats

চারটি কমান্ড চার রকম প্রশ্নের উত্তর দেয়। ভুল কমান্ড বেছে নিলে উত্তরও ভুল জায়গায় খুঁজবেন।

প্রশ্নকমান্ডমনে রাখুন
App কী বলছে?docker logs --tail 50 -fExited container-এও চলে
ভেতরে কী আছে?docker exec -it … shনতুন process চালায়, শুধু running-এ চলে
কোন process চলছে?docker topHost-এর দিক থেকে দেখা PID দেখায়
কত খরচ হচ্ছে?docker stats --no-streamএকবার মেপে বের হয়ে আসে
$ docker run -d --name abcl-dr-web --hostname demo -m 64m \
    -p 127.0.0.1:18391:80 nginx:1.27-alpine
dcdee61895cf…

$ docker logs --tail 2 abcl-dr-web
… [error] 31#31: *2 open() "/usr/share/nginx/html/nope" failed (2: No such file or…
… "GET /nope HTTP/1.1" 404 153 "-" "curl/8.18.0" "-"

$ docker exec abcl-dr-web sh -c 'ps -o pid,user,args | head -3'
PID   USER     COMMAND
    1 root     nginx: master process nginx -g daemon off;
   30 nginx    nginx: worker process

$ docker top abcl-dr-web -o pid,args
PID                 COMMAND
31645               nginx: master process nginx -g daemon off;
31691               nginx: worker process
…

$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' abcl-dr-web
NAME          CPU %     MEM USAGE / LIMIT   MEM %
abcl-dr-web   0.00%     16.57MiB / 64MiB    25.89%

একই master process ভেতর থেকে PID 1, কিন্তু host থেকে 31645. দুটিই সত্যি: PID namespace শুধু ভেতরের দৃশ্য বদলায়। Container আসলে host-এরই process (Operating Systems-এর Containers পর্ব). Log-এর দুটি লাইনও আলাদা পথে এসেছে: access log যায় stdout-এ, আর error log যায় stderr-এ। Image দুটিকেই container-এর মূল আউটপুটে পাঠায়, তাই docker logs দুটিই দেখায়। -m 64m দেওয়ায় LIMIT ঘরে 64MiB এসেছে, আর MEM % সেই সীমার সাপেক্ষে হিসাব হচ্ছে।

Log-এর দুটি ফাঁদ। এক, docker rm-এ log-ও মুছে যায়, তাই production-এ log বাইরে পাঠাতে হয় (পর্ব ৬৪). দুই, অনেক ইনস্টলেশনে default logging driver json-file, যার ফাইল নিজে থেকে ছোট হয় না। --log-opt max-size=10m --log-opt max-file=3 না দিলে একটি বাচাল container host-এর disk ভরে ফেলতে পারে।

একটি অভ্যাস এখানেই গড়ে নিন: docker exec দেখার হাতিয়ার, মেরামতের নয়। চলন্ত container-এর ভেতরে ঢুকে একটি config বদলালে আপাতত কাজ হতে পারে, কিন্তু সেই বদল থাকে শুধু writable layer-এ। পরের deploy-এ নতুন container আসামাত্র তা হারিয়ে যায়, আর কেউ জানতেও পারে না কী বদলানো হয়েছিল। স্থায়ী বদল করতে হয় Dockerfile, environment বা config ফাইলে, তারপর নতুন container চালাতে হয়। তাই ঠিক করার সময়ও প্রশ্নটি হলো: এই বদল পরের container-এ পৌঁছাবে কীভাবে?

৩ · Port publish — host থেকে container-এ ঢোকার দরজা

Container-এর নিজের network আছে। তার ভেতরে nginx port 80-তে শুনছে, কিন্তু সেটি host-এর port 80 নয়। Host থেকে ঢুকতে হলে port publish করতে হয়। লঞ্চঘাটের কথা ভাবুন: টার্মিনালের ঘাট নম্বর (host port) আর লঞ্চের ভেতরের কেবিন নম্বর (container port) আলাদা। টিকিট কাউন্টার দুটিকে মিলিয়ে দেয়।

Host, bridge network, container আর ডেটা

Published port, DNS by name, volume outliving the container

Host, bridge network, container আর volume Host (Linux মেশিন) curl → 127.0.0.1:8080 -p 127.0.0.1:8080:80 · শুধু এই মেশিন থেকে User network: shop-net DNS 127.0.0.11: নাম থেকে IP web nginx · port 80 writable layer container-এর সাথে যায় api app · port 9000 writable layer container-এর সাথে যায় Bind mount host-এর ./site আপনি সামলান Named volume shop-data container মুছলেও থাকে tmpfs · host-এর memory-তে container থামলেই মুছে যায় memory সীমার হিসাবেও ধরা হয়
ছবি ৩৯.১ — একটি host-এ port, network আর ডেটার জায়গা। Writable layer container-এর সাথে যায়, volume থেকে যায়।

Dockerfile-এর EXPOSE 80 নিজে কোনো দরজা খোলে না; সে শুধু জানায় app কোন port ব্যবহার করে। দরজা খোলে -p. Publish না করলে docker ps-এর PORTS ঘরে শুধু 80/tcp দেখায়, কোনো তীরচিহ্ন থাকে না। তিন অংশের রূপটি মনে রাখুন: -p আইপি:host-port:container-port. শুরুতে আইপি লিখলে দরজাটি কার জন্য খোলা, সেটিও স্পষ্ট থাকে।

$ docker run -d --name abcl-dr-web --hostname demo \
    -p 127.0.0.1:18391:80 nginx:1.27-alpine
$ docker ps --filter name=abcl-dr-web --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES         STATUS         PORTS
abcl-dr-web   Up 2 seconds   127.0.0.1:18391->80/tcp
$ docker port abcl-dr-web
80/tcp -> 127.0.0.1:18391
$ curl -sI http://127.0.0.1:18391/ | sed -n 1,2p
HTTP/1.1 200 OK
Server: nginx/1.27.5

$ docker run -d --name abcl-dr-web2 -p 127.0.0.1:18391:80 nginx:1.27-alpine
… Bind for 127.0.0.1:18391 failed: port is already allocated
$ docker ps -a --filter name=abcl-dr-web2 --format '{{.Names}} {{.Status}}'
abcl-dr-web2 Created

কলামটি বাঁ থেকে ডানে পড়ুন: host-এর ঠিকানা আর port, তারপর container-এর port. দ্বিতীয় docker run ব্যর্থ হয়েছে, কারণ bind address, port আর protocol একই। খেয়াল করুন, ব্যর্থ container মুছে যায়নি; সে Created অবস্থায় পড়ে আছে, তাই আবার একই নাম ব্যবহার করতে হলে আগে docker rm লাগবে। একই port-এ UDP (-p 127.0.0.1:18391:80/udp) এই run-এ কোনো সংঘাত ছাড়াই চলেছে।

IP না লিখলে দরজা সবার জন্য খোলা। -p 8080:80 সাধারণত সব interface-এ শোনে, অর্থাৎ 0.0.0.0-এ। আর অনেক Linux host-এ Docker নিজের firewall নিয়ম বসায়, তাই ufw-এর মতো host firewall-এর নিয়ম এড়িয়ে port খোলা থাকতে পারে। ডেটাবেস বা admin panel সবসময় 127.0.0.1-এ publish করুন, অথবা একেবারেই publish না করে network-এর ভেতরে রাখুন। Cloud-এ পরের স্তর হলো security group (পর্ব ২৭). Packet-এর পুরো পথ, NAT আর veth-এর ব্যাখ্যা আছে Cloud ও Container Networking পর্বে।

নিচের চেকার সেই নিয়মটিই মেলায়: protocol আর port এক হলে, এবং IP এক হলে অথবা একপক্ষ সব interface হলে, সংঘাত। TRY তালিকা বদলে চালান।

// Port-publishing conflict checker. Edit TRY and run again.
const TRY = [
  "127.0.0.1:8080:80",     // web
  "127.0.0.1:8080:80",     // second web, same address
  "127.0.0.1:8080:53/udp", // same number, other protocol
  "127.0.0.1:8081:80",     // other host port, same container port
  "9000:9000",             // no IP: all interfaces
  "127.0.0.1:9000:9000",   // overlaps the line above
];
function parse(spec) {
  const [ports, proto = "tcp"] = spec.split("/");
  const p = ports.split(":");
  const ip = p.length === 3 ? p[0] : "0.0.0.0";
  return { ip, port: +p[p.length - 2], cport: +p[p.length - 1], proto, spec };
}
const clash = (a, b) => a.proto === b.proto && a.port === b.port &&
  (a.ip === b.ip || a.ip === "0.0.0.0" || b.ip === "0.0.0.0");

const taken = [];
for (const spec of TRY) {
  const want = parse(spec);
  const hit = taken.find(t => clash(want, t));
  if (hit) {
    console.log(`FAIL  -p ${spec}`);
    console.log(`      Bind for ${want.ip}:${want.port} failed: held by -p ${hit.spec}`);
    continue;
  }
  taken.push(want);
  const open = want.ip === "0.0.0.0" ? "  WARNING: reachable from every network" : "";
  console.log(`OK    -p ${spec}${open}`);
}
console.log(`held: ${taken.length} of ${TRY.length} requests`);

আউটপুটের মূল লাইন: FAIL -p 127.0.0.1:8080:80, আর শেষে held: 4 of 6 requests. চতুর্থ অনুরোধ চলেছে, কারণ container port একই হলেও host port আলাদা; এক image থেকে দুটি container চালাতে এটাই লাগে। ষষ্ঠটি ব্যর্থ, কারণ 9000:9000 আগেই সব interface দখল করেছে, আর 127.0.0.1 তার ভেতরেই পড়ে। পঞ্চম লাইনের WARNING-টি আসল Docker দেয় না; এটি আপনার জন্য রাখা হয়েছে।

৪ · ডেটা কোথায় থাকে — writable layer, volume, bind mount, tmpfs

প্রথমে সমস্যাটি নিজের চোখে দেখুন। একটি container ফাইল লিখল, তারপর মোছা হলো। নতুন container সেই ফাইল পাচ্ছে না:

$ docker run --name abcl-dr-w1 alpine:3.20 sh -c 'echo hello > /data.txt; cat /data.txt'
hello
$ docker rm abcl-dr-w1
$ docker run --rm --name abcl-dr-w2 alpine:3.20 sh -c 'cat /data.txt'; echo rc=$?
cat: can't open '/data.txt': No such file or directory
rc=1

এটি ত্রুটি নয়, এটিই নকশা। Container মেসের ভাড়াটের মতো: ঘর ছাড়লে ঘরের ভেতরের সব চলে যায়। যা রাখতে চান, তা রাখতে হবে গুদামঘরে, মানে container-এর বাইরে। Docker-এ চার রকম জায়গা আছে।

জায়গাকোথায় থাকেকতদিন টেকেকখন ব্যবহার
Writable layerContainer-এর নিজের স্তরdocker rm পর্যন্তসাময়িক ফাইল; স্থায়ী ডেটা নয়
Named volumeDocker-এর সামলানো জায়গাdocker volume rm পর্যন্তDatabase, upload, production ডেটা
Bind mountHost-এর নির্দিষ্ট ফোল্ডারHost-এর ফাইল যতদিনDevelopment-এ কোড, config ফাইল
tmpfsHost-এর memoryContainer থামা পর্যন্তগোপন সাময়িক ফাইল, cache

Cloud-এর “volume” (পর্ব ২০) একটি block disk, আর Docker volume হলো host-এর ভেতরে Docker-এর সামলানো জায়গা। নাম এক, জিনিস আলাদা। এবার ধাপে ধাপে দেখুন, ডেটা কীভাবে container-কে ছাড়িয়ে টিকে থাকে:

abcl-dr-v1প্রথম container
abcl-dr-v2পরের container
abcl-dr-datanamed volume, /data-তে বসানো
orders.txtvolume-এর ভেতরের ফাইল
এই মুহূর্তের অবস্থা
Container
Volume
orders.txt
  1. Volume তৈরি। Container-এর আগেই গুদামঘরটি বানানো হলো। নাম দেওয়া আছে, তাই পরে খুঁজে পাওয়া যাবে।
  2. প্রথম container লিখল। -v abcl-dr-data:/data volume-টিকে /data-তে mount করেছে। ফাইলটি writable layer-এ নয়, volume-এ গেছে।
  3. Container মুছে গেল। --rm শেষ হতেই container মুছেছে। কিন্তু volume-এর ফাইল অক্ষত, কারণ সে container-এর অংশ ছিল না।
  4. নতুন container, পুরোনো ডেটা। একদম নতুন container একই volume লাগাল, আগের লাইনটি পেল, আর নিজের লাইন যোগ করল। এখন ফাইলে দুটি লাইন। ডেটা container-এর নয়, volume-এর; এই প্রমাণটিই মূল কথা।
  5. মোছা হয় শুধু আদেশে। docker volume rm ছাড়া এটি যায় না। docker rm -v-ও named volume মোছে না (এই run-এ যাচাই করা); সে শুধু নামহীন volume মোছে। সুবিধা আর বিপদ দুটোই এখানে: ভুলে ডেটা হারায় না, কিন্তু অব্যবহৃত volume জায়গা খেতে থাকে।

Bind mount আর tmpfs-এর আচরণ এই run-এ এভাবে দেখা গেল:

# bind mount, read-only inside the container
$ echo '<h1>from host v1</h1>' > site/index.html
$ docker run -d --name abcl-dr-b -p 127.0.0.1:18392:80 \
    -v "$PWD/site:/usr/share/nginx/html:ro" nginx:1.27-alpine
$ curl -s http://127.0.0.1:18392/
<h1>from host v1</h1>
$ echo '<h1>from host v2</h1>' > site/index.html
$ curl -s http://127.0.0.1:18392/
<h1>from host v2</h1>
$ docker exec abcl-dr-b sh -c 'echo x > /usr/share/nginx/html/a.txt'
sh: can't create /usr/share/nginx/html/a.txt: Read-only file system

# tmpfs vs writable layer, same container started twice
$ docker run --name abcl-dr-t3 --tmpfs /scratch:size=1m alpine:3.20 \
    sh -c 'ls /scratch /root/; echo keep > /scratch/k; echo keep2 > /root/k2'
/root/:

/scratch:
$ docker start -a abcl-dr-t3
/root/:
k2

/scratch:

Bind mount-এ host-এর ফাইল বদলানো মাত্র container নতুন লেখা দেখিয়েছে, কোনো restart ছাড়াই। :ro থাকায় ভেতর থেকে লেখা আটকে গেছে। tmpfs-এর পরীক্ষায় দ্বিতীয়বার চালুর পরে /root/k2 আছে (writable layer), কিন্তু /scratch/k নেই (tmpfs). অর্থাৎ container থামলেই tmpfs মুছে যায়, মোছার দরকার হয় না।

Cloud-এ মনে রাখুন

Docker volume থাকে একটি নির্দিষ্ট host-এর disk-এ। ওই host মরলে বা VM মুছলে local volume-ও যায়, যদি না host-এর disk আলাদা টিকে থাকে। তাই volume মানেই backup নয়। Production ডেটায় তিনটি প্রশ্ন করুন: ডেটা কোন disk-এ, snapshot বা backup কোথায় (পর্ব ২৪), আর restore করে দেখা হয়েছে কি না। অনেক দল এজন্য database-কে managed service-এ রাখে (পর্ব ২২), আর container-কে stateless রাখে। তখন যেকোনো container মুছে নতুন চালালে কোনো ক্ষতি হয় না।

খালি volume আর খালি bind mount এক নয়। খালি named volume /usr/share/nginx/html-এ লাগালে image-এর ফাইল সেখানে কপি হয়েছে (50x.html, index.html). কিন্তু খালি ফোল্ডার bind mount করলে ফল এসেছে files: 0: host-এর ফোল্ডার image-এর ফাইল ঢেকে দেয়। “container চালু হলো, কিন্তু সব ফাইল উধাও” সমস্যার অনেক সময় এটাই কারণ। আর -v /:/host-এর মতো পুরো host filesystem bind mount করবেন না।

৫ · Container-এর network — নাম ধরে ডাকা

কোনো network না বললে container বসে default bridge-এ। একটি bridge হলো একই host-এ থাকা container-গুলোর ছোট্ট নিজস্ব switch. কিন্তু default bridge-এ একটি বড় অভাব আছে: নাম ধরে খোঁজা যায় না। docker network create দিয়ে বানানো user-defined bridge-এ Docker একটি embedded DNS দেয়। নিচের ধাপগুলোতে দুটি container-এর কথা বলা দেখুন। Container-এর IP প্রতিবার বদলাতে পারে, তাই নামটিই আসল ঠিকানা।

abcl-dr-clialpine, wget চালায়
abcl-dr-netbridge network
127.0.0.11Docker-এর DNS
abcl-dr-apinginx, port 80
Network-এর অবস্থা
Network-এ আছে
DNS-কে প্রশ্ন
ফলাফল
Terminalnetwork বানানো
$ docker network create abcl-dr-net
dd5a5903cadc…
Terminalpublish লাগেনি
$ docker run -d --name abcl-dr-api --network abcl-dr-net nginx:1.27-alpine
/etc/resolv.conf (client-এর ভেতরে)আসল আউটপুট
nameserver 127.0.0.11
options ndots:0
nslookup abcl-dr-apiIP লাইন বাদ
Server:    127.0.0.11
Name:      abcl-dr-api
…
Terminalএক container অন্যটিকে ডাকল
$ docker run --rm --name abcl-dr-cli --network abcl-dr-net alpine:3.20 \
    wget -qO- http://abcl-dr-api/ | grep -i '<title'
<title>Welcome to nginx!</title>
  1. Network তৈরি। একটি আলাদা bridge বানানো হলো। এখনো ভেতরে কেউ নেই।
  2. Server যোগ দিল। abcl-dr-api network-এ ঢুকল, আর তার নাম DNS-এ লেখা হলো। কোনো -p নেই, তাই host থেকে এটি নাগালের বাইরে। ভেতরের সার্ভিসের জন্য এটাই ভালো।
  3. Client যোগ দিল। তার resolv.conf-এ nameserver হলো 127.0.0.11. এটি বাইরের কোনো সার্ভার নয়; container-এর ভেতর থেকে Docker-এর DNS resolver-এর ঠিকানা।
  4. নাম থেকে IP. wget আগে DNS-কে জিজ্ঞেস করল। Docker জানে এই নাম কোন container-এর, তাই উত্তর দিল। IP-টি এই run-এর নিজস্ব, তাই দেখানো হয়নি।
  5. আসল অনুরোধ। Client সরাসরি api-এর port 80-তে গেল, bridge দিয়ে। Port publish লাগেনি, কারণ দুজন একই network-এ। উত্তরে এলো nginx-এর পাতা। এক container আরেকটিকে নাম ধরে ডেকেছে, এটাই প্রমাণ।
# client on the default bridge, server on abcl-dr-net
$ docker run --rm --name abcl-dr-cli alpine:3.20 wget -T 3 -qO- http://abcl-dr-api/; echo rc=$?
wget: bad address 'abcl-dr-api'
rc=1

# both on the default bridge
$ docker run -d --name abcl-dr-api2 nginx:1.27-alpine
$ docker run --rm --name abcl-dr-cli alpine:3.20 wget -T 3 -qO- http://abcl-dr-api2/; echo rc=$?
wget: bad address 'abcl-dr-api2'
rc=1

# attach a running container to the network, and the name works
$ docker run -d --name abcl-dr-lone alpine:3.20 sleep 60
$ docker exec abcl-dr-lone wget -T 3 -qO- http://abcl-dr-api/ | grep -c title
wget: bad address 'abcl-dr-api'
0
$ docker network connect abcl-dr-net abcl-dr-lone
$ docker exec abcl-dr-lone wget -T 3 -qO- http://abcl-dr-api/ | grep -i '<title'
<title>Welcome to nginx!</title>

দ্বিতীয় পরীক্ষাটি মূল কথা: দুজনই default bridge-এ, তবু bad address. সেখানে IP দিয়ে কথা বলা গেলেও নাম চলে না। তৃতীয় পরীক্ষা দেখায়, চলন্ত container-কেও docker network connect দিয়ে আরেকটি network-এ জোড়া যায়। একটি container একাধিক network-এ থাকতে পারে। এই কৌশলে web সামনের ও পেছনের দুই network-এই থাকে, আর database শুধু পেছনের network-এ। পর্ব ৪০-এ Compose এই network নিজেই বানিয়ে দেবে।

--networkকী পায়কখন
(কিছু না বললে) bridgeআলাদা network, নাম ধরে খোঁজা নেইএকবারের ছোট পরীক্ষা
নিজের networkআলাদা network, DNS by nameপ্রায় সব app-এর জন্য
hostHost-এর network সরাসরি, কোনো আলাদা করা নেইবিশেষ ক্ষেত্রে, বুঝে; -p-এর মানে থাকে না
noneLoopback ছাড়া কিছু নেইNetwork লাগে না এমন batch কাজ

৬ · Resource সীমা — memory মারে, CPU ধীর করে

Memory সীমা, OOM kill আর exit 137

সীমা না দিলে একটি container পুরো host-এর memory খেয়ে ফেলতে পারে, আর বাকি সব container বিপদে পড়ে। মেসের একজন বাসিন্দা পুরো পানির ট্যাংক শেষ করলে যা হয়। --memory (ছোট রূপ -m) cgroup-এর memory.max বসায়। সীমা পার হলে আর reclaim সম্ভব না হলে kernel-এর OOM killer সেই cgroup-এর একটি process-কে SIGKILL দেয়। কোনো সতর্কবার্তা নেই, app কিছু লেখার সুযোগও পায় না।

৮ MiB করে memory নেয় এমন একটি ছোট্ট Python প্রোগ্রাম চালানো হলো:

$ cat oom.py
chunks = []
for i in range(1, 100):
    chunks.append(b"x" * (8 * 1024 * 1024))
    print(f"allocated {i * 8} MiB", flush=True)

$ docker run --name abcl-dr-oom --memory 64m --memory-swap 64m \
    -v "$PWD/oom.py:/oom.py:ro" python:3.12-alpine python /oom.py; echo "rc=$?"
allocated 8 MiB
…
allocated 48 MiB
allocated 56 MiB
rc=137
$ docker inspect -f 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} mem={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}' abcl-dr-oom
exit=137 oom=true mem=67108864 swap=67108864

শেষ লাইন 56 MiB, তারপর কিছুই নেই: না traceback, না MemoryError. Process-টি মরেছে হঠাৎ, তাই log পড়ে কারণ বোঝা যায় না। OOMKilled ঘরটিই আসল প্রমাণ। 64m মানে 64 MiB, অর্থাৎ 64 × 2²⁰ = 67108864 byte, যা inspect-এ মিলে গেছে।

পরীক্ষা (এই run)শেষ সফল ধাপExitOOMKilled
--memory 64m --memory-swap 64m56 MiB137true
--memory 64m (swap ঘর 134217728)120 MiB137true
32m সীমা, 64m tmpfs-এ ddলেখা অসম্পূর্ণ137true
sh PID 1, ভেতরের Python মরল56 MiB0true

চারটি সারি থেকে চারটি শিক্ষা। প্রথমত, --memory-swap হলো memory আর swap-এর মোট সীমা। না দিলে Docker মোট সীমা দ্বিগুণ ধরে (host-এ swap থাকলে), তাই সীমা 64 হলেও 120 MiB পর্যন্ত গেছে, শুধু অনেক ধীরে। দ্বিতীয়ত, tmpfs-এ লেখা ডেটাও memory সীমায় গোনা হয়। তৃতীয়ত, ৪ নম্বর সারিটি সবচেয়ে সূক্ষ্ম: ছোট child process মরেছে, PID 1 (sh) বেঁচে ছিল, তাই exit 0, অথচ OOMKilled true. চতুর্থত, 137 মানেই OOM নয়; এই পর্বের শুরুতে docker stop-এও 137 এসেছিল, সেখানে oom=false.

সীমা কত দেবেন? একটি হিসাব

ধরে নিই একটি API-এর peak ব্যবহার load test-এ 180 MiB, আর আমরা ৩০% বাড়তি জায়গা রাখব: 180 × 1.3 = 234 MiB, তাই গোল করে --memory 256m. এবার host-এর হিসাব। ধরে নিই VM-এ 8 GiB RAM, OS আর Docker-এর জন্য 1 GiB রাখা হলো, তাহলে হাতে 7 GiB = 7168 MiB. 7168 ÷ 256 = 28, অর্থাৎ সব container peak-এ থাকলেও সর্বোচ্চ ২৮টি নিরাপদ। এর বেশি বসালে আপনি ধরে নিচ্ছেন সবাই একসাথে peak-এ যাবে না; একে overcommit বলে, আর ভুল হলে এবার host-এর OOM killer এলোমেলো process মারবে।

নিচের সিমুলেটর একই সিদ্ধান্ত নেয়। CASES-এ সীমা বদলে দেখুন কখন OOM হয়।

// Memory limit and the OOM decision. Sizes in MiB. Edit CASES.
// app = what the program itself needs at start, chunk = each allocation.
const CASES = [
  { name: "--memory 64m --memory-swap 64m", limit: 64, total: 64, app: 8, chunk: 8, want: 800 },
  { name: "--memory 64m (swap not set)", limit: 64, total: 128, app: 8, chunk: 8, want: 800 },
  { name: "--memory 256m, app wants 120", limit: 256, total: 256, app: 8, chunk: 8, want: 120 },
];
function run(c) {
  let done = 0, swapPeak = 0;
  while (done + c.chunk <= c.want) {
    const next = c.app + done + c.chunk;
    if (next > c.total) {
      console.log(`  need ${next} > allowed ${c.total}: OOM kill`);
      return { exit: 137, oom: true, done, swapPeak };
    }
    done += c.chunk;
    swapPeak = Math.max(swapPeak, next - c.limit);
  }
  return { exit: 0, oom: false, done, swapPeak };
}
for (const c of CASES) {
  console.log(c.name);
  const r = run(c);
  console.log(`  allocated ${r.done} MiB, swap peak ${r.swapPeak} MiB`);
  console.log(`  exit=${r.exit} OOMKilled=${r.oom}`);
}

প্রথম দুটি কেসের ফল (allocated 56 MiB আর allocated 120 MiB, swap peak 64 MiB) আসল run-এর সাথে মিলেছে। ধরে নেওয়া হয়েছে Python নিজে শুরুতে প্রায় ৮ MiB নেয়; এটি মাপা হিসাব নয়, মিলিয়ে নেওয়া অনুমান। তৃতীয় কেসে exit=0 OOMKilled=false: সীমা চাহিদার চেয়ে বড় হলে কিছুই হয় না। আসল ব্যবহার তাই আগে মাপুন, তারপর একটু বাড়তি জায়গা রেখে সীমা বসান।

CPU সীমা — মারে না, ধীর করে

Memory সীমা ভাঙলে process মরে। CPU সীমা ভাঙলে শুধু অপেক্ষা করতে হয়। কারণ CPU সময় ফুরালে ফেরত নেওয়ার কিছু নেই, পরের সময়ের জন্য থামিয়ে রাখলেই চলে। --cpus 0.5 মানে প্রতি 100 ms-এর মধ্যে সব থ্রেড মিলিয়ে মোট 50 ms CPU সময়। এটি একটি CPU quota, কোনো নির্দিষ্ট core নয়। ফুরিয়ে গেলে পরের period পর্যন্ত থামিয়ে রাখা হয়; একে বলে CPU throttling.

# burn.py: busy loop for 5 s of wall time, then print CPU used and cpu.stat
$ docker run --rm --name abcl-dr-cpu -v "$PWD/burn.py:/burn.py:ro" python:3.12-alpine python /burn.py
cpu.max: max 100000
wall 5.00s  cpu 5.00s  ratio 1.00  loops 78M
periods 0 throttled 0 throttled_sec 0.0

$ docker run --rm --name abcl-dr-cpu --cpus 0.5 -v "$PWD/burn.py:/burn.py:ro" python:3.12-alpine python /burn.py
cpu.max: 50000 100000
wall 5.00s  cpu 2.51s  ratio 0.50  loops 41M
periods 51 throttled 50 throttled_sec 2.49

$ docker run --rm --name abcl-dr-cpu --cpus 0.25 -v "$PWD/burn.py:/burn.py:ro" python:3.12-alpine python /burn.py
cpu.max: 25000 100000
wall 5.00s  cpu 1.26s  ratio 0.25  loops 20M
periods 52 throttled 51 throttled_sec 3.82

$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}' abcl-dr-cpu
NAME          CPU %
abcl-dr-cpu   49.69%

এই সংখ্যাগুলো প্রতি run-এ ও প্রতি মেশিনে একটু আলাদা হবে; অনুপাতটিই আসল। --cpus 0.5 ভেতরে cpu.max: 50000 100000 বসিয়েছে: quota 50000 µs, period 100000 µs. ৫ সেকেন্ডে CPU মিলেছে 2.51 s, লুপ 78M থেকে নেমে 41M. প্রায় প্রতিটি period-এই throttle হয়েছে (51-এর মধ্যে 50).

হিসাবের সূত্র

quota = cpus × 100 ms. একটি কাজে C ms CPU লাগলে এক থ্রেডে পূর্ণ period = ⌈C ÷ quota⌉ − 1, আর মোট সময় = পূর্ণ period × 100 ms + শেষের বাকি অংশ। উদাহরণ: 120 ms কাজ, --cpus 0.5: 50 + 50 + 20. দুটি পূর্ণ period গেল 200 ms, বাকি 20 ms, মোট 220 ms. সীমা না থাকলে যা 120 ms লাগত।

// CPU limit as quota per 100 ms period. Wall time of one job.
const PERIOD = 100;                              // ms
function wall(cpuMs, cpus, threads) {
  const quota = Math.min(cpus * PERIOD, threads * PERIOD);
  let left = cpuMs, t = 0, stalls = 0;
  for (;;) {
    const run = Math.min(quota, left);
    left -= run;
    if (left <= 0) return { ms: t + run / threads, stalls };
    t += PERIOD;
    if (quota < threads * PERIOD) stalls++;     // quota ran out: frozen
  }
}
const JOBS = [
  [5000, 1, 1], [5000, 0.5, 1], [5000, 0.25, 1],
  [120, 2, 1], [120, 0.5, 1], [180, 0.25, 1],
  [40, 1, 8], [400, 1, 8],
];
console.log("cpu-ms  --cpus  threads  cpu.max        wall-ms  stalls");
for (const [cpu, cpus, th] of JOBS) {
  const r = wall(cpu, cpus, th);
  const max = `${cpus * 100000} 100000`;
  console.log(String(cpu).padStart(6), String(cpus).padStart(7),
    String(th).padStart(8), " " + max.padEnd(14), r.ms.toFixed(1).padStart(7),
    String(r.stalls).padStart(7));
}

সারি 120 0.5 1-এ 220.0 ms, হিসাবের সাথে মিলেছে। শেষ দুটি সারি লক্ষ্য করুন। ৮ থ্রেডে 40 ms কাজ শেষ হয় 5 ms-এ। কিন্তু ৮ থ্রেডে 400 ms কাজে --cpus 1-এর quota প্রতি period-এ 12.5 ms-এই ফুরিয়ে যায়, আর ৩ বার থামতে হয়, ফলে 312.5 ms. গড় CPU কম দেখালেও এমন হঠাৎ চাপে latency লাফ দেয়। “CPU 40%, তবু API ধীর” সমস্যার পেছনে অনেক সময় এটাই থাকে।

৭ · Restart policy — কে আবার চালু করবে

Container থামলে Docker daemon restart policy দেখে সিদ্ধান্ত নেয়। চারটি নীতি পাঁচটি container-এ একসাথে চালিয়ে ১২ সেকেন্ড পরে গোনা হলো। সব container-এর কাজ একটাই: সাথে সাথে থেমে যাওয়া।

$ docker run -d --name abcl-dr-r-no  --restart no             alpine:3.20 sh -c 'exit 1'
$ docker run -d --name abcl-dr-r-of  --restart on-failure:3   alpine:3.20 sh -c 'exit 1'
$ docker run -d --name abcl-dr-r-of0 --restart on-failure:3   alpine:3.20 sh -c 'exit 0'
$ docker run -d --name abcl-dr-r-al  --restart always         alpine:3.20 sh -c 'exit 0'
$ docker run -d --name abcl-dr-r-us  --restart unless-stopped alpine:3.20 sh -c 'exit 1'
# 12 seconds later, docker inspect -f with Name, RestartPolicy, RestartCount, Status, ExitCode
/abcl-dr-r-no  policy=no             count=0 status=exited     exit=1
/abcl-dr-r-of  policy=on-failure     count=3 status=exited     exit=1
/abcl-dr-r-of0 policy=on-failure     count=0 status=exited     exit=0
/abcl-dr-r-al  policy=always         count=7 status=restarting exit=0
/abcl-dr-r-us  policy=unless-stopped count=7 status=restarting exit=1
# docker stop on the last two, wait 3 s
/abcl-dr-r-al  count=7 status=exited exit=0
/abcl-dr-r-us  count=7 status=exited exit=1
PolicyExit 0-এর পরেExit ≠ 0-এর পরেdocker stop-এর পরে daemon restart হলে
no (default)নানানা
on-failure[:N]নাহ্যাঁ, N বার পর্যন্তনা
alwaysহ্যাঁহ্যাঁহ্যাঁ, আবার চালু হয়
unless-stoppedহ্যাঁহ্যাঁনা, থেমেই থাকে

টেবিলের শেষ কলামটি Docker-এর ডকুমেন্টেশন থেকে নেওয়া; daemon restart করে আমরা পরীক্ষা করিনি। আর docker stop দুটি নীতিকেই থামিয়েছে, count 7-ই রয়ে গেছে। count=7-ই বলছে Docker ক্রমাগত চেষ্টা করে না। সে অপেক্ষা বাড়ায়, যাকে বলে exponential backoff: ডকুমেন্টেশন অনুযায়ী 100 ms থেকে শুরু, প্রতিবার দ্বিগুণ, আর container অন্তত ১০ সেকেন্ড চললে আবার 100 ms. আসল run-এ always আর exit 1-এ এই গণনা পাওয়া গেছে:

সময়৩ সে.৬ সে.৯ সে.১৫ সে.৩০ সে.
RestartCount56789

Policy বদলাতে container মুছতে হয় না: docker update --restart unless-stopped <name> চলন্ত container-এই নতুন নিয়ম বসায়। নিচের সিমুলেটর এই নিয়মগুলো একটি কাল্পনিক ঘড়িতে চালায়। ইনপুট বদলে দেখুন, app-এর চলার সময় (up) বাড়লে কী হয়।

// Restart policy with backoff, simulated clock (seconds).
function sim({ policy, retries = Infinity, exit, up, stopAt = Infinity, until }) {
  let t = 0, delay = 0.1, count = 0, last = "";
  const seen = [];                               // [time, count]
  for (;;) {
    t += up;                                     // the container runs, then exits
    if (t >= stopAt) { last = `docker stop at ${stopAt}s`; break; }
    const wants = policy === "always" || policy === "unless-stopped" ||
      (policy === "on-failure" && exit !== 0 && count < retries);
    if (!wants) { last = `policy says no (exit ${exit})`; break; }
    if (up >= 10) delay = 0.1;                   // ran 10 s or more: reset
    count++;
    seen.push([t, count]);
    if (t + delay >= until) { last = `still looping at ${until}s`; break; }
    t += delay;
    delay = Math.min(delay * 2, 60);             // double, cap at 1 minute
  }
  return { count, last, seen };
}
const at = (r, s) => r.seen.filter(x => x[0] <= s).length;

const crash = sim({ policy: "always", exit: 1, up: 0.25, until: 400 });
console.log("always, exit 1, 0.25 s each run");
console.log("  count at 3/6/9/15/30 s:", [3, 6, 9, 15, 30].map(s => at(crash, s)).join(" "));
console.log("  count at 2/5 min:", [120, 300].map(s => at(crash, s)).join(" "));
for (const c of [
  { policy: "no", exit: 1, up: 0.25, until: 60 },
  { policy: "on-failure", retries: 3, exit: 1, up: 0.25, until: 60 },
  { policy: "on-failure", retries: 3, exit: 0, up: 0.25, until: 60 },
  { policy: "always", exit: 1, up: 12, until: 300 },
  { policy: "unless-stopped", exit: 1, up: 0.25, stopAt: 20, until: 60 },
]) {
  const r = sim(c);
  console.log(`${c.policy} exit=${c.exit} up=${c.up}s -> count ${r.count}: ${r.last}`);
}

প্রথম সারি count at 3/6/9/15/30 s: 5 6 7 8 9 আসল গণনার সাথে হুবহু মিলেছে; এখানে প্রতি চালুতে ০.২৫ সেকেন্ড ধরা হয়েছে। পরের লাইনে 2/5 min: 11 14: অপেক্ষা ১ মিনিটে থামার পরে গণনা ধীরে ধীরে বাড়ে। সিমুলেটরে এই সর্বোচ্চ অপেক্ষা 60 সেকেন্ড ধরা হয়েছে। সবচেয়ে গুরুত্বপূর্ণ সারি always exit=1 up=12s -> count 25: app ১২ সেকেন্ড চলে মরলে অপেক্ষা প্রতিবার 100 ms-এ ফিরে আসে, তাই crash loop চলতেই থাকে এবং বাইরে থেকে দেখতে প্রায় সুস্থ লাগে।

Restart মেরামত নয়। এটি হঠাৎ crash-এর পরে সার্ভিস ফিরিয়ে আনে, কিন্তু কারণ ঠিক করে না। Config ভুল থাকলে হাজারবার restart করলেও একই ফল। বেড়ে চলা restart count-এ তাই alert রাখুন (পর্ব ৬৬). একক host-এ long-running service-এ সাধারণত unless-stopped বা on-failure বেছে নিন; একবার চলে শেষ হওয়া কাজে no বা on-failure:N. Cluster-এ এই দায়িত্ব orchestrator নেয়, তখন Docker-এর policy আর লাগে না (পর্ব ৪৩).

৮ · কম অধিকারে চালানো — read-only আর cap-drop

Container-এর ভেতরে root থাকলে সে host-এর পুরো root নয়, তবু Docker তাকে কিছু Linux capabilities দেয়। দরকার না থাকলেও সেগুলো থাকে। Least privilege মানে যা দরকার নেই, তা শুরুতেই সরিয়ে ফেলা। কোনো app আক্রান্ত হলেও তখন ক্ষতির সীমা ছোট থাকে। Non-root user-এর কথা পর্ব ৩৮-এ আছে; এখানে চালানোর সময়ের দুটি flag.

$ docker run --rm --name abcl-dr-ro --read-only alpine:3.20 sh -c 'touch /x; echo rc=$?'
touch: /x: Read-only file system
rc=1

$ docker run --name abcl-dr-ro --read-only nginx:1.27-alpine
…
nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system)
# exit=1. Give writable tmpfs only where nginx needs it:
$ docker run -d --name abcl-dr-ro --read-only --tmpfs /var/cache/nginx --tmpfs /run \
    -p 127.0.0.1:18393:80 nginx:1.27-alpine
$ curl -sI http://127.0.0.1:18393/ | head -1
HTTP/1.1 200 OK

$ docker run --rm --name abcl-dr-c alpine:3.20 sh -c 'grep CapEff /proc/self/status; touch /tmp/f; chown 1000 /tmp/f; echo chown rc=$?'
CapEff: 00000000a80425fb
chown rc=0
$ docker run --rm --name abcl-dr-c --cap-drop=ALL alpine:3.20 sh -c 'grep CapEff /proc/self/status; touch /tmp/f; chown 1000 /tmp/f; echo chown rc=$?'
CapEff: 0000000000000000
chown: /tmp/f: Operation not permitted
chown rc=1

$ docker run --name abcl-dr-c --cap-drop=ALL nginx:1.27-alpine
…
nginx: [emerg] chown("/var/cache/nginx/client_temp", 101) failed (1: Operation not permitted)
# exit=1. Add back only what this run needed:
$ docker run -d --name abcl-dr-c --cap-drop=ALL --cap-add=CHOWN --cap-add=SETGID \
    --cap-add=SETUID --cap-add=NET_BIND_SERVICE --cap-add=DAC_OVERRIDE \
    -p 127.0.0.1:18394:80 nginx:1.27-alpine
$ curl -sI http://127.0.0.1:18394/ | head -1
HTTP/1.1 200 OK

$ docker run --rm --name abcl-dr-c --cap-drop=ALL --read-only --security-opt no-new-privileges \
    --user 1000:1000 --memory 64m --cpus 0.5 --pids-limit 50 \
    python:3.12-alpine python -c 'print("hardened ok")'
hardened ok

দুটি ক্ষেত্রেই app প্রথমে ভেঙেছে, আর সেটাই পদ্ধতি। Read-only root filesystem দিলে যে path-এ লেখা দরকার, error সেটি নাম ধরে বলে দেয়; শুধু সেটিতে tmpfs দিন। --cap-drop=ALL-এ CapEff সব বিট শূন্য হয়েছে, আর error দেখে কোন capability লাগবে তা বোঝা গেছে। এই পাঁচটি capability এই image-এর এই run-এ যথেষ্ট ছিল; অন্য version-এ যাচাই করে নিন। শেষ লাইনে no-new-privileges আর --pids-limit-সহ পুরো কঠোর সেটিং hardened ok ছাপিয়েছে। মনে রাখবেন, এটি সুরক্ষার একটি স্তর মাত্র। AI যা-ই বলুক, --privileged এর ঠিক উল্টো পথ।

Flagযা আটকায়খেয়াল রাখুন
--read-onlyভেতরে ফাইল বদলানো বা নতুন tool রাখালেখার path-এ tmpfs বা volume দিন
--cap-drop=ALLRoot-এর বাড়তি ক্ষমতাশুধু দরকারিটি --cap-add
--userRoot হিসেবে চলাফাইলের মালিকানা মিলিয়ে নিন
no-new-privilegessetuid দিয়ে ক্ষমতা বাড়ানোSetuid-নির্ভর tool কাজ নাও করতে পারে
--pids-limitঅসংখ্য process তৈরি করে host ভরে ফেলাThread-ও এই সীমায় গোনা হয়
--memory, --cpusএকজনের জন্য সবার ক্ষতিআগে মেপে নিন

৯ · ভেঙে পড়া container-এর কারণ খোঁজা

রাতে alert এলো: orders সার্ভিস থেমে আছে। আন্দাজে restart না দিয়ে একটি ক্রম মেনে চলুন। হাসপাতালের ডাক্তারের মতো: আগে চিহ্ন দেখা, তারপর পরীক্ষা, শেষে চিকিৎসা। একটি কাল্পনিক অনলাইন বাজার অ্যাপের ছোট image দিয়ে এটি করা হলো। App-টি চালু হতে DB_URL নামের environment variable চায়।

# 1. what state is it in?
$ docker ps -a --filter name=abcl-dr-crash --format 'table {{.Names}}\t{{.Status}}'
NAMES           STATUS
abcl-dr-crash   Exited (1) 2 seconds ago

# 2. what did it say last? (logs work on an exited container)
$ docker logs abcl-dr-crash
starting orders service
Traceback (most recent call last):
  File "/app/app.py", line 3, in <module>
    db = os.environ["DB_URL"]
…
KeyError: 'DB_URL'

# 3. how did it end?
$ docker inspect -f 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} cmd={{.Config.Cmd}}' abcl-dr-crash
exit=1 oom=false cmd=[python /app/app.py]

# 4. exec cannot help: there is no process to join
$ docker exec abcl-dr-crash sh
Error response from daemon: container 695da60e89cc… is not running

# 5. look inside: copy from the stopped container, or start a shell from the same image
$ docker cp abcl-dr-crash:/app/app.py copied.py
$ docker run --rm --name abcl-dr-dbg --entrypoint sh abcl-dr-app:1 -c 'ls /app; env | grep -c DB_URL'
app.py
0

# 6. fix the cause and run a new container
$ docker run --rm --name abcl-dr-fix -e DB_URL=postgres://db.example/orders abcl-dr-app:1
starting orders service
connected to postgres://db.example/orders

ধাপ ২-তেই কারণ পাওয়া গেছে, তবু ধাপ ৩ বাদ দেবেন না: exit=1 oom=false OOM-কে বাদ দেয়। ধাপ ৪ দেখায় কেন docker exec মৃত container-এ চলে না। ধাপ ৫-এ --entrypoint sh একই image থেকে একটি shell খুলেছে, আর গণনা 0 প্রমাণ করেছে DB_URL সেট করা নেই। কারণ না বোঝা পর্যন্ত exited container-টি মুছবেন না; crash-এর প্রমাণ তার সাথেই আছে।

যা দেখছেনসম্ভাব্য অর্থপরের কাজ
Exit 1, log-এ tracebackApp-এর ভুল বা config নেইLog, environment, mount দেখুন
Exit 137, OOMKilled trueMemory সীমা পারব্যবহার মাপুন, leak খুঁজুন, তারপর সীমা বদলান
Exit 137, OOMKilled falsedocker stop সময় পার বা docker killSignal handling দেখুন, --init ভাবুন
Exit 0, OOMKilled trueভেতরের child মরেছেকোন worker মরল খুঁজুন
Exit 126 বা 127Command চলেনিImage-এর CMD আর PATH দেখুন
Restarting (1) 33 seconds agoCrash loop চলছেLog দেখুন; restart আরেকবার দেবেন না

আরও গভীরে: docker run একটি একক কাজ নয়। CLI কথা বলে daemon-এর সাথে, daemon containerd-কে, আর containerd প্রতি container-এর জন্য একটি shim রেখে runc দিয়ে process চালায়। তাই container চালু করতে ব্যর্থ হলে error-এ runc create failed ধরনের লেখা দেখতে পারেন। Status-এ restarting, removing আর dead-ও থাকতে পারে। Inspect-এর State একটি JSON: {"Status":"exited","Running":false,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":0,"ExitCode":3,…}. Script-এ এখান থেকেই অবস্থা পড়া নিরাপদ, কারণ docker ps-এর লেখা মানুষের জন্য, যন্ত্রের জন্য নয়।

পরীক্ষা ও ইন্টারভিউতে:

  • docker stop আর docker kill-এর পার্থক্য? Stop আগে নরম signal দিয়ে অপেক্ষা করে; kill সরাসরি SIGKILL পাঠায়।
  • Exit 137 মানে কি OOM? না। শুধু SIGKILL; OOM কি না তা OOMKilled ঘর বলে।
  • Volume আর bind mount-এর পার্থক্য? Volume Docker সামলায়, bind mount host-এর নির্দিষ্ট path; production ডেটায় সাধারণত volume.
  • Container নাম ধরে কথা বলে না কেন? সম্ভবত default bridge-এ আছে; user-defined network-এ রাখুন।
  • always আর unless-stopped-এর পার্থক্য? হাতে থামানোর পরে daemon restart হলে always আবার চালু হয়, unless-stopped হয় না।

১০ · AI-এর যুগে container চালানো

AI খুব দ্রুত একটি লম্বা docker run লাইন লিখে দেয়। লাইনটি চলবেও। সমস্যা হলো, চলা মানেই নিরাপদ নয়। AI আপনার মেশিন, ডেটা বা firewall দেখতে পায় না।

AI-এর ভুলঝুঁকিভালো রূপ
-p 5432:5432Database সব interface-এ খোলা-p 127.0.0.1:5432:5432, বা publish নয়
--privilegedপ্রায় সব সুরক্ষা বন্ধদরকারি একটি --cap-add
-e PASSWORD=admin123গোপন তথ্য history আর inspect-এSecret manager (পর্ব ৩৩)
:latest tagআজ যা চলল, কাল আলাদা হতে পারেনির্দিষ্ট version বা digest (পর্ব ৪১)
Volume ছাড়া databasedocker rm-এ সব ডেটা শেষNamed volume আর backup
সীমা ছাড়া containerএকটি leak-এ পুরো host বিপদে--memory, --cpus, --pids-limit
docker system prune -a --volumesসতর্কতা ছাড়াই image আর ডেটা মোছানিজের জিনিস নাম ধরে মোছা
নেই এমন flagError, অথবা ভুল অর্থdocker run --help আর অফিসিয়াল ডকুমেন্ট

AI-কে কাজ দেওয়ার সময় সীমা আর শর্ত বলে দিন, আর প্রতিটি flag-এর ব্যাখ্যা চান। গোপন তথ্য কখনো prompt-এ দেবেন না; নকল নাম ব্যবহার করুন।

আমি একটি Python API-এর Docker container একটি Linux server-এ চালাতে চাই। Image: example-api:1.4.2, app শোনে port 9000-এ। Database আলাদা container-এ চলে, যার নাম example-db. শর্ত: ১. শুধু 127.0.0.1-এ publish করো; সামনে nginx থাকবে। ২. User-defined network ব্যবহার করো। ৩. --memory, --memory-swap, --cpus আর --pids-limit দাও, আর কেন এই মান, সেটি বলো। মান জানতে আমাকে কী মাপতে হবে, তাও বলো। ৪. --read-only আর --cap-drop=ALL দিয়ে শুরু করো। কোন path-এ tmpfs লাগতে পারে, বলো। ৫. --privileged, latest tag, docker.sock mount আর পাসওয়ার্ড লেখা চলবে না। প্রতিটি flag কী করে এক লাইনে বলো। কোনো flag নিয়ে নিশ্চিত না হলে সেটি লিখে জানাও। শেষে যাচাইয়ের কমান্ড আর clean-up কমান্ড দাও।

AI-এর লেখা docker run চালানোর আগে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর উত্তর দেখুন।

প্র ০১Container-এর PID 1 এত বিশেষ কেন? --init না দিলে কী কী গোলমাল হতে পারে?

সাধারণ Linux মেশিনে PID 1 হলো systemd বা অন্য একটি init, যা এই কাজের জন্যই লেখা। Container-এ আলাদা PID namespace থাকায় আপনার app-টিই PID 1 হয়ে যায়। অথচ app-টি কখনো init হওয়ার জন্য লেখা হয়নি। এতে দুটি সমস্যা তৈরি হয়।

  • Signal: Kernel PID namespace-এর init-কে রক্ষা করে। সে নিজে handler না বসালে SIGTERM-এর default আচরণ তার ওপর খাটে না। তাই সাধারণ Python প্রোগ্রাম docker stop উপেক্ষা করল, আর পুরো grace period নষ্ট হলো। আসল run-এ এর ফল ছিল ৩ সেকেন্ড অপেক্ষা আর 137.
  • Zombie process: কোনো process মরলে তার parent-কে exit status নিতে হয়। Parent আগেই মরে গেলে orphan process-কে PID 1 দত্তক নেয়, আর তাকেই সেই কাজ করতে হয়। সাধারণ app তা করে না, ফলে শেষ হওয়া process table-এ জমতে থাকে। অনেক child চালায় এমন সার্ভিসে এটি --pids-limit পর্যন্ত পৌঁছে যেতে পারে।

এই সমস্যার পরিচিত সমাধান হলো tini-র মতো ছোট init. Docker-এ --init flag এই ধরনের একটি ছোট init চালায়, যা Docker 1.13 (২০১৭)-এ যোগ হয়েছিল। বিকল্প হলো app-এ নিজেই SIGTERM handler লেখা। ভালো app তা করেই: নতুন অনুরোধ নেওয়া বন্ধ করে, হাতের কাজ শেষ করে, connection বন্ধ করে বের হয়। Shell-form CMD-এ আরেকটি ফাঁদ আছে: PID 1 হয় sh, আর সে signal app-এ পাঠায় না।

মূল উপলব্ধি: Container মানে ছোট VM নয়; তার ভেতরে কোনো init আপনাআপনি নেই। Graceful shutdown তাই আপনার নকশার অংশ, আর exit code দেখেই বোঝা যায় সেটি কাজ করছে কি না।

প্র ০২Host-এর firewall চালু থাকার পরও একটি published port বাইরে থেকে খোলা থাকতে পারে কেন? এটি কীভাবে এড়াবেন?

কারণটি লুকিয়ে আছে packet-এর পথে। সাধারণ host firewall (যেমন ufw) মূলত host-এর নিজের দিকে আসা packet-এর নিয়ম সামলায়। কিন্তু Docker-এর publish করা port-এ আসা packet-এর গন্তব্য NAT দিয়ে container-এর ঠিকানায় বদলে যায়। তাই সেই packet host-এ শেষ না হয়ে forward হয়, আর Docker-এর নিজের নিয়ম সেই পথে আগে কাজ করে। ফলে অনেক Linux host-এ দেখা যায়, firewall-এ port বন্ধ, তবু Docker-এর port খোলা। Docker-এর ডকুমেন্টেশনও এই বিষয়ে সতর্ক করে। ঠিক কোন firewall ব্যবস্থায় কী হবে, তা version আর distribution-ভেদে আলাদা হতে পারে, তাই নিজের host-এ বাইরে থেকে পরীক্ষা করা জরুরি।

সুরক্ষার স্তরগুলো ক্রম ধরে ভাবুন:

  • Publish না করা: ভেতরের সার্ভিস (database, cache) user network-এ থাকুক। সবচেয়ে ভালো দরজা হলো যে দরজা নেই।
  • 127.0.0.1-এ বাঁধা: সামনে nginx থাকলে (পর্ব ১৪) app-কে শুধু loopback-এ রাখুন।
  • Cloud-এর স্তর: Security group আর network ACL host-এর বাইরে থেকে আটকায় (পর্ব ২৭), তাই container-এর ভুল সেখানে পৌঁছায় না।
  • যাচাই: মেশিনের বাইরে থেকে port পরীক্ষা করুন; শুধু নিজের কনফিগ পড়ে সন্তুষ্ট হবেন না।

খোলা database-এর ডেটা ফাঁস হওয়ার ঘটনা প্রায়ই এই ধরনের ভুল থেকেই হয়: developer ভেবেছেন “আমার firewall তো আছে।” -p 5432:5432 লাইনটি দেখতে নিরীহ, তাই কোড রিভিউতেও চোখ এড়িয়ে যায়।

মূল উপলব্ধি: একটি নিরাপত্তা নিয়ম কাজ করছে কি না, তা বুঝতে packet-এর আসল পথ জানতে হয়। নিয়ম লেখা আর নিয়ম কার্যকর হওয়া এক কথা নয়।

প্র ০৩Memory সীমা দিলেই কি app নিরাপদ? Java, Go বা Python সীমা জানে কীভাবে?

সীমা দিলে host নিরাপদ হয়, app নয়। Kernel শুধু সীমা প্রয়োগ করে; app-কে ভালোভাবে বলে না “কমাও।” আর এখানেই runtime-এর আচরণ জরুরি হয়ে ওঠে।

পুরোনো Java সংস্করণ host-এর পুরো memory দেখে heap-এর আকার ঠিক করত। ফলে 512 MiB সীমার container-এ heap সীমার চেয়ে অনেক বড় হতে চাইত, আর GC চালানোর আগেই OOM kill হতো। পরে JVM container-সচেতন হয় (Java 10-এ, পরে Java 8-এর update-এও যুক্ত হয়), আর cgroup দেখে হিসাব করতে শেখে। Go 1.19-এ GOMEMLIMIT এসেছে, যা garbage collector-কে একটি নরম সীমা জানায়। Python-এ এমন কোনো স্বয়ংক্রিয় সীমা সাধারণত নেই, তাই আমাদের প্রোগ্রামটি কোনো MemoryError না দিয়েই মরেছে। সংখ্যাগুলো version-এর সাথে বদলায়, তাই নিজের runtime-এর ডকুমেন্ট দেখুন।

আরেকটি স্তর হলো cgroup v2-এর memory.high, যা kill না করে চাপ দিয়ে ধীর করে। Docker-এর --memory-reservation ভিন্ন, নরম একটি লক্ষ্য। আর worker pool-ওয়ালা সার্ভিসে সবচেয়ে সূক্ষ্ম বিপদ: একটি worker মারা গেলে মূল process বেঁচে থাকে, যেমনটি আমাদের exit 0, OOMKilled true run-এ হয়েছে। সার্ভিস তখন আধা-সুস্থ অবস্থায় চলে। এজন্য cgroup v2-তে memory.oom.group পুরো দলকে একসাথে মারতে পারে, আর Kubernetes-এর নতুন সংস্করণ এই পথ ব্যবহার করে বলে জানা যায়; নিজের version দেখে নিন। Kubernetes-এর memory limit আসবে পর্ব ৪৮-এ।

ভালো অভ্যাস: load test-এ peak মাপুন, তার ওপরে কিছু জায়গা রাখুন, runtime-কে সীমা জানান, restart count আর OOM সংখ্যায় alert রাখুন। Swap দিয়ে সমস্যা ঢাকা গেলেও গতি নষ্ট হয়; আমাদের 120 MiB-র run চলেছে, কিন্তু সার্ভারে সেই ধীরতাই আরেকটি incident হতে পারত।

মূল উপলব্ধি: Memory সীমা হলো একটি চুক্তি, যা app, runtime আর kernel তিন পক্ষকেই জানতে হবে। একপক্ষ না জানলে ফল নিঃশব্দ SIGKILL.

প্র ০৪CPU limit কি সবসময় ভালো? অনেক দল কেন production-এ এটি নিয়ে তর্ক করে?

CPU limit একটি নিশ্চিত সুবিধা দেয়: একটি container অন্যদের ভাগ কেড়ে নিতে পারে না। কিন্তু দাম হলো latency. আমাদের সিমুলেটরে ৮ থ্রেডের 400 ms কাজ --cpus 1-এ 312.5 ms নিয়েছিল, যদিও মেশিনে হয়তো core খালি পড়ে ছিল। গড়ে CPU 40% দেখালেও একটি 100 ms period-এর শুরুতে সব থ্রেড একসাথে দৌড়ে quota শেষ করে ফেলে, তারপর সবাই বসে থাকে। Monitoring-এর এক মিনিটের গড়ে এটি একেবারেই দেখা যায় না।

তাই তর্কের দুই পক্ষ:

  • Limit রাখার পক্ষে: আচরণ অনুমানযোগ্য, খরচ নিয়ন্ত্রিত, আর একজনের bug অন্যের সমস্যা হয় না। ভাগ করা host-এ এটি প্রায় আবশ্যিক।
  • Limit তুলে দেওয়ার পক্ষে: শুধু ন্যায্য ভাগ (--cpu-shares বা Kubernetes-এর request) দিলে খালি core নষ্ট হয় না, আর latency-র ঝাঁকুনি কমে। কিছু দল তাই latency-সংবেদনশীল সার্ভিসে এই পথ নেয়।

আরেকটি গোপন ফাঁদ: runtime অনেক সময় host-এর core দেখে থ্রেড সংখ্যা ঠিক করে। ৬৪ core-এর host-এ --cpus 2 container-এ যদি ৬৪টি worker থ্রেড জন্মায়, quota চোখের পলকে শেষ। Go-তে এজন্য automaxprocs নামের library বহুল ব্যবহৃত ছিল, আর নতুন Go সংস্করণ cgroup সীমা নিজেই দেখতে শুরু করেছে বলে জানা যায়। JVM-ও এখন cgroup দেখে। প্রমাণ পেতে cpu.stat-এর nr_throttled আর throttled_usec দেখুন, যেমনটি আমরা দেখেছি: 51 period-এর মধ্যে 50 throttled.

মূল উপলব্ধি: কোনো একটি “সঠিক” উত্তর নেই। Host ভাগ করা হলে limit ন্যায্যতা দেয়, latency-সংবেদনশীল হলে throttle মেপে সিদ্ধান্ত নিন। গড় CPU নয়, throttle সংখ্যা দেখুন।

প্র ০৫Restart policy থাকলে self-healing হয়ে গেল? Backoff কেন দরকার, আর এর সীমা কোথায়?

Restart একটি নির্দিষ্ট রকম ব্যর্থতাই সারায়: সাময়িক ও এলোমেলো ব্যর্থতা। যেমন, ক্ষণিকের network সমস্যায় app ভেঙে পড়ল, আর পরের চেষ্টায় সব ঠিক। কিন্তু config ভুল, schema না মেলা, disk ভরা বা নির্ভরশীল সার্ভিস বন্ধ থাকলে হাজার restart-ও একই ফল আনে।

Backoff না থাকলে কী হতো? একটি app মুহূর্তে ক্র্যাশ করলে সে প্রতি সেকেন্ডে কয়েকবার চালু হতো। প্রতিবার database-এ নতুন connection, log-এ traceback, CPU-তে চাপ। অর্থাৎ অসুস্থ সার্ভিস তার নির্ভরশীল সিস্টেমকেও অসুস্থ করে। ধরুন ৫০টি container একসাথে crash করল আর database ফিরে আসতে সময় নিচ্ছে। সবাই একসাথে আঘাত করলে সেটি উঠতেই পারবে না; একে thundering herd বলে। তাই Docker অপেক্ষা দ্বিগুণ করে, আর আমাদের আসল run-এ ৩০ সেকেন্ডে মাত্র 9 বার চেষ্টা হয়েছে। একই ধারণা আছে পর্ব ১১-এর systemd-তে, আর Kubernetes-এর CrashLoopBackOff-এও (পর্ব ৪৩), যদিও তাদের সংখ্যা আলাদা।

সীমাগুলোও জেনে রাখুন। Docker-এর restart একটি মাত্র host-এ কাজ করে; host মরলে সব শেষ। Process চলছে মানেই app কাজ করছে, তাও নয়: deadlock-এ আটকে থাকা app-কে restart policy ধরে না। সেটি ধরে health check (পর্ব ১৭, পর্ব ৪৩). আর ১২ সেকেন্ড চলে মরা app বারবার backoff রিসেট করে, যা আমাদের সিমুলেটর দেখিয়েছে। তাই restart count-এ alert না থাকলে বাইরে থেকে সব সুস্থ দেখাবে, আর ভেতরে সার্ভিস কাটা কাটা চলবে।

মূল উপলব্ধি: Restart হলো ব্যান্ডেজ, চিকিৎসা নয়। এটি সময় কিনে দেয়; সেই সময়ে কারণ খুঁজে বের করার কাজটি মানুষের, আর সেজন্য লাগে সংকেত: restart count, exit code, log.

অনুশীলন

Docker না থাকলেও এগুলো করা যাবে; সিমুলেটর আর আসল আউটপুট দিয়েই যাচাই করুন।

  1. হিসাব: একটি container --cpus 0.25 --memory 256m দিয়ে চলছে। (ক) cpu.max-এ কী দেখবেন? (খ) memory.max কত byte? (গ) এক থ্রেডে 180 ms CPU লাগে এমন একটি অনুরোধের সময় কত?

    (ক) 25000 100000. (খ) 256 × 2²⁰ = 268435456 byte. (গ) quota প্রতি period-এ 25 ms. 180 ÷ 25 = 7.2, তাই ৭টি পূর্ণ period (175 ms কাজ, 700 ms সময়), তারপর বাকি 5 ms. মোট 705 ms. cpu_quota.js-এর আউটপুটের 180 0.25 সারিতেও এই সংখ্যাই আছে।

  2. ফল আগে বলুন: নিচের চারটি কমান্ড পরপর চললে শেষ দুটির আউটপুট আর exit code কী হবে? volume data আগে ছিল না।
    docker run --rm -v data:/d alpine:3.20 sh -c 'echo 1 >> /d/n'
    docker run --rm -v data:/d alpine:3.20 sh -c 'echo 1 >> /d/n'
    docker run --rm alpine:3.20 cat /d/n; echo "rc=$?"
    docker run --rm -v data:/d alpine:3.20 wc -l /d/n; echo "rc=$?"

    আসল run-এ (নাম আলাদা ছিল) ফল:

    cat: can't open '/d/n': No such file or directory
    rc=1
    2 /d/n
    rc=0

    প্রথম দুটি container নিজে থেকেই তৈরি হওয়া volume-এ দুটি লাইন লিখেছে, আর --rm-এও named volume মোছেনি। তৃতীয়টিতে -v নেই, তাই /d ফোল্ডারটিই নেই। চতুর্থটি আবার volume লাগিয়ে দুটি লাইনই পেয়েছে।

  3. নকশা ও সমালোচনা: AI এই কমান্ড দিল। অন্তত পাঁচটি সমস্যা ধরুন, আর একটি ভালো রূপ লিখুন: docker run -d --privileged --restart always -p 5432:5432 -e POSTGRES_PASSWORD=admin123 postgres:latest

    সমস্যা: --privileged অকারণ; port সব interface-এ খোলা; পাসওয়ার্ড command-এ লেখা ও দুর্বল; latest tag; ডেটার কোনো volume নেই; memory আর CPU সীমা নেই; নির্ভরশীল সার্ভিসের জন্য user network নেই। একটি ভালো রূপ (version-টি নমুনা, নিজের প্রয়োজনে মিলিয়ে নিন):

    docker network create shop-net
    docker volume create shop-db
    docker run -d --name shop-db --network shop-net \
      --restart unless-stopped \
      --memory 1g --memory-swap 1g --cpus 1 --pids-limit 200 \
      --env-file ./db.env \
      -v shop-db:/var/lib/postgresql/data \
      postgres:16.4

    কোনো -p নেই: app একই network থেকে shop-db নামে ঢুকবে। db.env ফাইলটি git-এ যাবে না, আর production-এ secret manager ভালো (পর্ব ৩৩). এই নকশা চালানোর আগে backup-এর ব্যবস্থাও ভাবুন (পর্ব ২৪). এই কমান্ডটি আমরা চালাইনি; নমুনা হিসেবে পড়ুন।

  4. রোগ নির্ণয়: চারটি container-এর অবস্থা দেওয়া আছে। প্রতিটির সম্ভাব্য কারণ আর পরের কাজ বলুন। (ক) exit=137 oom=false, docker stop ধীর ছিল। (খ) exit=137 oom=true. (গ) exit=0 oom=true. (ঘ) Restarting (1) 33 seconds ago.

    (ক) Grace period শেষে SIGKILL. App signal পাচ্ছে না: SIGTERM handler লিখুন বা --init দিন, exec form CMD ব্যবহার করুন। (খ) Memory সীমা পার: docker stats দিয়ে আসল ব্যবহার মাপুন, leak আছে কি না দেখুন, প্রমাণ পেলে সীমা বাড়ান। (গ) ভেতরের একটি child মরেছে, মূল process বেঁচে ছিল: কোন worker মরল খুঁজুন, app-এর আচরণ যাচাই করুন। (ঘ) Crash loop চলছে, এখন backoff-এর অপেক্ষায়: docker logs দেখুন, আবার restart না দিয়ে কারণ ঠিক করুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমভেঙে পড়া container-এর কারণ খোঁজার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. docker ps -a দিয়ে অবস্থা দেখা
  2. docker logs দিয়ে শেষ কথা পড়া
  3. docker inspect দিয়ে exit code আর OOMKilled দেখা
  4. --entrypoint sh দিয়ে একই image-এ shell খুলে পরীক্ষা
  5. কারণ ঠিক করে নতুন container চালানো

একটি container-এর State.ExitCode 137 আর State.OOMKilled false. সবচেয়ে সম্ভাব্য ব্যাখ্যা কোনটি?

  • Memory সীমা পার হয়েছে
  • Image-এ command পাওয়া যায়নি
  • docker stop-এর সময় পার হয়ে SIGKILL এসেছে, অথবা docker kill চলেছে
  • App segmentation fault করেছে

Database ডেটা container মোছার পরও টিকিয়ে রাখতে production-এ সাধারণত কোনটি ব্যবহার করা হয়?

  • Container-এর writable layer
  • Named volume
  • tmpfs
  • Image-এর একটি layer

দুটি container default bridge-এ আছে। একটি থেকে wget http://other/ চালালে bad address আসছে। সবচেয়ে ভালো সমাধান কোনটি?

  • দুটি container-এই -p দেওয়া
  • দুটিকেই host network-এ চালানো
  • IP হাতে লিখে রাখা
  • একটি user-defined network বানিয়ে দুটিকেই সেখানে রাখা

--cpus 0.5 দেওয়া একটি container বেশি CPU চাইলে কী ঘটে?

  • Period-এর quota ফুরালে পরের period পর্যন্ত তাকে থামিয়ে রাখা হয়
  • Kernel তাকে SIGKILL দিয়ে মেরে ফেলে
  • OOM killer চালু হয়
  • তাকে অর্ধেক core-এ বেঁধে দেওয়া হয়

--memory 128m দিলেন, --memory-swap দিলেন না, আর host-এ swap আছে। Memory আর swap মিলিয়ে মোট সীমা সাধারণত কত হবে?

  • 128 MiB
  • 256 MiB
  • কোনো সীমা নেই
  • 192 MiB

একটি container --restart unless-stopped দিয়ে চলছিল। আপনি docker stop চালালেন, পরে Docker daemon restart হলো। কী হবে?

  • আবার চালু হবে
  • Container মুছে যাবে
  • থেমেই থাকবে
  • ১০০ ms পরে চালু হবে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে একসাথে:

মনে রাখুন

  1. Lifecycle: created → running → paused → exited → removed. Exited container-এ log থাকে, removed-এ কিছুই নয়।
  2. Exit code = 128 + signal: 137 SIGKILL, 143 SIGTERM, 139 SIGSEGV. OOM কি না, তা বলে শুধু OOMKilled.
  3. স্থায়ী ডেটা named volume-এ; port সবসময় IP লিখে publish করুন; নাম ধরে ডাকতে user-defined network.
  4. --memory পার হলে মৃত্যু, --cpus পার হলে ধীরতা. দুটোই আগে মেপে বসান।
  5. Restart মেরামত নয়; crash-এ ক্রম মানুন: ps → logs → inspect → shell → ঠিক করা.

পরের পর্বে: এখন পর্যন্ত প্রতিটি container হাতে চালিয়েছেন। পর্ব ৪০-এ Docker Compose এই network, volume আর environment একটি ফাইলে লিখে পুরো stack একসাথে চালাবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান