सर्वोत्तम तरीके


आधिकारिक सर्वोत्तम प्रथाओं का पालन कीजिए

आधिकारिक Dockerfile सर्वोत्तम प्रथाओं में बहुत उपयोगी जानकारी है, जो बताती है कि आपके Dockerfile को बेहतर कैसे बनाया जाए।

प्रदर्शन

आपको मुख्य रूप से प्रदर्शन पर ध्यान देना चाहिए (खासकर टेस्ट रनर के लिए)। इससे यह सुनिश्चित होगा कि आपका टूल जितनी तेज़ी से संभव हो चले और टाइम-आउट न हो।

मापिए

निष्पादन का समय बार-बार मापना टूल के प्रदर्शन का अंदाज़ा लगाने का बढ़िया तरीका है। किसी बदलाव के बाद और पहले, दोनों समय निष्पादन का समय मापने की आदत डालिए। भले ही आपको "निश्चित" लगे कि कोई बदलाव प्रदर्शन को बेहतर बनाएगा, तब भी आपको निष्पादन का समय मापना चाहिए।

स्क्रिप्ट

जहाँ संभव हो, प्रदर्शन को अपने आप मापने के लिए स्क्रिप्ट बनाइए (जिसे बेंचमार्किंग भी कहते हैं)। एक बहुत उपयोगी कमांड-लाइन टूल hyperfine है, लेकिन जो आपके टूल के लिए सबसे उपयुक्त लगे, वह बेझिझक इस्तेमाल कीजिए।

नए ट्रैक टूल रेपो में ये दो स्क्रिप्ट मिलेंगी:

  1. ./bin/benchmark.sh: ट्रैक टूल के कोड को बेंचमार्क करती है (सोर्स कोड)
  2. ./bin/benchmark-in-docker.sh: ट्रैक टूल की Docker इमेज को बेंचमार्क करती है (सोर्स कोड)
Note

अगर आप किसी ऐसे ट्रैक टूल रेपो पर काम कर रहे हैं जिसमें ये फाइलें नहीं हैं, तो ऊपर दिए गए सोर्स लिंक से इन्हें अपने रेपो में बेझिझक कॉपी कर लीजिए।

Caution

बेंचमार्किंग स्क्रिप्ट टूल के प्रदर्शन का अनुमान लगाने में मदद कर सकती हैं। लेकिन ध्यान रखिए कि Exercism के प्रोडक्शन सर्वर पर प्रदर्शन अक्सर इससे कम होता है।

अलग-अलग बेस इमेज के साथ प्रयोग कीजिए

अलग-अलग बेस इमेज (जैसे Ubuntu की जगह Alpine) आज़माकर देखिए कि कोई एक दूसरे से (काफी) बेहतर प्रदर्शन देती है या नहीं। अगर प्रदर्शन लगभग बराबर हो, तो सबसे छोटी इमेज चुनिए।

इंटरनल नेटवर्क आज़माइए

देखिए कि none की जगह internal नेटवर्क इस्तेमाल करने से प्रदर्शन बेहतर होता है या नहीं। ज़्यादा जानकारी के लिए नेटवर्क डॉक्स देखिए।

रन-टाइम कमांड की जगह बिल्ड-टाइम कमांड को प्राथमिकता दीजिए

ट्रैक टूल एक बार चलने वाला और अल्पकालिक Docker कंटेनर चलाता है, जो ये चरण पूरे करता है।

  1. एक Docker कंटेनर बनाया जाता है।
  2. Docker कंटेनर को सही आर्गुमेंट के साथ चलाया जाता है।
  3. 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 इमेज से अलग आर्किटेक्चर के लिए हैं
  • डॉक्युमेंटेशन

पैकेज मैनेजर की फाइलें हटाइए

ज़्यादातर Docker इमेज को अतिरिक्त पैकेज इंस्टॉल करने पड़ते हैं, जो आम तौर पर पैकेज मैनेजर से किया जाता है। ये पैकेज बिल्ड टाइम पर ही इंस्टॉल करने होंगे (क्योंकि रन टाइम पर इंटरनेट कनेक्शन उपलब्ध नहीं होता)। इसलिए अतिरिक्त पैकेज इंस्टॉल करने के बाद पैकेज मैनेजर की कैशिंग या हिसाब रखने वाली फाइलें हटा देनी चाहिए।

apk

जो डिस्ट्रिब्यूशन apk पैकेज मैनेजर इस्तेमाल करते हैं (जैसे Alpine), उन्हें पैकेज इंस्टॉल करने के लिए apk add इस्तेमाल करते समय --no-cache फ्लैग लगाना चाहिए:

RUN apk add --no-cache curl
apt-get/apt

जो डिस्ट्रिब्यूशन 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 की सामान्य संरचना कुछ इस तरह होती है:

  1. एक नई स्टेज बनाइए (हम इसे "build" स्टेज कहेंगे)। इस स्टेज का इस्तेमाल सिर्फ बिल्ड टाइम पर होगा।
  2. ज़रूरी अतिरिक्त पैकेज इंस्टॉल कीजिए ("build" स्टेज में)।
  3. वे कमांड चलाइए जिन्हें अतिरिक्त पैकेज चाहिए ("build" स्टेज के अंदर)।
  4. एक नई स्टेज बनाइए (हम इसे "runtime" स्टेज कहेंगे)। यही स्टेज आगे बनने वाली Docker इमेज बनाएगी और रन टाइम पर चलेगी।
  5. चरण 3 में (यानी "build" स्टेज में) चली कमांड के नतीजे इस स्टेज ("runtime" स्टेज) में कॉपी कीजिए।

इस व्यवस्था में अतिरिक्त पैकेज सिर्फ "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" ]
Note

C# टेस्ट रनर का Dockerfile भी कुछ ऐसा ही करता है, बस यहाँ build स्टेज एक ऐसी मौजूदा Docker इमेज इस्तेमाल कर सकती है जिसमें लाइब्रेरी इंस्टॉल करने के लिए ज़रूरी अतिरिक्त पैकेज पहले से इंस्टॉल हों।

टेस्टिंग

इंटीग्रेशन टेस्ट इस्तेमाल कीजिए

यूनिट टेस्ट बहुत उपयोगी हो सकते हैं, लेकिन हमारा सुझाव है कि आप इंटीग्रेशन टेस्ट लिखने पर ध्यान दें। इनका सबसे बड़ा फायदा यह है कि ये बेहतर ढंग से जाँचते हैं कि प्रोडक्शन में टूल कैसे चलता है, और इससे आपके टूल के कार्यान्वयन पर भरोसा बढ़ने में मदद मिलती है।

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 डायरेक्टरी
Caution

हमारा प्रोडक्शन वातावरण फिलहाल रीड-ओनली फाइलसिस्टम लागू नहीं करता, लेकिन भविष्य में कर सकता है। इसीलिए नए टेस्ट रनर/एनालाइज़र/रिप्रेज़ेंटर का बेस टेम्पलेट रीड-ओनली फाइलसिस्टम के साथ शुरू होता है। अगर किसी रीड-ओनली फाइल पर चीज़ें काम न करें, तो (फिलहाल) बेझिझक लिखने लायक फाइलसिस्टम मान लीजिए।