Docker और Kubernetes: आधुनिक डेवलपर के लिए व्यावहारिक मार्गदर्शिका

हर डेवलपर आख़िरकार उसी दीवार से टकराता है। आप अपनी मशीन पर कोड लिखते हैं, वह बिल्कुल ठीक चलता है, आप उसे स्टेजिंग पर भेजते हैं और वह किसी ग़ायब सिस्टम निर्भरता या लाइब्रेरी के अलग संस्करण की रहस्यमय त्रुटि के साथ फट जाता है। «मेरी मशीन पर तो चलता है» वाली यह पुरानी समस्या दशकों तक सॉफ़्टवेयर विकास का पीछा करती रही, और Docker ने उसे सिर्फ़ हल ही नहीं किया — उसने हल इतना आसान कर दिया कि अब उसका उपयोग न करने का कोई बहाना नहीं बचा।

पर Docker केवल पैकेजिंग की समस्या हल करता है। ऐप्लिकेशन कंटेनर में आ जाने के बाद भी उसे प्रोडक्शन में चलाना होता है — संभवतः कई सर्वरों पर, लोड संतुलन के साथ, बिना ठहराव वाली तैनाती, स्वास्थ्य जाँच और स्वचालित पुनर्प्राप्ति के साथ। यहीं Kubernetes आता है। और यहीं अधिकांश डेवलपर जटिलता में खो जाते हैं, क्योंकि Kubernetes अमूर्तताओं की एक पूरी नई शब्दावली लाता है जिसे आत्मसात करने में समय लगता है।

यह मार्गदर्शिका शोर को काटती है। इसमें है कि कंटेनर भीतर से असल में क्या हैं, कुशल और सुरक्षित Dockerfile कैसे लिखें, असली ऐप्लिकेशन तैनात करने के लिए Kubernetes की कौन-सी अवधारणाएँ जाननी ज़रूरी हैं, स्थानीय विकास के लिए Docker Compose और Kubernetes के बीच क्या अदला-बदली है, और सबसे महत्वपूर्ण — हर औज़ार तक कब हाथ बढ़ाना चाहिए और कब उसे छोड़ देना चाहिए।

कंटेनर असल में क्या हैं

कंटेनर कोई हल्की वर्चुअल मशीन नहीं है। यही सबसे आम ग़लतफ़हमी है और इसी से ग़लत मानसिक मॉडल बनते हैं। वर्चुअल मशीन हाइपरवाइज़र के ऊपर पूरा अतिथि ऑपरेटिंग सिस्टम चलाती है — अपने कर्नेल, अपनी स्मृति और अपने डिवाइस ड्राइवरों के साथ। कंटेनर होस्ट का कर्नेल साझा करता है और एक अलग-थलग उपयोगकर्ता प्रक्रिया की तरह चलता है। अलगाव Linux कर्नेल की सुविधाओं से आता है: प्रक्रियाओं को अलग रखने के लिए नेमस्पेस, संसाधन सीमित करने के लिए cgroups, और छवि की कुशल परतों के लिए ओवरले फ़ाइल सिस्टम।

यह भेद इसलिए मायने रखता है क्योंकि यही उस व्यवहार की व्याख्या करता है जो आप देखेंगे। कंटेनर मिलीसेकंडों में शुरू होते हैं, क्योंकि कर्नेल बूट करने की ज़रूरत नहीं। वे कम स्मृति लेते हैं, क्योंकि दूसरा कर्नेल और अतिरिक्त सिस्टम प्रक्रियाएँ नहीं होतीं। पर इसी का अर्थ यह भी है कि Linux पर चल रहा कंटेनर होस्ट से अलग कर्नेल संस्करण नहीं चला सकता, और Windows कंटेनर को Windows होस्ट चाहिए। macOS पर Docker Desktop इसी वजह से Linux कंटेनरों को एक हल्की वर्चुअल मशीन के भीतर चलाता है।

छवि केवल-पठनीय टेम्प्लेट है: फ़ाइल सिस्टम का स्नैपशॉट और मेटाडेटा। कंटेनर उस छवि का चालू उदाहरण है, जिसके ऊपर लिखने योग्य एक परत होती है। आप छवि एक बार बनाकर उससे दर्जनों कंटेनर चला सकते हैं। Docker की दुनिया में यही आधारभूत इकाई है, और इसे साफ़ समझ लेने से बाक़ी सब आसान हो जाता है।

Dockerfile की बेहतरीन प्रथाएँ

Dockerfile छवि बनाने की विधि है। हर निर्देश एक नई परत बनाता है, और परतें कैश होती हैं। इसका अर्थ है कि निर्देशों का क्रम सीधे बिल्ड की गति, छवि के आकार और सुरक्षा पर असर डालता है। असली प्रोजेक्टों में ये सिद्धांत सबसे ज़्यादा मायने रखते हैं।

परतों को बदलाव की आवृत्ति के अनुसार क्रम दें

Docker हर परत को बनने के बाद कैश कर लेता है। अगर पिछली बार से परत बदली नहीं, तो Docker कैश वाली प्रति दोबारा इस्तेमाल कर लेता है। इसलिए जो निर्देश कम बदलते हैं वे ऊपर रहें और जो अक्सर बदलते हैं वे नीचे। सिस्टम निर्भरताएँ (apt-get, apk add) लगभग कभी नहीं बदलतीं। ऐप्लिकेशन की निर्भरताएँ (npm install, pip install) तब बदलती हैं जब लॉक-फ़ाइल बदलती है। स्रोत कोड हर कमिट पर बदलता है।

# Bad: source code before dependencies
FROM node:20-alpine
WORKDIR /app
COPY . .                 # busts the cache for everything below
RUN npm ci                # runs on every build, even if package.json is unchanged
EXPOSE 3000
CMD ["node", "dist/index.js"]

# Good: stable layers first
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci                # cached unless package.json changed
COPY . .                  # only a source edit busts this layer
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

फ़र्क़ नाटकीय है। ख़राब Dockerfile हर कमिट पर सारी निर्भरताएँ दोबारा बनाता है। अच्छा तभी बनाता है जब लॉक-फ़ाइल बदले, जो आमतौर पर हर कमिट पर नहीं, हर पुल-रिक्वेस्ट पर एक बार होता है। पाँच सौ निर्भरताओं वाले Node.js प्रोजेक्ट में यह प्रति बिल्ड लगभग दो मिनट बचाता है।

बहु-चरणीय बिल्ड

बहु-चरणीय बिल्ड एक ही Dockerfile से ऐप्लिकेशन बनाने और न्यूनतम रनटाइम छवि तैयार करने, दोनों की सुविधा देते हैं। बिल्ड चरण में कंपाइलर, विकास-निर्भरताएँ और औज़ार रहते हैं। रनटाइम चरण केवल संकलित परिणाम की प्रतिलिपि लेता है। इससे प्रोडक्शन छवियाँ छोटी रहती हैं और हमले का दायरा घटता है।

# Build stage
FROM node:20-alpine AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage - start from a clean, minimal base
FROM node:20-alpine AS runner
WORKDIR /app

# Only what is needed to run
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/package.json ./
COPY --from=builder /build/node_modules ./node_modules

EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

रनटाइम चरण में TypeScript कंपाइलर, स्रोत फ़ाइलें और विकास-निर्भरताएँ नहीं होतीं। सामान्य ऐप्लिकेशन के लिए इससे छवि 800 MB से घटकर 200 MB से नीचे आ जाती है। COPY --from=builder वाली रचना ही मूल विचार है: वह पिछले चरण से फ़ाइलें उठाती है, उसकी परतें साथ नहीं लाती।

root के बजाय सामान्य उपयोगकर्ता से चलाएँ

कंटेनर डिफ़ॉल्ट रूप से root के रूप में चलते हैं। यह सुरक्षा का ख़तरा है: अगर हमलावर आपके ऐप्लिकेशन की कमज़ोरी का लाभ उठाता है, तो उसे कंटेनर के भीतर root मिल जाता है। सुधार आपके Dockerfile में एक पंक्ति है जो ग़ैर-root उपयोगकर्ता पर चली जाती है। अधिकांश आधार छवियाँ इसी के लिए node या nobody उपयोगकर्ता के साथ आती हैं।

सुरक्षा के अलावा, बहु-चरणीय बिल्ड और परतों का सही क्रम CI/CD की गति भी सुधारते हैं। छवि बनाने में बचाया गया हर मिनट वह मिनट है जो आपके डेवलपर तैनाती की प्रतीक्षा में नहीं गँवाते। दस डेवलपरों की टीम जो दिन में पाँच बार तैनात करती है, प्रति बिल्ड दो मिनट की बचत से साल भर में साठ घंटे से अधिक वापस पाती है।

Kubernetes की बुनियाद

Kubernetes कंटेनरों का ऑर्केस्ट्रेटर है। यह मशीनों (नोड) का एक क्लस्टर लेता है, उन पर कंटेनर नियोजित करता है, उन्हें चलता रखता है, नेटवर्क संभालता है और आपके सिस्टम की वांछित अवस्था बताने के लिए एक घोषणात्मक API देता है। आप Kubernetes से कहते हैं कि आपको क्या चाहिए — API सर्वर की तीन प्रतियाँ, पोर्ट 8080 खुला, क्रमिक अद्यतन की रणनीति — और वह उसे साकार करता है।

सीखने की चढ़ाई असली है, क्योंकि Kubernetes अमूर्तताओं की कई परतें लाता है। जिन तीन से आपका सबसे ज़्यादा वास्ता पड़ेगा, वे हैं Pod, Deployment और Service।

Pod

Pod Kubernetes में तैनाती की सबसे छोटी इकाई है। यह एक या अधिक कंटेनरों का प्रतिनिधित्व करता है जो नेटवर्क नेमस्पेस और भंडारण वॉल्यूम साझा करते हैं। व्यवहार में अधिकांश Pod एक ही कंटेनर चलाते हैं। साइडकार पैटर्न (मुख्य कंटेनर के साथ लॉगिंग या प्रॉक्सी कंटेनर) बहु-कंटेनर Pod का उपयोग करते हैं, पर रोज़मर्रा की ऐप्लिकेशन तैनाती में प्रति Pod एक ही कंटेनर होता है।

आप शायद ही कभी सीधे Pod बनाते हैं। Pod क्षणभंगुर हैं — उन्हें किसी भी समय समाप्त और पुनर्नियोजित किया जा सकता है। अगर आपने हाथ से Pod बनाया और उसका नोड बैठ गया, तो Pod हमेशा के लिए चला गया। यहीं Deployment काम आते हैं।

Deployment

Deployment एक जैसे Pod के समूह (ReplicaSet) को संभालता है। यह क्रमिक अद्यतन, स्केलिंग, स्व-उपचार और रोलबैक का काम देखता है। बिना-अवस्था वाले ऐप्लिकेशन तैनात करने के लिए आप यही संसाधन उपयोग करेंगे।

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  labels:
    app: api-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          image: myregistry/api-server:v1.2.3
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: 250m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000

यह Deployment API सर्वर की तीन प्रतियाँ घोषित करता है। Kubernetes ध्यान रखेगा कि हमेशा तीन Pod चलते रहें। कोई Pod गिरे तो Kubernetes उसकी जगह नया बना देता है। क्रमिक अद्यतन के दौरान (छवि का टैग बदलने पर) Kubernetes Pod एक-एक करके बदलता है, ताकि कोई ठहराव न हो। livenessProbe बताता है कि Pod जीवित है; readinessProbe बताता है कि वह ट्रैफ़िक लेने को तैयार है।

Service

Pod के IP पते बदलते रहते हैं। हर बार नया Pod बनने पर उसे नया पता मिलता है। Service एक स्थिर नेटवर्क बिंदु देता है, जो अपने चयनकर्ता से मेल खाने वाले Pod पर लोड बाँटता है। आपके सिस्टम के बाक़ी हिस्से इसी के ज़रिये आपका ऐप्लिकेशन ढूँढते और उससे बात करते हैं।

apiVersion: v1
kind: Service
metadata:
  name: api-server
spec:
  selector:
    app: api-server
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP

यह Service क्लस्टर के भीतर एक स्थिर पते पर पोर्ट 80 को app: api-server लेबल वाले Pod के पोर्ट 3000 से जोड़ता है। क्लस्टर के भीतर दूसरी सेवाएँ इसे DNS नाम api-server से पुकार सकती हैं। बाहरी ट्रैफ़िक के लिए Service के ऊपर LoadBalancer या Ingress का उपयोग होता।

Kubernetes वह मंच नहीं है जिस पर आप तैनात करते हैं। यह वह मंच है जिसे आप तैनाती का विवरण देते हैं। आदेशात्मक और घोषणात्मक के बीच का अंतर वह सबसे महत्वपूर्ण मानसिक बदलाव है जो आप कर सकते हैं।

स्थानीय विकास: Docker Compose बनाम Kubernetes

टीमें सबसे बड़ी ग़लती यह मान लेने की करती हैं कि चूँकि प्रोडक्शन में Kubernetes है, इसलिए स्थानीय विकास में भी वही चाहिए। Docker Compose और Kubernetes अलग उद्देश्यों के लिए हैं, और स्थानीय काम के लिए ग़लत चुनाव अनावश्यक घर्षण पैदा करता है।

Docker Compose स्थानीय विकास के लिए बना है। यह एक ही मशीन पर चलता है, कंटेनर सेकंडों में खड़े करता है, और उसका सरल YAML प्रारूप सीधे उसी से मेल खाता है जो आपको चाहिए: वेब सर्वर, डेटाबेस, Redis का एक उदाहरण और शायद क़तार का एक कर्मी। आप सेवाएँ परिभाषित करते हैं और docker compose up सब कुछ चालू कर देता है — लॉग आपके टर्मिनल में, पोर्ट localhost पर, और हॉट रीलोड सीधे काम करता हुआ।

version: "3.8"
services:
  api:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

यह Compose फ़ाइल आपको हॉट रीलोड वाला काम करता विकास परिवेश, स्थायी भंडारण वाला स्थानीय डेटाबेस और सेवाओं के बीच सही नेटवर्क देती है। यह लगभग तीस पंक्तियों की YAML है और दस सेकंड से कम में खड़ी हो जाती है।

Minikube, Kind और k3s स्थानीय रूप से Kubernetes चला सकते हैं, पर वे भारी ख़र्च जोड़ते हैं। उन्हें अधिक स्मृति चाहिए, वे देर से शुरू होते हैं और ऐसी जटिलता लाते हैं (इनग्रेस कंट्रोलर, सर्विस मेश, स्टोरेज क्लास) जिसकी ज़रूरत तब नहीं होती जब आप एक ही फ़ीचर पर काम कर रहे हों। स्थानीय Kubernetes तब उपयोगी है जब आपको ख़ुद Kubernetes का विशिष्ट व्यवहार जाँचना हो — Pod निष्कासन की नीतियाँ, क्षैतिज स्वतः-स्केलिंग, या अपने संसाधन-परिभाषाएँ — पर रोज़मर्रा के विकास में यह Compose का विकल्प नहीं है।

  • स्थानीय विकास के लिए Docker Compose लें। वह तेज़, सरल है और सीधे उन्हीं कंटेनरों से मेल खाता है जो आप चलाते हैं।
  • Kubernetes (Minikube या Kind के ज़रिये) एकीकरण-परीक्षण के लिए लें, जब प्रोडक्शन ConfigMap, Secret या अपने कंट्रोलर जैसी Kubernetes सुविधाओं पर टिका हो।
  • दूरस्थ विकास क्लस्टर केवल तभी लें जब GPU तक पहुँच, विशेष हार्डवेयर या प्रोडक्शन की हूबहू नक़ल वाला साझा स्टेजिंग चाहिए।
  • सिर्फ़ इसलिए दो Kubernetes क्लस्टर स्थानीय रूप से न चलाएँ कि आपके पास दो परिवेश हैं। Compose इसे एक --profile फ़्लैग से संभाल लेता है।
  • अगर आपकी टीम ऐप्लिकेशन कोड लिखने से ज़्यादा समय Kubernetes की कॉन्फ़िगरेशन सुलझाने में लगा रही है, तो आप बहुत दूर निकल आए हैं। Compose पर लौटें और जटिलता तभी जोड़ें जब उसके बिना होने वाला दर्द उसे संभालने के दर्द से बड़ा हो जाए।

आम फंदे और उनसे बचने के तरीक़े

अवधारणाएँ समझ लेने के बाद भी कुछ ग़लतियाँ टीमों में बार-बार दोहराई जाती हैं। ये वे हैं जिन्हें याद रखकर आप दो-दिन लंबी डिबगिंग से बच सकते हैं।

छवि टैग की अराजकता सबसे आम प्रोडक्शन समस्या है। Deployment में latest टैग का अर्थ है कि आप नहीं जानते कि किस नोड पर कौन-सा संस्करण चल रहा है। Kubernetes छवि तभी खींचता है जब वह नोड पर न हो, इसलिए एक नोड का latest दूसरे नोड के latest से अलग संस्करण हो सकता है। हमेशा सिमेंटिक संस्करण या कमिट का SHA उपयोग करें। इससे भी बेहतर — छवि का पूर्ण डाइजेस्ट; केवल वही निश्चित रूप से अपरिवर्तनीय है।

संसाधन अनुरोध और सीमाएँ अक्सर छोड़ दी जाती हैं या अंदाज़े से रखी जाती हैं। अनुरोध न हों तो Kubernetes आपके Pod को समझदारी से नियोजित नहीं कर सकता और नोड पर बोझ बढ़ जाता है। सीमाएँ न हों तो एक Pod की स्मृति-रिसाव उसी नोड के दूसरे Pod गिरा सकती है। दोनों निर्धारित करें, और वास्तविक उपयोग के आधार पर उन्हें ठीक करने के लिए Vertical Pod Autoscaler को सुझाव-मोड में चलाएँ।

ConfigMap और Secret परिवेश-चर या फ़ाइलों के रूप में जोड़े जाते हैं। परिवेश-चर सुविधाजनक हैं, पर उनमें कोई भी बदलाव Pod के पुनरारंभ के बाद ही लागू होता है। फ़ाइल-आधारित माउंट बिना पुनरारंभ के भी अद्यतन हो सकते हैं (Pod फ़ाइल पढ़ते समय नई सामग्री देखता है), पर कई ऐप्लिकेशन शुरू होते ही कॉन्फ़िगरेशन कैश कर लेते हैं। जानें कि आपका ऐप्लिकेशन कौन-सा तरीक़ा अपनाता है और उसी हिसाब से कॉन्फ़िगरेशन की रणनीति बनाएँ।

Kubernetes में स्थायी वॉल्यूम कोई जादू नहीं हैं। PersistentVolumeClaim भंडारण माँगता है, पर नीचे का स्टोरेज क्लास आपके क्लाउड प्रदाता के अनुसार सेट होना चाहिए। डिफ़ॉल्ट स्टोरेज क्लास नेटवर्क भंडारण उपयोग कर सकती है, जिसका प्रदर्शन स्थानीय SSD से अलग होता है। अगर आपके डेटाबेस का प्रदर्शन मायने रखता है, प्रोडक्शन में जाने से पहले अपनी स्टोरेज क्लास जाँच लें।

Kubernetes में लॉगिंग और डिबगिंग एक अकेले सर्वर की तुलना में कठिन है। Pod क्षणभंगुर हैं, इसलिए Pod हटते ही लॉग ग़ायब हो जाते हैं। जीवंत धारा के लिए kubectl logs --tail=50 -f pod-name काम आता है, पर प्रोडक्शन डिबगिंग के लिए केंद्रीकृत समाधान चाहिए (Loki, Elasticsearch या क्लाउड लॉगिंग सेवा)। इसी तरह kubectl exec -it pod-name -- sh से चालू कंटेनर के भीतर झाँका जा सकता है, पर याद रखें कि उसके भीतर किए गए बदलाव पुनरारंभ पर मिट जाते हैं।

क्या आपको सचमुच Kubernetes चाहिए?

यह सवाल कोई पूछना नहीं चाहता, क्योंकि Kubernetes बायोडेटा में अच्छा दिखता है और परिचालन परिपक्वता का संकेत देता है। पर Kubernetes एक ठोस समस्या का हल है: कई कंटेनरीकृत सेवाओं को कई मशीनों पर, स्वचालित पुनर्प्राप्ति, स्केलिंग और क्रमिक तैनाती के साथ चलाना। अगर आपके पास एक या दो सेवाएँ एक ही सर्वर पर हैं, तो Kubernetes ज़रूरत से ज़्यादा है।

निर्णय का सीधा ढाँचा यह है। अगर आप एक ऐसा ऐप्लिकेशन तैनात कर रहे हैं जो प्रति सेकंड दस हज़ार से कम अनुरोध संभालता है, तो प्रोडक्शन में Docker Compose वाला एक सर्वर (हाँ, कई कार्यभारों के लिए Compose वहाँ बढ़िया चलता है) आपका काम अच्छी तरह चला देगा। Caddy या Nginx जैसा रिवर्स प्रॉक्सी जोड़ें, स्वचालित बैकअप सेट करें — और आपके पास ऐसा प्रोडक्शन सिस्टम होगा जिसे एक अकेला डेवलपर पूरी तरह समझ और संभाल सकता है।

Kubernetes पर तब जाएँ जब कई सेवाएँ हों जिन्हें स्वतंत्र रूप से तैनात करना हो; जब सेवा-दर-सेवा स्केलिंग चाहिए (API को दस प्रतियाँ चाहिए, कर्मी को दो); जब बिना ठहराव वाली तैनाती रोज़मर्रा की बात हो; या जब टीम में कम से कम एक व्यक्ति हो जिसकी मुख्य ज़िम्मेदारी अवसंरचना है। इन शर्तों के पूरा होने से पहले Kubernetes की परिचालन लागत — क्लस्टर प्रबंधन में और डेवलपर के दिमाग़ पर — शुद्ध घाटा है।

कई टीमों के लिए बीच का रास्ता फ़ायदेमंद है। स्थानीय विकास के लिए Docker Compose और प्रोडक्शन के लिए AWS App Runner, Google Cloud Run या Fly.io जैसा प्रबंधित कंटेनर मंच। ये मंच कंटेनर तैनाती, स्वचालित HTTPS और स्केलिंग देते हैं, बिना इसके कि आपको Kubernetes का नियंत्रण-तल संभालना पड़े। कंटेनरीकरण का अधिकांश लाभ आपको Kubernetes की सीखने की चढ़ाई के बिना मिल जाता है।

सबसे अच्छी अवसंरचना रणनीति वही है जो आपकी टीम को फ़ीचर भेजने देती है। Docker और Kubernetes औज़ार हैं, पहचान नहीं। जब वे मदद करें तब उन्हें लें, और जब न करें तब छोड़ दें।

See what your own repository can account for.

Thirty minutes on a repository you choose, including the part the record cannot attribute.