SSD Nodes Learn
गाइड Matt Connorलेखक: Matt Connor · अपडेट किया गया: 2026-07-24

Ubuntu 24.04 पर Docker Compose कैसे इंस्टॉल करें

Ubuntu 24.04 पर Docker Engine और Compose v2 सेटअप करें। ufw port-publishing की समस्या और Docker volumes के बैकअप लेने का सही तरीका यहाँ सीखें।

आप क्या बना रहे हैं

इस साइट पर लगभग हर चीज़ के लिए Docker Compose आधार (substrate) का काम करता है। Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — इन सभी गाइड्स की शुरुआत "write this compose file" से होती है, और यह पेज समझाता है कि उस फ़ाइल का वास्तविक अर्थ क्या है। आप Ubuntu 24.04 पर Docker के अपने apt repository से Docker Engine और Compose v2 plugin इंस्टॉल करेंगे। इसके बाद आप एक वास्तविक two-service stack सेटअप करेंगे — Miniflux (एक छोटा RSS reader) और PostgreSQL। यह जोड़ा उन सभी patterns का अभ्यास कराता है जो बड़े apps में उपयोग किए जाते हैं: pinned images, healthcheck के साथ एक database, एक named volume, .env फ़ाइल में secrets, और केवल localhost के लिए publish किया गया एक port।

इंस्टॉलेशन में पाँच मिनट लगते हैं। इस गाइड का बाकी हिस्सा उन समस्याओं को कवर करता है जो बाद में आती हैं: docker group का root के समान होना, published ports का ufw rules को बायपास करना, और docker compose down का वह flag जो बिना किसी confirmation prompt के आपका database डिलीट कर देता है।

Prerequisites: एक नया Ubuntu 24.04 KVM VPS, sudo अधिकार वाला एक user, और 1 GB या उससे अधिक RAM। यदि Docker पहले से इंस्टॉल है तो भी चलेगा — पहला section बताता है कि क्या हटाना है।

Ubuntu के बजाय Docker के repo से install करें

पहला command चलाने से पहले इन दो गलतियों से बचें। Ubuntu का अपना docker.io package काम तो करता है, लेकिन यह Docker के releases से पीछे रहता है और इसमें वह plugin layout नहीं होता जिसकी अन्य tools को आवश्यकता होती है। और standalone docker-compose binary — जिसमें hyphen है — वह Compose v1 है: यह Python पर आधारित है और 2023 से end-of-life हो चुका है; पुराने tutorials के fail होने का यही कारण है। आज का Compose docker compose है (बिना hyphen के), जो एक CLI plugin है, और इसे engine वाले ही repository से install किया जाता है।

यदि system पर इनमें से कुछ भी पहले से मौजूद है, तो उसे पहले हटा दें — इसमें docker-compose-v2 (Ubuntu का plugin package) भी शामिल है, ताकि सब कुछ एक ही repository से आए:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

एक नए VPS पर Package 'docker.io' is not installed, so not removed सामान्य output है। इसके बाद Docker का repository जोड़ें और install करें:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

इन तीनों layers को verify करें:

docker --version
docker compose version
sudo docker run --rm hello-world

शुरुआती दो version strings print करेंगे — Docker Compose version v2.x.x यह confirm करता है कि आपके पास plugin है, न कि पुराना v1 binary। hello-world run का अंत Hello from Docker! के साथ होना चाहिए। यह package boot के समय service को enable कर देता है; systemctl is-enabled docker enabled print करेगा।

docker group root है — सावधानी से निर्णय लें

अभी हर docker command के लिए sudo की आवश्यकता होती है, क्योंकि /var/run/docker.sock पर daemon socket का owner root और docker group है। बिना membership के आपको सबसे सामान्य Docker error मिलेगा:

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

Standard समाधान:

sudo usermod -aG docker $USER

Group membership login के समय लागू होती है, इसलिए आपकी current shell में error बना रहेगा। इस session के लिए newgrp docker चलाएँ, या log out करके पुनः log in करें; इसके बाद id आपके groups में docker को list करेगा।

अब स्पष्ट रूप से सच्चाई: host पर docker group की membership root है। यह "root-ish" या "elevated" नहीं है — यह root है। उस group का कोई भी सदस्य बिना password के docker run --rm -it -v /:/host alpine chroot /host चला सकता है और पूरे filesystem का owner बन सकता है। यह group सुविधा के लिए है, containment के लिए नहीं।

Docker का rootless mode वास्तविक विकल्प है — इसमें daemon स्वयं आपके unprivileged user के रूप में चलता है। इसके नुकसान ये हैं: 1024 से नीचे के ports के लिए extra setup चाहिए, networking में userspace shim के कारण overhead बढ़ता है, और कुछ images बिना real root के सही से काम नहीं करतीं। एक single-admin VPS पर जहाँ एकमात्र login के पास पहले से sudo होता है, वहाँ group से व्यवहार में कोई बदलाव नहीं आता। यहाँ के सभी guides यही मानकर चलते हैं — बस इसे कभी भी sudo से कम न समझें।

Compose file की संरचना

प्रत्येक stack के लिए अपना अलग directory रखें — directory का नाम project name बन जाता है, जो containers, networks, और volumes के नाम के आगे prefix के रूप में जुड़ता है:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

compose.yml बनाएँ (यह आधुनिक नाम है; docker-compose.yml अभी भी काम करता है)। पुराने version: key का उपयोग न करें — यह obsolete है और Compose इसके होने पर warning देता है।

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

ऊपर दी गई हर line एक निर्णय है। इन्हें एक-एक करके लागू करें।

Image versions को pin करें — :latest और pull का मतलब अनचाहा upgrade है

postgres:latest के बजाय postgres:16-alpine का उपयोग करें। Tag permanent नहीं होता: हर बार pull करने पर :latest उस version पर re-resolve हो जाता है जो maintainer ने हाल ही में push किया है। इसे upgrade की आदत — docker compose pull && docker compose up -d — के साथ जोड़ें, तो :latest का मतलब है कि major-version jumps तब होंगे जब upstream उन्हें release करेगा, न कि तब जब आप चाहें। PostgreSQL के मामले में यह केवल एक संभावना नहीं है: 16 से 17 तक का अचानक jump container को crash-loop में डाल देगा क्योंकि incompatible data directory के कारण container crash हो जाएगा; Postgres major upgrades के लिए dump और restore की आवश्यकता होती है, केवल restart की नहीं।

कम से कम major version को pin करें (postgres:16-alpine, 16.x patch releases का पालन करता है), और applications को miniflux/miniflux:2.2.9 जैसे सटीक release पर pin करें — file लिखते समय project के releases page से current version चेक करें। इसके बाद upgrade करना केवल एक intentional line edit बन जाता है, जो git diff में दिखाई देता है।

127.0.0.1 पर publish करें, क्योंकि Docker ufw को bypass कर देता है

"127.0.0.1:8080:8080" — host address, host port, container port। अधिकांश tutorials में "8080:8080" लिखा जाता है, जो 0.0.0.0:8080:8080 का shorthand है: यानी सभी interfaces पर listen करना, जिसमें public interface भी शामिल है।

यहाँ एक trap है, जो लगभग सभी को एक बार परेशान करता है। Docker एक port को publish करने के लिए DNAT rule लिखता है जो filtering से पहले packet के destination को container के internal IP में rewrite कर देता है, इसलिए packet FORWARD path लेता है और INPUT तक कभी नहीं पहुँचता, जहाँ आपके ufw rules होते हैं। sudo ufw deny 8080 success दिखाता है, ufw status port denied दिखाता है, लेकिन service अभी भी पूरी internet को जवाब देती है। आपका firewall खराब नहीं है; इसे design के अनुसार bypass किया जा रहा है। Docker ufw को bypass क्यों करता है, और container traffic को सही ढंग से filter कैसे करें इस mechanism और ports के लिए DOCKER-USER fix के बारे में विस्तार से बताता है।

इस समस्या को खत्म करने वाली आदत: जब तक आपके पास कोई विशेष कारण न हो, published ports को 127.0.0.1 से bind करें, और जो कुछ भी public होना चाहिए उसके लिए एक reverse proxy का उपयोग करें। Traefik reverse proxy guide इसी के अगले step के रूप में Traefik reverse proxy guide बनाता है — एक container जो ports 80 और 443 को control करता है और hostname के आधार पर TLS के साथ बाकी सब तक route करता है। (क्या आप पुराने Traefik v2 setup से आ रहे हैं? Traefik v2 to v3 migration guide renames और rule changes को कवर करता है।)

Stack शुरू करने के बाद bind को verify करें: sudo ss -tlnp | grep 8080 में 127.0.0.1:8080 दिखना चाहिए, न कि 0.0.0.0:8080 या *:8080

Named volumes बनाम bind mounts

db-data:/var/lib/postgresql/data एक named volume है: Docker /var/lib/docker/volumes/ के अंतर्गत एक directory बनाता है और उसे container में mount करता है। इसका विकल्प bind mount, ./data:/var/lib/postgresql/data है, जो host पर आपके द्वारा चुने गए path को map करता है।

व्यावहारिक उपयोग का अंतर: named volumes केवल उन containers के लिए जिन्हें data की आवश्यकता है — सबसे पहले databases, क्योंकि Docker volume को उसी ownership के साथ initialize करता है जिसकी image अपेक्षा करती है और file permissions सही काम करती हैं। bind mounts उन files के लिए जिन्हें आप host से touch करते हैं — वे config files जिन्हें आप text editor से edit करते हैं, एक media library जिसे आप rsync करते हैं, या कोई भी ऐसी चीज़ जिसका path आप स्पष्ट रखना चाहते हैं। Bind-mount की क्लासिक विफलता ownership है: container UID 999 के रूप में चलता है, आपका host directory UID 1000 का मालिक है, और app startup पर logs में permission denied के साथ crash हो जाता है। Named volumes इस तरह के bugs को लगभग खत्म कर देते हैं, लेकिन इसके बदले में data Docker-managed path पर रहता है — जिसका विवरण नीचे दिया गया है।

environment और .env — secrets को git से दूर रखें

${POSTGRES_PASSWORD} आपके shell से नहीं पढ़ा जाता; Compose इसे compose.yml के बगल में रखी .env नामक file से interpolate करता है। इसे ऐसे बनाएँ:

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

openssl rand -hex 24 के साथ वास्तविक values generate करें। जानबूझकर Hex का उपयोग करें, base64 का नहीं: यह password DATABASE_URL connection string के अंदर जाता है, और base64 द्वारा produce किए गए /, +, और = characters URL parsing को तोड़ देते हैं — यह विफलता syntax error के बजाय authentication error के रूप में सामने आती है और काफी समय बर्बाद करती है। .gitignore line को पहले commit करने से पहले डाल दें: compose file को publish और version करना सुरक्षित है, लेकिन .env file कभी सुरक्षित नहीं होती, और यदि कोई secret git history का हिस्सा बन गया है, तो उसे rotate करना ही पड़ता है। यदि आप किसी variable के गायब होने पर stack शुरू करते हैं, तो Compose warning देता है और empty string के साथ जारी रखता है — जो Postgres password के मामले में एक broken deployment का कारण बनता है:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config पूरी तरह से interpolated file प्रिंट करता है — यह check करने का सबसे तेज़ तरीका है कि containers को वास्तव में क्या प्राप्त होगा; याद रखें कि इसके output में आपके secrets शामिल होते हैं।

depends_on किसी का इंतज़ार नहीं करता — जब तक आप healthcheck न जोड़ें

एक साधारण depends_on: [db] केवल start order को control करता है: Compose पहले Postgres को launch करता है और एक पल बाद app को, जबकि Postgres अभी भी connections स्वीकार करने से कुछ सेकंड दूर होता है। App database तक पहुँचता है, fail होता है, और इस आधार पर crash या retry करता है कि उसे कितनी अच्छी तरह लिखा गया है।

Reliable version वह है जो ऊपर दी गई file में उपयोग किया गया है: db service एक healthcheck define करती है (Postgres विशेष रूप से इसी के लिए pg_isready प्रदान करता है), और app condition: service_healthy के साथ depends_on declare करता है। Compose database को start करता है, हर 10 सेकंड में check को poll करता है, और check pass होने के बाद ही Miniflux को start करता है। यदि database कभी healthy नहीं होता है — गलत password, corrupt volume — तो app कभी start नहीं होता और Compose आपको बताता है कि कौन सा dependency fail हुआ:

dependency failed to start: container miniflux-db-1 is unhealthy

वह message आपको docker compose logs db की ओर इशारा करता है, जहाँ वास्तविक error मौजूद है।

restart: unless-stopped

दोनों services पर restart: unless-stopped का मतलब है कि containers crash होने के बाद और VPS reboot होने के बाद वापस आ जाते हैं, लेकिन यदि आपने जानबूझकर docker compose stop चलाया है, तो वे बंद ही रहेंगे। विकल्प always manual stop के बाद भी containers को पुनर्जीवित (resurrect) कर देता है — जो शायद आपकी मंशा नहीं होती। restart policy के बिना, सुबह 4 बजे kernel-update reboot होने पर आपकी services चुपचाप बंद हो जाती हैं जब तक कि आप उन्हें नोटिस न करें।

Daily commands

Rozana ke saare kaam paanch commands se hote hain, jinhe project directory se chalaya jata hai.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d ko baar-baar chalana safe hai — yeh file ko reality se compare karta hai aur sirf un services ko touch karta hai jinka config ya image badla gaya ho. Upgrade pair un tags ko fetch karta hai jinhe aapne pin kiya hai: postgres:16-alpine ke niche patch releases milenge, lekin exact pin ke liye tab tak kuch nahi milega jab tak aap use edit nahi karte — yahi iska maqsad hai. Upgrades ke baad purani images jama ho jati hain; disk space bachane ke liye docker image prune -f ka upyog karein.

Ab destructive command ke baare mein: docker compose down safe hai — containers aur network ko discard kiya ja sakta hai, aur aapka data volume mein hota hai. docker compose down -v named volumes ko bhi delete kar deta hai. Isse aapka database turant delete ho jayega, bina kisi confirmation prompt ya undo option ke. -v flag experiments khatam karne ke liye hai; real data wale stack par iska upyog bilkul rm -rf ki tarah karein. /var/lib/docker/volumes/ ke niche koi trash can nahi hota.

Running container ke andar shell access ke liye: docker compose exec db psql -U miniflux aapko database mein le jayega, aur docker compose exec miniflux sh aapko app mein shell dega.

आपका डेटा वास्तव में कहाँ रहता है

Named volumes को project prefix मिलता है, इसलिए miniflux नामक directory में db-data बदलकर miniflux_db-data हो जाता है:

docker volume ls
docker volume inspect miniflux_db-data

inspect output में महत्वपूर्ण line शामिल है:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

वह directory database है — host filesystem पर root-owned, और यह down, upgrades, और container rebuilds के बाद भी सुरक्षित रहता है। आपके backups को भी ठीक इसी directory को capture करना चाहिए।

Named volume का backup लें

Standard pattern एक throwaway container का उपयोग करना है। यह volume को host directory के साथ read-only mode में mount करता है, और फिर tar command चलाता है:

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

इसमें कुछ भी install करने की आवश्यकता नहीं है और कोई process running नहीं रहता। Restore करने की प्रक्रिया इसका mirror image है — tar xzf को एक नए empty volume में, mounts को reverse करके, restore किया जाता है।

Databases के लिए एक सावधानी: running Postgres data directory को tar करने से mid-write state capture हो सकती है, जिससे database ठीक से start नहीं होगा। इसके लिए या तो tar प्रक्रिया के दौरान docker compose stop का उपयोग करें, या — बेहतर तरीका — logical dump लें, जो structure के अनुसार consistent होता है:

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

-T उस pseudo-terminal को disable करता है जिसे Compose default रूप से allocate करता है — dump output को TTY के माध्यम से pipe करने से data corrupt हो सकता है। इसे cron में सेट करें और result को VPS से बाहर copy करें; जिस disk पर data मौजूद है, उसी पर backup रखना केवल एक copy है, backup नहीं। Nextcloud guide इन्हीं दो patterns का उपयोग करके एक full scheduled routine बनाता है।

Failure modes, jinhe aap dekhenge

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — aap abhi docker group mein nahi hain, ya aap usme hain lekin session group mein judne se pehle ka hai. id aapke effective groups dikhata hai; newgrp docker current shell ko theek karta hai, logout karke phir se login karne se sab theek ho jayega.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — alag samasya: daemon band hai. sudo systemctl status docker aur sudo journalctl -u docker -n 50 iska karan batate hain. VPS par aam karan full disk hota hai — pehle df -h /var/lib/docker check karein.

Bind for 127.0.0.1:8080 failed: port is already allocated — koi dusra container pehle se hi us host port ka upyog kar raha hai. docker ps dikhata hai kaun sa; kuch hafton pehle ke kisi experimental docker run ka purana container aksar iska karan hota hai. Agar docker ps saaf hai, to koi non-Docker process port ko hold kar raha hai: sudo ss -tlnp | grep 8080 uska naam batata hai.

yaml: line 14: did not find expected key — di gayi line par ya uske thik upar indentation ki galti hai. Compose files YAML hoti hain: do-space indentation ka upyog karein, sirf spaces ka istemal karein, aur kahin bhi tab character hone par error aayega. docker compose config bina kuch start kiye file ko validate karta hai, aur har edit ke baad ise chalana ek acchi aadat hai.

The ufw surprise koi error nahi dikhata, jo ise khatarnak banata hai: deployment kaam karta hai, ufw status sahi dikhta hai, lekin bahar se port scan karne par aapka database mil jata hai. Upar diye gaye ports section ko phir se padhein, har ports: entry mein 127.0.0.1: prefix check karein, aur kisi dusre machine se curl http://your-vps-ip:8080 ke saath confirm karein — "connection refused" hi sahi jawab hai.

Yahan se, Traefik guide is single stack ko ek HTTPS entry point ke piche kai apps mein badal deta hai, aur what's worth self-hosting in 2026 un apps ki list hai jinhe aap is par chala sakte hain.

a Minecraft server on a VPS jaisa game server Compose seekhne ke liye ek accha pehla project hai.

FAQ

Why do I get "permission denied while trying to connect to the Docker daemon socket"?

Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.

Does docker compose down delete my data?

Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.

What is the difference between docker-compose and docker compose?

docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.

Why can I reach my Docker container from the internet even though ufw blocks the port?

Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.

Should I use a named volume or a bind mount?

Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.