आधिकारिक Dockerfile सर्वोत्तम प्रथाओं में बहुत उपयोगी जानकारी है, जो बताती है कि आपके Dockerfile को बेहतर कैसे बनाया जाए।
आपको मुख्य रूप से प्रदर्शन पर ध्यान देना चाहिए (खासकर टेस्ट रनर के लिए)। इससे यह सुनिश्चित होगा कि आपका टूल जितनी तेज़ी से संभव हो चले और टाइम-आउट न हो।
निष्पादन का समय बार-बार मापना टूल के प्रदर्शन का अंदाज़ा लगाने का बढ़िया तरीका है। किसी बदलाव के बाद और पहले, दोनों समय निष्पादन का समय मापने की आदत डालिए। भले ही आपको "निश्चित" लगे कि कोई बदलाव प्रदर्शन को बेहतर बनाएगा, तब भी आपको निष्पादन का समय मापना चाहिए।
जहाँ संभव हो, प्रदर्शन को अपने आप मापने के लिए स्क्रिप्ट बनाइए (जिसे बेंचमार्किंग भी कहते हैं)। एक बहुत उपयोगी कमांड-लाइन टूल hyperfine है, लेकिन जो आपके टूल के लिए सबसे उपयुक्त लगे, वह बेझिझक इस्तेमाल कीजिए।
नए ट्रैक टूल रेपो में ये दो स्क्रिप्ट मिलेंगी:
./bin/benchmark.sh: ट्रैक टूल के कोड को बेंचमार्क करती है (सोर्स कोड)./bin/benchmark-in-docker.sh: ट्रैक टूल की Docker इमेज को बेंचमार्क करती है (सोर्स कोड)अगर आप किसी ऐसे ट्रैक टूल रेपो पर काम कर रहे हैं जिसमें ये फाइलें नहीं हैं, तो ऊपर दिए गए सोर्स लिंक से इन्हें अपने रेपो में बेझिझक कॉपी कर लीजिए।
बेंचमार्किंग स्क्रिप्ट टूल के प्रदर्शन का अनुमान लगाने में मदद कर सकती हैं। लेकिन ध्यान रखिए कि Exercism के प्रोडक्शन सर्वर पर प्रदर्शन अक्सर इससे कम होता है।
अलग-अलग बेस इमेज (जैसे Ubuntu की जगह Alpine) आज़माकर देखिए कि कोई एक दूसरे से (काफी) बेहतर प्रदर्शन देती है या नहीं। अगर प्रदर्शन लगभग बराबर हो, तो सबसे छोटी इमेज चुनिए।
देखिए कि none की जगह internal नेटवर्क इस्तेमाल करने से प्रदर्शन बेहतर होता है या नहीं।
ज़्यादा जानकारी के लिए नेटवर्क डॉक्स देखिए।
ट्रैक टूल एक बार चलने वाला और अल्पकालिक Docker कंटेनर चलाता है, जो ये चरण पूरे करता है।
इसलिए चरण 2 में चलने वाला कोड टूल के हर एक रन में चलता है। इसी वजह से चरण 2 में चलने वाले कोड की मात्रा घटाना प्रदर्शन सुधारने का बढ़िया तरीका है। ऐसा करने का एक तरीका है कोड को रन-टाइम से बिल्ड-टाइम पर ले जाना। रन-टाइम कोड हर एक टूल रन में चलता है, जबकि बिल्ड-टाइम कोड सिर्फ एक बार चलता है (जब 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 पर आधारित हैं।
कुछ इमेज के खास "स्लिम" वेरिएंट होते हैं, जिनमें से कुछ फीचर हटा दिए जाते हैं और इससे इमेज का आकार छोटा हो जाता है।
उदाहरण के लिए, node:20.16.0-slim इमेज node:20.16.0 इमेज से पाँच गुना छोटी है:
REPOSITORY TAG SIZE
node 20.16.0 1.09GB
node 20.16.0-slim 219MB
"स्लिम" वेरिएंट छोटे होने की वजह यह है कि उनमें फीचर कम होते हैं। हो सकता है आपकी इमेज को अतिरिक्त फीचर की ज़रूरत न हो, और अगर नहीं है, तो "स्लिम" वेरिएंट इस्तेमाल करने के बारे में सोचिए।
अपनी इमेज का आकार घटाने का एक सीधा, लेकिन बढ़िया तरीका है जो चीज़ें आपको नहीं चाहिए, उन्हें हटा देना। इनमें ऐसी चीज़ें शामिल हो सकती हैं:
ज़्यादातर 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 टेस्ट रनर को अपनी ज़रूरी लाइब्रेरी (gems) इंस्टॉल करने से पहले 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 फाइल होता है।
फिर हम इस फाइल की तुलना "ज्ञात सही" (यानी "अपेक्षित") आउटपुट फाइल (जिसका नाम 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 डायरेक्टरीहमारा प्रोडक्शन वातावरण फिलहाल रीड-ओनली फाइलसिस्टम लागू नहीं करता, लेकिन भविष्य में कर सकता है। इसीलिए नए टेस्ट रनर/एनालाइज़र/रिप्रेज़ेंटर का बेस टेम्पलेट रीड-ओनली फाइलसिस्टम के साथ शुरू होता है। अगर किसी रीड-ओनली फाइल पर चीज़ें काम न करें, तो (फिलहाल) बेझिझक लिखने लायक फाइलसिस्टम मान लीजिए।