অফিসিয়াল Dockerfile বেস্ট প্র্যাকটিস পাতায় আপনার Dockerfile আরও উন্নত করার বিষয়ে অনেক দরকারি বিষয় আছে।
আপনার মূল লক্ষ্য হওয়া উচিত পারফরম্যান্স অপ্টিমাইজ করা (বিশেষ করে টেস্ট রানারের ক্ষেত্রে)। এতে আপনার টুলিং যত দ্রুত সম্ভব চলবে এবং টাইম-আউট হবে না।
এক্সিকিউশন টাইম বারবার মাপা টুলিংয়ের পারফরম্যান্স সম্পর্কে ধারণা পাওয়ার দারুণ একটি উপায়। কোনো পরিবর্তনের পরেও এবং আগেও এক্সিকিউশন টাইম মাপার অভ্যাস গড়ে তুলুন। এমনকি যখন আপনার মনে হয় কোনো পরিবর্তন নিশ্চিতভাবেই পারফরম্যান্স বাড়াবে, তখনও এক্সিকিউশন টাইম মাপা উচিত।
সম্ভব হলে পারফরম্যান্স স্বয়ংক্রিয়ভাবে মাপার জন্য স্ক্রিপ্ট তৈরি করুন (একে _বেন্চমার্কিং_ও বলা হয়)। একটি খুবই কাজের কমান্ড-লাইন টুল হলো hyperfine, তবে আপনার টুলিংয়ের জন্য যেটি সবচেয়ে বেশি মানানসই মনে হয়, সেটিই নির্দ্বিধায় ব্যবহার করুন।
নতুন ট্র্যাক টুলিং রিপোজিটরিগুলোতে নিচের দুটি স্ক্রিপ্ট পাওয়া যাবে:
./bin/benchmark.sh: ট্র্যাক টুলিং কোড বেন্চমার্ক করে (সোর্স কোড)./bin/benchmark-in-docker.sh: ট্র্যাক টুলিংয়ের Docker ইমেজ বেন্চমার্ক করে (সোর্স কোড)যদি আপনি এমন একটি ট্র্যাক টুলিং রিপোজিটরিতে কাজ করেন যেখানে এই ফাইলগুলো নেই, তাহলে উপরের সোর্স লিংক ব্যবহার করে নির্দ্বিধায় সেগুলো আপনার রিপোতে কপি করে নিতে পারেন।
বেন্চমার্কিং স্ক্রিপ্ট টুলিংয়ের পারফরম্যান্স অনুমান করতে সাহায্য করে। তবে মনে রাখবেন, Exercism-এর প্রোডাকশন সার্ভারে পারফরম্যান্স প্রায়ই কম হয়।
বিভিন্ন বেস ইমেজ নিয়ে পরীক্ষা করে দেখুন (যেমন Ubuntu-র বদলে Alpine), দেখুন একটি অন্যটির চেয়ে (উল্লেখযোগ্যভাবে) ভালো পারফরম্যান্স দেয় কি না। পারফরম্যান্স যদি প্রায় সমান হয়, তাহলে সবচেয়ে ছোট ইমেজটি বেছে নিন।
none-এর বদলে internal নেটওয়ার্ক ব্যবহার করলে পারফরম্যান্স বাড়ে কি না, তা দেখে নিন।
আরও তথ্যের জন্য নেটওয়ার্ক ডকুমেন্টেশন দেখুন।
ট্র্যাক টুলিং একটি একবার-চলা, অল্পস্থায়ী Docker কন্টেইনার রান করে, যা নিচের ধাপগুলো সম্পাদন করে।
তাই ধাপ ২-এ যে কোড চলে, সেটি প্রতিটি টুলিং রানেই চলে। এই কারণে ধাপ ২-এ চলা কোডের পরিমাণ কমানো পারফরম্যান্স বাড়ানোর দারুণ একটি উপায়। এটি করার একটি উপায় হলো কোড রান-টাইম থেকে বিল্ড-টাইমে সরিয়ে নেওয়া। রান-টাইম কোড প্রতিটি টুলিং রানে চলে, কিন্তু বিল্ড-টাইম কোড চলে মাত্র একবার (যখন Docker ইমেজ তৈরি হয়)।
বিল্ড-টাইম কোড একটি GitHub Actions ওয়ার্কফ্লোর অংশ হিসেবে একবার চলে। তাই বিল্ড-টাইমে চলা কোড (তুলনামূলকভাবে) ধীর হলেও সমস্যা নেই।
Haskell টেস্ট রানারে টেস্ট চালাতে গেলে কিছু বেস লাইব্রেরি কম্পাইল করার দরকার হয়। যেহেতু প্রতিটি টেস্ট রান একটি নতুন কন্টেইনারে হয়, তাই এর মানে হলো এই কম্পাইলিং প্রতিটি টেস্ট রানেই করতে হয়! এটি এড়াতে Haskell টেস্ট রানারের Dockerfile-এ নিচের দুটি কমান্ড আছে:
COPY pre-compiled/ .
RUN stack build --resolver lts-20.18 --no-terminal --test --no-run-tests
প্রথমে pre-compiled ডিরেক্টরিটি ইমেজে কপি করা হয়।
এই ডিরেক্টরিটি একটি টেস্ট অনুশীলনী হিসেবে সেট আপ করা, এবং এটি আসল অনুশীলনীটি যে বেস লাইব্রেরিগুলোর উপর নির্ভর করে, সেগুলোর উপরই নির্ভর করে।
তারপর আমরা ওই ডিরেক্টরিতে টেস্ট চালাই, যা আসল অনুশীলনীর জন্য টেস্ট চালানোর মতোই।
টেস্ট চালালে বেস কম্পাইল হয়ে যায়, তবে পার্থক্য হলো এটি ঘটে বিল্ড টাইমে।
ফলে তৈরি হওয়া Docker ইমেজের বেস লাইব্রেরিগুলো আগেই কম্পাইল করা থাকে।
এর মানে রান টাইমে আর কম্পাইল করার দরকার হয় না, যার ফলে এক্সিকিউশন হয় (অনেক) দ্রুত।
কিছু ভাষায় কোড আগেভাগে বা ঠিক চালানোর সময়ে কম্পাইল করার সুযোগ থাকে। এটি বিল্ড টাইম বনাম রান টাইম-এর একটি ভারসাম্যের বিষয়, আর এখানেও আমরা পারফরম্যান্সের কারণেই বিল্ড টাইম এক্সিকিউশনকে বেছে নিই।
C# টেস্ট রানারের Dockerfile-এ এই পদ্ধতিই ব্যবহার করা হয়েছে, যেখানে কোড ঠিক চলার সময়ে (রান টাইমে) কম্পাইল করার বদলে টেস্ট রানারকে আগেভাগে (বিল্ড টাইমে) একটি বাইনারিতে কম্পাইল করা হয়। এর মানে রান টাইমে করার কাজ কমে যায়, যা পারফরম্যান্স বাড়াতে সাহায্য করবে।
আপনার ইমেজের আকার কমানোর চেষ্টা করা উচিত, যার ফলে এটি:
ভিন্ন ভিন্ন ডিস্ট্রিবিউশনের ইমেজের আকারও ভিন্ন হয়।
উদাহরণস্বরূপ, alpine:3.20.2 ইমেজটি ubuntu:24.10 ইমেজের চেয়ে দশ গুণ ছোট:
REPOSITORY TAG SIZE
alpine 3.20.2 8.83MB
ubuntu 24.10 101MB
সাধারণভাবে Alpine-ভিত্তিক ইমেজগুলো সবচেয়ে ছোট ইমেজগুলোর মধ্যে পড়ে, তাই অনেক টুলিং ইমেজ Alpine-এর উপর ভিত্তি করে তৈরি।
কিছু ইমেজের বিশেষ "slim" ভ্যারিয়েন্ট থাকে, যেখানে কিছু ফিচার সরিয়ে দেওয়া হয়, ফলে ইমেজের আকার ছোট হয়।
উদাহরণস্বরূপ, node:20.16.0-slim ইমেজটি node:20.16.0 ইমেজের চেয়ে পাঁচ গুণ ছোট:
REPOSITORY TAG SIZE
node 20.16.0 1.09GB
node 20.16.0-slim 219MB
"slim" ভ্যারিয়েন্টগুলো ছোট হওয়ার কারণ হলো এতে ফিচার কম থাকে। আপনার ইমেজের হয়তো অতিরিক্ত ফিচারগুলোর দরকার নেই, তাহলে "slim" ভ্যারিয়েন্ট ব্যবহারের কথা ভাবুন।
আপনার ইমেজের আকার কমানোর একটি স্পষ্ট, তবে দারুণ উপায় হলো আপনার যেসব জিনিস দরকার নেই, সেগুলো সরিয়ে ফেলা। এর মধ্যে থাকতে পারে:
বেশিরভাগ Docker ইমেজে অতিরিক্ত প্যাকেজ ইনস্টল করতে হয়, যা সাধারণত একটি প্যাকেজ ম্যানেজারের মাধ্যমে করা হয়। এই প্যাকেজগুলো বিল্ড টাইমে ইনস্টল করতে হয় (কারণ রান টাইমে কোনো ইন্টারনেট সংযোগ থাকে না)। তাই অতিরিক্ত প্যাকেজ ইনস্টল করার পর প্যাকেজ ম্যানেজারের যেকোনো ক্যাশিং/হিসাব রাখার ফাইল সরিয়ে ফেলা উচিত।
যে ডিস্ট্রিবিউশনগুলো apk প্যাকেজ ম্যানেজার ব্যবহার করে (যেমন Alpine), তারা প্যাকেজ ইনস্টল করতে apk add ব্যবহার করার সময় --no-cache ফ্ল্যাগ ব্যবহার করা উচিত:
RUN apk add --no-cache curl
যে ডিস্ট্রিবিউশনগুলো apt-get/apk প্যাকেজ ম্যানেজার ব্যবহার করে (যেমন Ubuntu), তারা প্যাকেজ ইনস্টল করার পরে এবং একই RUN কমান্ডের মধ্যে apt-get autoremove -y এবং rm -rf /var/lib/apt/lists/* কমান্ড চালানো উচিত:
RUN apt-get update && \
apt-get install curl -y && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
Docker-এ মাল্টি-স্টেজ বিল্ড নামে একটি ফিচার আছে। এটি দিয়ে আপনি আপনার Dockerfile-কে আলাদা আলাদা স্টেজে ভাগ করতে পারেন, যেখানে কেবল শেষ স্টেজটিই তৈরি হওয়া Docker ইমেজে থাকে (বাকিগুলো কেবল শেষ স্টেজটি তৈরি করতে সহায়তা করার জন্যই থাকে)। প্রতিটি স্টেজকে নিজস্ব ছোট একটি Dockerfile হিসেবে ভাবতে পারেন; স্টেজগুলো ভিন্ন ভিন্ন বেস ইমেজ ব্যবহার করতে পারে।
আপনার Dockerfile-এ যদি এমন প্যাকেজ ইনস্টল করার দরকার হয় যেগুলো কেবল বিল্ড টাইমেই লাগে, তাহলে মাল্টি-স্টেজ বিল্ড বিশেষভাবে কাজে আসে। এমন পরিস্থিতিতে আপনার Dockerfile-এর সাধারণ গঠনটি হয় এমন:
এই সেটআপে অতিরিক্ত প্যাকেজগুলো কেবল "build" স্টেজেই ইনস্টল হয়, "runtime" স্টেজে নয়, তাই সেগুলো তৈরি হওয়া Docker ইমেজে যুক্ত হয় না।
Fortran টেস্ট রানারের কিছু ফাইল ডাউনলোড করতে curl দরকার।
কিন্তু এর রান টাইম ইমেজে curl-এর দরকার নেই, তাই এটি মাল্টি-স্টেজ বিল্ডের জন্য একেবারে উপযুক্ত উদাহরণ।
প্রথমে এর Dockerfile-এ একটি স্টেজ ডিফাইন করা হয়েছে (নাম "build"), যেখানে curl প্যাকেজ ইনস্টল করা হয়।
এরপর curl দিয়ে ওই স্টেজে ফাইল ডাউনলোড করা হয়।
FROM alpine:3.15 AS build
RUN apk add --no-cache curl
WORKDIR /opt/test-runner
COPY bust_cache .
WORKDIR /opt/test-runner/testlib
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/CMakeLists.txt
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/TesterMain.f90
WORKDIR /opt/test-runner
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/config/CMakeLists.txt
Dockerfile-এর দ্বিতীয় অংশে একটি নতুন স্টেজ ডিফাইন করা হয় এবং COPY কমান্ড ব্যবহার করে "build" স্টেজ থেকে ডাউনলোড করা ফাইলগুলো নিজের স্টেজে কপি করা হয়:
FROM alpine:3.15
RUN apk add --no-cache coreutils jq gfortran libc-dev cmake make
WORKDIR /opt/test-runner
COPY --from=build /opt/test-runner/ .
COPY . .
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
Ruby টেস্ট রানারের প্রয়োজনীয় লাইব্রেরিগুলো (gem) ইনস্টল করার আগে git, openssh, build-base, gcc আর wget প্যাকেজগুলো ইনস্টল করা দরকার।
এর Dockerfile শুরু হয় একটি স্টেজ দিয়ে (যার নাম দেওয়া হয়েছে build), যা ওই প্যাকেজগুলো ইনস্টল করে (apk add-এর মাধ্যমে) এবং তারপর ডিপেন্ডেন্সিগুলো ইনস্টল করে (bundle install-এর মাধ্যমে):
FROM ruby:3.2.2-alpine3.18 AS build
RUN apk update && apk upgrade && \
apk add --no-cache git openssh build-base gcc wget git
COPY Gemfile Gemfile.lock .
RUN gem install bundler:2.4.18 && \
bundle config set without 'development test' && \
bundle install
এরপর এটি সেই স্টেজ ডিফাইন করে যেটি দিয়ে ফলাফল Docker ইমেজ তৈরি হবে।
এই স্টেজ আগের স্টেজ যে ডিপেন্ডেন্সিগুলো ইনস্টল করেছিল, সেগুলো ইনস্টল করে না; বরং এটি COPY কমান্ড ব্যবহার করে build স্টেজ থেকে ইনস্টল করা লাইব্রেরিগুলো নিজের স্টেজে কপি করে:
FROM ruby:3.2.2-alpine3.18
RUN apk add --no-cache bash
WORKDIR /opt/test-runner
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY . .
ENTRYPOINT [ "sh", "/opt/test-runner/bin/run.sh" ]
C# টেস্ট রানারের Dockerfile-এও প্রায় একই কাজ করা হয়েছে, তবে এখানে build স্টেজ এমন একটি তৈরি Docker ইমেজ ব্যবহার করতে পারে যেখানে লাইব্রেরি ইনস্টল করার জন্য দরকারি অতিরিক্ত প্যাকেজগুলো আগেই ইনস্টল করা আছে।
ইউনিট টেস্ট খুবই কাজের হতে পারে, তবে আমরা ইন্টিগ্রেশন টেস্ট লেখার উপর মনোযোগ দেওয়ার পরামর্শ দিই। এর মূল সুবিধা হলো, এটি প্রোডাকশনে টুলিং কীভাবে চলে তা আরও ভালোভাবে পরীক্ষা করে, ফলে আপনার টুলিংয়ের ইমপ্লিমেন্টেশনের উপর আস্থা বাড়ে।
প্রোডাকশন এনভায়রনমেন্ট সবচেয়ে ভালোভাবে নকল করতে ইন্টিগ্রেশন টেস্টগুলো উচিত প্রোডাকশন এনভায়রনমেন্টের মতোই টুলিং চালানো। এর মানে হলো Docker ইমেজ তৈরি করা এবং তারপর তৈরি ইমেজটি একটি সলিউশনে চালিয়ে তার আউটপুট যাচাই করা।
ইন্টিগ্রেশন টেস্টগুলো গোল্ডেন টেস্ট হিসেবে লেখা উচিত, অর্থাৎ এমন টেস্ট যেখানে প্রত্যাশিত আউটপুট একটি ফাইলে রাখা থাকে। ট্র্যাক-টুলিংয়ের ইন্টিগ্রেশন টেস্টের জন্য এটি একেবারে উপযুক্ত, কারণ টুলিংয়ের আউটপুটও ফাইল হয়।
কোনো সলিউশনে টেস্ট রানার চালালে তার আউটপুট হয় একটি results.json ফাইল।
এরপর আমরা এই ফাইলটি একটি "known good" (অর্থাৎ "expected") আউটপুট ফাইলের (যার নাম expected_results.json) সাথে মিলিয়ে দেখতে পারি, টেস্ট রানার ঠিকভাবে কাজ করছে কি না তা যাচাই করতে।
নিরাপত্তাই হলো আমাদের টুলিং চালাতে Docker কন্টেইনার ব্যবহার করার একটি প্রধান কারণ।
Docker Hub-এ অনেক Docker ইমেজ আছে, তবে অফিসিয়াল ইমেজগুলো ব্যবহার করার চেষ্টা করুন। এই ইমেজগুলো বাছাই করা এবং এগুলো অনিরাপদ হওয়ার সম্ভাবনা (অনেক) কম।
বিল্ড যেন স্থিতিশীল থাকে (অর্থাৎ হঠাৎ করে ভেঙে না পড়ে), তা নিশ্চিত করতে আপনার বেস ইমেজগুলো সবসময় নির্দিষ্ট ট্যাগে পিন করা উচিত। এর মানে এইটির বদলে:
FROM alpine:latest
আপনার ব্যবহার করা উচিত:
FROM alpine:3.20.2
পরেরটি ব্যবহার করলে বিল্ড সবসময় একই ভার্সন ব্যবহার করবে।
ডিফল্টভাবে অনেক ইমেজ এমন একটি ইউজার দিয়ে চলে যার রুট প্রিভিলেজ আছে। তাই নন-প্রিভিলেজড ইউজার হিসেবে চালানোর কথা ভাবা উচিত।
FROM alpine
RUN groupadd -r myuser && useradd -r -g myuser myuser
# RUN <COMMANDS THAT REQUIRE ROOT USER, E.G. INSTALLING PACKAGES>
USER myuser
সর্বশেষ ভার্সনগুলো ইনস্টল করা (প্রায়) সবসময়ই ভালো একটি অভ্যাস
RUN apt-get update && \
apt-get install curl
আমরা উৎসাহিত করি যেন Docker ফাইল রিড-অনলি ফাইলসিস্টেম ব্যবহার করে লেখা হয়। কেবল নিচের ডিরেক্টরিগুলোকেই লেখার উপযোগী ধরে নেওয়া উচিত:
/tmp ডিরেক্টরিআমাদের প্রোডাকশন এনভায়রনমেন্টে বর্তমানে রিড-অনলি ফাইলসিস্টেম প্রয়োগ করা হয় না, তবে ভবিষ্যতে হয়তো করা হতে পারে। এই কারণে নতুন টেস্ট রানার/অ্যানালাইজার/রেপ্রেজেন্টারের বেস টেমপ্লেট শুরু হয় রিড-অনলি ফাইলসিস্টেম নিয়েই। রিড-অনলি ফাইলে যদি কাজ না চলে, তাহলে (আপাতত) নির্দ্বিধায় লেখার উপযোগী ফাইলসিস্টেম ধরে নিতে পারেন।