SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-27

آموزش نصب و استفاده از Docker Compose در Ubuntu 24.04

نصب Docker Engine و Compose v2 روی Ubuntu 24.04، راه اندازی دو سرویس با compose.yml، رفع مشکل دور زدن قوانین ufw توسط داکر و نحوه بک‌آپ‌گیری از ولوم‌ها را بیاموزید.

آنچه در حال ساخت آن هستید

Docker Compose زیرساخت تقریباً تمام موارد دیگر در این سایت است. Nextcloud، Vaultwarden، n8n، Immich، Rocket.Chat، راهنمای هر یک از این سرویس‌ها با «نوشتن این فایل compose» آغاز می‌شود و این صفحه توضیح می‌دهد که آن فایل در واقع چه معنایی دارد. شما Docker Engine و پلاگین Compose v2 را از مخزن رسمی apt داکر روی Ubuntu 24.04 نصب خواهید کرد، سپس یک استک واقعی دو سرویسی شامل Miniflux (یک RSS reader سبک) و PostgreSQL را راه‌اندازی می‌کنید؛ زیرا این ترکیب تمام الگوهایی که برنامه‌های بزرگ‌تر استفاده می‌کنند را پوشش می‌دهد: ایمیج‌های با نسخه ثابت (pinned)، دیتابیس با healthcheck، ولوم نام‌گذاری‌شده، رمزها در یک فایل .env و پورتی که فقط روی localhost منتشر شده است.

نصب آن پنج دقیقه زمان می‌برد. باقی این راهنما به بخش‌هایی می‌پردازد که بعداً دردسرساز می‌شوند: گروه docker که در واقع همان دسترسی root با نامی دیگر است، پورت‌های منتشر شده‌ای که قوانین ufw شما را نادیده می‌گیرند و یک فلگ در docker compose down که دیتابیس شما را بدون هیچ پرسش تأییدی حذف می‌کند.

پیش‌نیازها: یک سرور مجازی (KVM VPS) با سیستم‌عامل تازه Ubuntu 24.04، یک کاربر با دسترسی sudo و حداقل یک گیگابایت رم. اگر از قبل داکر را نصب کرده‌اید نیز مشکلی نیست؛ بخش اول توضیح می‌دهد که چه مواردی را باید حذف کنید.

نصب از مخزن Docker، نه مخزن Ubuntu

پیش از اجرای اولین دستور، باید از دو اشتباه رایج اجتناب کنید. بسته docker.io خودِ Ubuntu کار می‌کند، اما از نسخه‌های رسمی Docker عقب‌تر است و ساختار پلاگینی که سایر ابزارها به آن وابسته هستند را ندارد. همچنین، فایل باینری مستقل docker-compose که با خط تیره نوشته می‌شود، مربوط به Compose v1 است؛ این نسخه مبتنی بر Python بوده، از سال 2023 منسوخ شده و دلیل اصلی خرابی آموزش‌های قدیمی است. Compose امروزی docker compose است که با فاصله نوشته می‌شود، یک CLI plugin محسوب شده و از همان مخزنی نصب می‌شود که موتور اصلی Docker در آن قرار دارد.

اگر هر یک از این موارد از قبل روی سیستم وجود دارد، ابتدا آن‌ها را پاک کنید، از جمله docker-compose-v2 که بسته‌بندی خودِ Ubuntu برای این پلاگین است، تا همه چیز از یک مخزن واحد تأمین شود:

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

خروجی Package 'docker.io' is not installed, so not removed در یک VPS تازه، طبیعی است. سپس مخزن Docker را اضافه کرده و آن را نصب کنید:

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

هر سه لایه را بررسی کنید:

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

دو دستور اول رشته‌های نسخه را چاپ می‌کنند و Docker Compose version v2.x.x تأیید می‌کند که شما پلاگین را در اختیار دارید، نه آن فایل باینری قدیمی و منسوخ v1. اجرای hello-world باید با Hello from Docker! پایان یابد. این بسته سرویس را در زمان بوت فعال می‌کند؛ systemctl is-enabled docker باید enabled را چاپ کند.

گروه docker همان دسترسی root است، با آگاهی کامل تصمیم بگیرید

در حال حاضر هر دستور docker به sudo نیاز دارد، زیرا سوکت daemon در مسیر /var/run/docker.sock متعلق به کاربر root و گروه docker است. بدون عضویت در این گروه، با رایج‌ترین خطای Docker که در گوگل جستجو می‌شود مواجه خواهید شد:

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

راه حل استاندارد:

sudo usermod -aG docker $USER

عضویت در گروه هنگام ورود به سیستم اعمال می‌شود، بنابراین خطا در shell فعلی شما باقی می‌ماند. برای این نشست دستور newgrp docker را اجرا کنید یا از سیستم خارج و دوباره وارد شوید؛ پس از آن id باید docker را در لیست گروه‌های شما نشان دهد.

و حالا بخش صادقانه ماجرا: عضویت در گروه docker به معنای داشتن دسترسی root روی میزبان است. نه «شبیه به root»، نه «دسترسی ارتقایافته»، بلکه دقیقاً root. هر کسی که در این گروه باشد می‌تواند دستور docker run --rm -it -v /:/host alpine chroot /host را اجرا کند و بدون نیاز به رمز عبور، مالکیت کل فایل‌سیستم را در اختیار بگیرد. این گروه برای راحتی ایجاد شده است، نه برای محدودسازی امنیتی.

حالت rootless در Docker جایگزین واقعی است، چرا که daemon در آن به عنوان کاربر بدون امتیاز شما اجرا می‌شود. این کار هزینه‌هایی دارد: پورت‌های زیر 1024 به تنظیمات اضافی نیاز دارند، شبکه از طریق یک shim در فضای کاربری اجرا می‌شود که سربار قابل اندازه‌گیری دارد، و برخی imageها بدون دسترسی واقعی root به درستی کار نمی‌کنند. در یک VPS تک‌مدیریتی که تنها کاربر آن از قبل دسترسی sudo دارد، این تغییر گروه در عمل چیزی را عوض نمی‌کند و تمام راهنماهای اینجا نیز همین فرض را دارند، فقط هرگز آن را به عنوان چیزی کمتر از sudo در اختیار کسی قرار ندهید.

ساختار یک فایل compose

برای هر stack یک دایرکتوری مجزا در نظر بگیرید. نام دایرکتوری به نام پروژه تبدیل می‌شود که به عنوان پیشوند برای کانتینرها، شبکه‌ها و volumeها استفاده خواهد شد:

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

از compose.yml استفاده کنید (نام مدرن؛ docker-compose.yml همچنان کار می‌کند). از کلید قدیمی version: صرف‌نظر کنید؛ این کلید منسوخ شده است و Compose در صورت مشاهده آن هشدار می‌دهد.

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:

هر خط در بالا یک تصمیم است. آن‌ها را یکی‌یکی بررسی کنید.

نسخه‌های image را ثابت کنید، :latest به همراه pull یک به‌روزرسانی کنترل‌نشده است

از postgres:16-alpine استفاده کنید، نه postgres:latest. یک تگ ثابت نیست: :latest هر بار که pull می‌کنید، به آخرین نسخه‌ای که maintainer منتشر کرده است بازمی‌گردد. اگر این را با عادت به‌روزرسانی روتین که در ادامه یاد می‌گیرید ترکیب کنید، docker compose pull && docker compose up -d و :latest باعث می‌شوند جهش‌های نسخه اصلی (major-version) دقیقاً زمانی رخ دهند که upstream آن‌ها را منتشر می‌کند، نه زمانی که شما انتخاب می‌کنید. در مورد PostgreSQL این یک فرضیه نیست: یک جهش ناگهانی از 16 به 17 باعث می‌شود کانتینر به دلیل ناسازگاری دایرکتوری داده، در یک حلقه crash-loop گیر کند، زیرا ارتقای نسخه اصلی Postgres نیازمند dump و restore است، نه فقط یک restart ساده.

حداقل نسخه اصلی را ثابت کنید (postgres:16-alpine از نسخه‌های patch 16.x پیروی می‌کند) و برنامه‌ها را روی یک نسخه دقیق مانند miniflux/miniflux:2.2.9 قفل کنید. صفحه releases پروژه را بررسی کنید و از نسخه‌ای که هنگام نوشتن فایل جاری است استفاده کنید. در این صورت، ارتقا به یک ویرایش تک‌خطی تبدیل می‌شود که آگاهانه انجام داده‌اید و در git diff قابل مشاهده است.

انتشار روی 127.0.0.1، زیرا Docker از ufw عبور می‌کند

"127.0.0.1:8080:8080"، آدرس میزبان، پورت میزبان، پورت کانتینر. اکثر آموزش‌ها "8080:8080" را می‌نویسند که مخفف 0.0.0.0:8080:8080 است: گوش دادن روی تمام اینترفیس‌ها، از جمله اینترفیس عمومی.

این همان تله‌ای است که تقریباً همه یک بار در آن می‌افتند. Docker با نوشتن یک قانون DNAT که مقصد بسته را به IP داخلی کانتینر تغییر می‌دهد، پورت را منتشر می‌کند. این اتفاق پیش از فیلترینگ رخ می‌دهد، بنابراین بسته از مسیر FORWARD عبور می‌کند و هرگز به INPUT، جایی که قوانین ufw شما قرار دارند، نمی‌رسد. sudo ufw deny 8080 موفقیت را گزارش می‌کند، ufw status پورت را مسدود نشان می‌دهد، اما سرویس همچنان به کل اینترنت پاسخ می‌دهد. فایروال شما خراب نیست؛ بلکه طبق طراحی از آن عبور می‌شود. چرا Docker از ufw عبور می‌کند و چگونه ترافیک کانتینر را واقعاً فیلتر کنیم مکانیزم این اتفاق و راهکار DOCKER-USER برای پورت‌هایی که باید عمومی بمانند را توضیح می‌دهد.

عادتی که این مشکل را کاملاً از بین می‌برد: پورت‌های منتشر شده را به 127.0.0.1 متصل (bind) کنید، مگر اینکه دلیل خاصی برای عدم انجام آن داشته باشید، و برای هر چیزی که باید در دسترس عموم باشد، یک reverse proxy در مقابل آن قرار دهید. این دقیقاً همان چیزی است که راهنمای reverse proxy با Traefik به عنوان گام بعدی پس از این صفحه می‌سازد؛ یک کانتینر که پورت‌های 80 و 443 را در اختیار دارد و با استفاده از نام دامنه و TLS، درخواست‌ها را به بقیه سرویس‌ها هدایت می‌کند. (اگر از تنظیمات قدیمی Traefik v2 می‌آیید؟ راهنمای مهاجرت از Traefik v2 به v3 تغییرات نام و قوانین را پوشش می‌دهد.)

پس از شروع stack، اتصال را بررسی کنید: 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/ ایجاد و مدیریت می‌کند و آن را به داخل کانتینر mount می‌کند. جایگزین آن bind mount است، ./data:/var/lib/postgresql/data، که یک مسیر انتخابی شما روی میزبان را نگاشت می‌کند.

تفکیکی که در عمل کارآمد است: named volumes برای داده‌هایی که فقط کانتینرها با آن‌ها سروکار دارند، به‌ویژه دیتابیس‌ها؛ زیرا Docker حجم را با مالکیت (ownership) مورد انتظار image مقداردهی اولیه می‌کند و مجوزهای فایل به‌درستی کار می‌کنند. Bind mounts برای فایل‌هایی که شما از روی میزبان با آن‌ها کار می‌کنید، مانند فایل‌های پیکربندی که با ویرایشگر متن تغییر می‌دهید، کتابخانه رسانه‌ای که با rsync منتقل می‌کنید، یا هر چیزی که می‌خواهید مسیر آن مشخص باشد. شکست کلاسیک در bind-mount مربوط به مالکیت است: کانتینر با UID 999 اجرا می‌شود، دایرکتوری میزبان شما متعلق به UID 1000 است و برنامه هنگام شروع با خطای permission denied در لاگ‌ها متوقف می‌شود. Named volumes این دسته از باگ‌ها را تقریباً از بین می‌برد، به قیمت اینکه داده‌ها در مسیری که توسط Docker مدیریت می‌شود قرار می‌گیرند (که در ادامه توضیح داده شده است).

environment و .env، اسرار را از git دور نگه دارید

${POSTGRES_PASSWORD} از shell شما خوانده نمی‌شود؛ Compose آن را از فایلی به نام .env که در کنار compose.yml قرار دارد، جایگذاری (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 تولید کنید. عمداً از Hex استفاده کنید، نه base64: این رمز عبور در رشته اتصال DATABASE_URL قرار می‌گیرد و کاراکترهای /، + و = که base64 تولید می‌کند، تجزیه URL را مختل می‌کنند. این یک شکست است که به صورت خطای احراز هویت (و نه خطای سینتکس) ظاهر می‌شود و یک شب وقت شما را می‌گیرد. خط .gitignore باید پیش از اولین commit اضافه شود: فایل compose برای انتشار و نسخه‌گذاری امن است، اما فایل .env هرگز نباید در git باشد. اگر یک متغیر را هنگام شروع stack فراموش کنید، Compose با صدای بلند هشدار می‌دهد و با یک رشته خالی ادامه می‌دهد، که برای رمز عبور Postgres به معنای یک deployment خراب است:

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

docker compose config فایل کاملاً جایگذاری‌شده را چاپ می‌کند؛ این سریع‌ترین راه برای بررسی چیزی است که کانتینرها واقعاً دریافت خواهند کرد. به یاد داشته باشید که خروجی آن شامل اسرار شماست.

depends_on منتظر چیزی نمی‌ماند، مگر اینکه healthcheck اضافه کنید

یک depends_on: [db] ساده فقط ترتیب شروع را کنترل می‌کند: Compose ابتدا Postgres را اجرا می‌کند و لحظه‌ای بعد برنامه را، در حالی که Postgres هنوز ثانیه‌ها با پذیرش اتصالات فاصله دارد. برنامه به دیتابیس متصل می‌شود، شکست می‌خورد و بسته به نحوه کدنویسی، crash می‌کند یا دوباره تلاش می‌کند.

نسخه قابل‌اطمینان همان چیزی است که در فایل بالا استفاده شده است: سرویس db یک healthcheck تعریف می‌کند (Postgres دقیقاً برای همین منظور pg_isready را ارائه می‌دهد) و برنامه depends_on را با condition: service_healthy اعلام می‌کند. Compose دیتابیس را شروع می‌کند، هر 10 ثانیه وضعیت را بررسی می‌کند و تنها زمانی Miniflux را شروع می‌کند که بررسی با موفقیت انجام شود. اگر دیتابیس هرگز سالم نشود (به دلیل رمز عبور اشتباه یا volume خراب)، برنامه هرگز شروع نمی‌شود و Compose به شما می‌گوید کدام وابستگی شکست خورده است:

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

آن پیام شما را به docker compose logs db هدایت می‌کند، جایی که خطای اصلی قرار دارد.

restart: unless-stopped

restart: unless-stopped در هر دو سرویس به این معنی است که کانتینرها پس از crash و پس از reboot سرور VPS برمی‌گردند، اما اگر عمداً دستور docker compose stop را اجرا کرده باشید، متوقف می‌مانند. جایگزین آن always است که کانتینرها را حتی پس از توقف دستی دوباره زنده می‌کند، که به‌ندرت همان چیزی است که شما می‌خواهید. بدون سیاست restart، یک reboot برای به‌روزرسانی هسته در ساعت 4 صبح، سرویس‌های شما را بی‌سروصدا تا زمانی که متوجه شوید، از دسترس خارج می‌کند.

افعال روزمره

همه کارهای روزمره با پنج دستور انجام می‌شوند که باید از داخل دایرکتوری پروژه اجرا شوند.

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 ایمن است؛ این دستور فایل را با وضعیت واقعی مقایسه کرده و فقط سرویس‌هایی را تغییر می‌دهد که تنظیمات یا image آن‌ها تغییر کرده باشد. جفت دستور ارتقا، هر آنچه را که تگ‌های ثابت (pinned) شما اکنون به آن اشاره دارند دریافت می‌کند: نسخه‌های patch تحت postgres:16-alpine؛ برای نسخه‌هایی که دقیقاً تعیین شده‌اند (pinned) تا زمانی که فایل را ویرایش نکنید تغییری رخ نمی‌دهد، که دقیقاً هدف همین است. پس از ارتقا، imageهای قدیمی انباشته می‌شوند؛ برای آزادسازی فضای دیسک از docker image prune -f استفاده کنید.

حالا دستور مخرب، با تأکید زیاد: اجرای docker compose down ایمن است، کانتینرها و شبکه قابل‌دورریختن هستند و داده‌های شما در volume قرار دارند. docker compose down -v volumeهای نام‌گذاری‌شده را نیز حذف می‌کند. این یعنی دیتابیس شما بلافاصله و بدون هیچ پرسش تأیید یا امکان بازگشت، از بین می‌رود. فلگ -v برای پاکسازی آزمایش‌ها وجود دارد؛ در پشته‌ای (stack) که داده‌های واقعی دارد، با آن همان‌طور رفتار کنید که با rm -rf رفتار می‌کنید. در /var/lib/docker/volumes/ سطل زباله‌ای وجود ندارد.

برای دسترسی به یک shell موقت داخل کانتینر در حال اجرا: docker compose exec db psql -U miniflux شما را به دیتابیس وارد می‌کند و docker compose exec miniflux sh یک shell در برنامه به شما می‌دهد.

محل واقعی ذخیره‌سازی داده‌ها

Named volumes از پیشوند پروژه استفاده می‌کنند، بنابراین db-data در دایرکتوری با نام miniflux به miniflux_db-data تبدیل می‌شود:

docker volume ls
docker volume inspect miniflux_db-data

خروجی inspect شامل خطی است که اهمیت دارد:

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

آن دایرکتوری همان پایگاه داده است که مالکیت آن در اختیار root در فایل‌سیستم میزبان قرار دارد و پس از down، ارتقاها و بازسازی کانتینر باقی می‌ماند. این دقیقاً همان بخشی است که بک‌آپ‌های شما باید از آن تهیه شود.

پشتیبان‌گیری از یک named volume

الگوی استاندارد، استفاده از یک container یک‌بارمصرف است که volume را به‌صورت read-only در کنار یک دایرکتوری میزبان mount می‌کند و محتوا را با tar فشرده می‌سازد:

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 .

بدون نیاز به نصب هیچ‌چیز و بدون باقی ماندن هیچ پروسهٔ در حال اجرایی؛ عملیات بازیابی نیز دقیقاً برعکس همین حالت است و با استفاده از tar xzf در یک volume جدید و خالی با همان mountهای معکوس انجام می‌شود.

یک نکتهٔ مهم برای پایگاه‌های داده: تهیه فایل tar از دایرکتوری داده‌های Postgres در حالی که در حال اجرا است، ممکن است وضعیت میانهٔ نوشتن (mid-write) را ثبت کند که باعث می‌شود دیتابیس به‌درستی بالا نیاید. یا برای چند ثانیه‌ای که tar در حال اجراست از docker compose stop استفاده کنید، یا بهتر از آن، یک logical dump بگیرید که ذاتاً منسجم است:

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

فلگ -T باعث غیرفعال شدن pseudo-terminal می‌شود که Compose به‌صورت پیش‌فرض تخصیص می‌دهد؛ چرا که ارسال خروجی dump از طریق TTY می‌تواند باعث خرابی فایل شود. یکی از این دستورات را در cron قرار دهید و نتیجه را از روی VPS کپی کنید؛ پشتیبان‌گیری روی همان دیسکی که داده‌های اصلی قرار دارند، فقط یک کپی است و نه یک نسخهٔ پشتیبان واقعی. در راهنمای Nextcloud یک روتین زمان‌بندی‌شدهٔ کامل دقیقاً بر اساس همین دو الگو پیاده‌سازی شده است.

حالت‌های شکست و پیام‌هایی که مشاهده خواهید کرد

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock، شما هنوز در گروه docker نیستید، یا عضو هستید اما نشست (session) فعلی شما پیش از اعمال این تغییر ایجاد شده است. id گروه‌های فعال شما را نشان می‌دهد؛ newgrp docker مشکل را در shell فعلی برطرف می‌کند، اما برای رفع کامل آن در همه جا، باید از سیستم خارج و دوباره وارد شوید.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?، مشکل متفاوتی است: خود daemon در حال اجرا نیست. sudo systemctl status docker و sudo journalctl -u docker -n 50 دلیل آن را بیان می‌کنند. در یک VPS، دلیل کلاسیک این اتفاق پر شدن دیسک است، پس ابتدا df -h /var/lib/docker را بررسی کنید.

Bind for 127.0.0.1:8080 failed: port is already allocated، یک container دیگر قبلاً آن پورت میزبان را اشغال کرده است. docker ps نشان می‌دهد کدام container این کار را کرده است؛ معمولاً یک container قدیمی و بلااستفاده از یک docker run آزمایشی در هفته‌های گذشته مقصر اصلی است. اگر docker ps خالی است، یک پردازش غیر Docker پورت را نگه داشته است: sudo ss -tlnp | grep 8080 نام آن پردازش را مشخص می‌کند.

yaml: line 14: did not find expected key، یک خطای تورفتگی (indentation) در خط ذکر شده یا درست بالای آن وجود دارد. فایل‌های Compose از نوع YAML هستند: تورفتگی دو فاصله، فقط با استفاده از space؛ وجود حتی یک کاراکتر tab در هر جای فایل کشنده است. docker compose config فایل را بدون اجرای هیچ سرویسی اعتبارسنجی می‌کند و اجرای آن پس از هر ویرایش، یک عادت کم‌هزینه و مفید است.

غافلگیری ufw هیچ خطایی چاپ نمی‌کند و همین موضوع آن را خطرناک می‌کند: استقرار (deploy) با موفقیت انجام می‌شود، ufw status درست به نظر می‌رسد، اما یک اسکن پورت از بیرون همچنان دیتابیس شما را پیدا می‌کند. بخش پورت‌ها را در بالا دوباره بخوانید، هر ورودی ports: را برای نبود پیشوند 127.0.0.1: بررسی کنید و از یک ماشین دیگر با استفاده از curl http://your-vps-ip:8080 تأیید بگیرید؛ پاسخ مورد انتظار شما connection refused است.

از اینجا به بعد، راهنمای Traefik این stack واحد را به برنامه‌های متعددی در پشت یک نقطه ورود HTTPS تبدیل می‌کند و آنچه ارزش self-hosting در سال 2026 را دارد لیست خریدی برای اجرا روی آن است. هنگامی که چندین مورد از این stackها در حال اجرا باشند و هر کدام فرم ورود مخصوص به خود را داشته باشند، یک سرور SSO خودمیزبان مانند Authentik آن‌ها را دوباره در یک حساب کاربری واحد در پشت همان proxy تجمیع می‌کند.

یک game server مانند یک Minecraft server روی یک VPS گزینه‌ای مناسب برای نخستین پروژه Compose و تمرین عملی است. اگر ترجیح می‌دهید کار را با چیزی یاد بگیرید که هر روز باز می‌کنید، openGym، یک workout tracker خودمیزبان یک stack کوچک است که به‌جای image tag به یک git tag ثابت متصل شده است؛ همچنین پیش از ثبت نخستین passkey به TLS در لایهٔ جلویی نیاز دارد. عکس‌ها معمولاً نخستین داده‌هایی هستند که افراد می‌خواهند از cloud دیگران خارج کنند. مقایسهٔ PhotoPrism و Immich حداقل RAM و روال backup موردنیاز را پیش از آن‌که یک volume را به هرکدام اختصاص دهید مشخص می‌کند. وقتی دو سرویس دیگر کافی به نظر نمی‌رسند، راه‌اندازی AFFiNE به‌عنوان یک workspace مشابه Notion همان الگوها را در قالب چهار container پیاده می‌کند و آزمون مناسبی است تا ببینید استفاده از tagهای ثابت، healthcheckها و volumeهای نام‌دار که در بالا به آن‌ها اشاره شد، به عادت تبدیل شده است یا نه.

FAQ

چرا خطای "permission denied while trying to connect to the Docker daemon socket" را دریافت می‌کنم؟

کاربر شما در گروه docker عضو نیست یا پس از شروع نشست فعلی به آن اضافه شده است؛ عضویت در گروه فقط پس از ورود مجدد اعمال می‌شود. دستور sudo usermod -aG docker $USER را اجرا کنید، سپس newgrp docker را بزنید یا از سیستم خارج و دوباره وارد شوید و با id وضعیت را تأیید کنید. این گروه دسترسی معادل root به میزبان می‌دهد، بنابراین فقط کاربرانی را اضافه کنید که به آن‌ها دسترسی sudo می‌دهید.

آیا دستور docker compose down داده‌های من را حذف می‌کند؟

دستور ساده docker compose down این کار را نمی‌کند؛ این دستور فقط کانتینرها و شبکه پروژه را حذف می‌کند. volumeهای نام‌گذاری‌شده باقی می‌مانند و با دستور up -d بعدی دوباره متصل می‌شوند. docker compose down -v شکل مخرب دستور است: این دستور volumeهای نام‌گذاری‌شده، از جمله دیتابیس شما را بدون تأیید و امکان بازگشت حذف می‌کند. هرگز -v را روی استکی که حاوی داده‌های واقعی است اجرا نکنید، مگر اینکه نسخه پشتیبان تأییدشده داشته باشید.

تفاوت docker-compose و docker compose در چیست؟

docker-compose (با خط تیره) نسخه 1 کامپوز است؛ یک باینری مستقل پایتون که در سال 2023 به پایان عمر خود رسید و نباید روی سرورهای جدید نصب شود. docker compose (با فاصله) نسخه 2 کامپوز است؛ یک پلاگین Go برای Docker CLI که به عنوان docker-compose-plugin از مخزن apt داکر نصب می‌شود. دستورات و فایل‌های YAML تقریباً کاملاً سازگار هستند، بنابراین وقتی در یک آموزش قدیمی docker-compose up ذکر شده، شما docker compose up را تایپ کنید.

چرا با وجود مسدود بودن پورت در ufw، همچنان می‌توانم از اینترنت به کانتینر داکر دسترسی داشته باشم؟

زیرا داکر پورت‌ها را با قوانین DNAT در زنجیره PREROUTING از iptables منتشر می‌کند و بسته‌های بازنویسی‌شده از مسیر FORWARD در زنجیره‌های اختصاصی داکر عبور می‌کنند؛ بنابراین آن‌ها هرگز به زنجیره INPUT که قوانین ufw در آن اعمال می‌شوند، نمی‌رسند. در نتیجه ufw deny 8080 هیچ تأثیری بر پورت منتشرشده کانتینر ندارد. این مشکل را از منبع حل کنید: پورت را روی 127.0.0.1: منتشر کنید و سرویس‌ها را از طریق یک reverse proxy در معرض دید قرار دهید.

آیا باید از named volume استفاده کنم یا bind mount؟

برای داده‌هایی که فقط کانتینر با آن‌ها سروکار دارد، به‌ویژه دیتابیس‌ها، از named volume استفاده کنید؛ زیرا داکر مالکیت فایل‌ها را طبق انتظار ایمیج تنظیم می‌کند و مجوزها به‌درستی کار می‌کنند. برای فایل‌هایی که از سمت میزبان نیز با آن‌ها کار می‌کنید، مانند تنظیماتی که ویرایش می‌کنید، رسانه‌هایی که آپلود می‌کنید یا هر فایلی که می‌خواهید مسیر آن مشخص باشد، از bind mount استفاده کنید. اگر کانتینری هنگام شروع با خطای permission denied در یک bind mount متوقف شد، اولین چیزی که باید بررسی کنید عدم تطابق UID بین میزبان و کانتینر است.