آموزش نصب و استفاده از Docker Compose روی Ubuntu 24.04
نصب Docker Engine و افزونه Compose روی Ubuntu 24.04 را بیاموزید. راهنمای راهاندازی دو سرویس با PostgreSQL، مدیریت فایلهای env، جلوگیری از دور زدن ufw و پشتیبانگیری از volumeها.
آنچه میسازید
Docker Compose زیربنای تقریباً تمام سرویسهای معرفیشده در این سایت است. Nextcloud، Vaultwarden، n8n، Immich، Rocket.Chat؛ تمام این راهنماها با «نوشتن این فایل compose» آغاز میشوند و این صفحه توضیح میدهد که آن فایل واقعاً چه معنایی دارد. شما Docker Engine و افزونه Compose را از مخزن رسمی apt شرکت Docker روی Ubuntu 24.04 نصب خواهید کرد. سپس یک پشته (stack) واقعی شامل دو سرویس را راهاندازی میکنید: Miniflux که یک RSS reader سبک است، به همراه PostgreSQL. این ترکیب تمام الگوهایی که برنامههای بزرگتر استفاده میکنند را پوشش میدهد: imageهای نسخه-ثابت (pinned)، دیتابیس با healthcheck، volume نامگذاریشده، secretها در یک فایل .env و پورتی که فقط روی localhost منتشر شده است.
نصب این موارد 5 دقیقه زمان میبرد. باقی این راهنما به بخشهایی میپردازد که بعداً باعث دردسر میشوند: گروه docker که در واقع همان دسترسی root است، پورتهای منتشرشدهای که قوانین ufw شما را دور میزنند، و یک flag در docker compose down که دیتابیس شما را بدون هیچ تأییدی پاک میکند.
پیشنیازها: یک سرور مجازی (KVM VPS) با سیستمعامل تازه Ubuntu 24.04، یک کاربر با دسترسی sudo و حداقل 1 گیگابایت رم. اگر Docker از قبل نصب شده باشد نیز مشکلی نیست؛ بخش اول توضیح میدهد که چه مواردی باید حذف شوند.
نصب از مخزن Docker، نه مخزن Ubuntu
پیش از اجرای اولین دستور، باید از دو اشتباه رایج اجتناب کنید. بسته docker.io خودِ Ubuntu کار میکند، اما از نسخههای رسمی Docker عقبتر است و ساختار پلاگینی که سایر ابزارها به آن وابستهاند را ندارد. همچنین، دستور خطتیره دار docker-compose مسیری است که آموزشهای قدیمی شما را به آن هدایت میکنند. در سرورهای قدیمی، این دستور معمولاً به Compose v1 اشاره دارد که با زبان Python نوشته شده، توسط Docker منسوخ اعلام شده و دلیل اصلی شکست خوردن آن آموزشهاست. Docker هنوز یک فایل باینری مستقل به نام docker-compose را که با کد Go امروزی ساخته شده منتشر میکند، اما صفحه نصب خودِ Docker این روش را توصیه نمیکند و تنها برای سازگاری با نسخههای قدیمی پشتیبانی میشود. امروزه Compose به صورت docker compose (با یک فاصله)، یعنی به عنوان یک CLI plugin، از همان مخزنی نصب میشود که موتور اصلی Docker در آن قرار دارد.
اگر هر یک از این موارد از قبل روی سیستم وجود دارد، ابتدا آنها را پاک کنید؛ این شامل docker-compose-v2 و docker-buildx که بستهبندیهای خودِ Ubuntu برای این پلاگینها هستند نیز میشود تا همه چیز از یک مخزن واحد تأمین شود:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx 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 v5.x.x تأیید میکند که شما پلاگین را در اختیار دارید، نه آن فایل باینری منسوخ v1. این پلاگین در دسامبر 2025 از نسخه v2 مستقیماً به v5 جهش کرد تا با نسخههای فرمت فایل compose قدیمی اشتباه گرفته نشود؛ بنابراین راهنماهایی که در اینجا v2.x.x را نشان میدهند، در واقع همان ابزار را توصیف میکنند. اجرای 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 (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 مربوط به مالکیت است: image پردازش خود را با یک UID ثابت اجرا میکند (image Miniflux با UID 65534 اجرا میشود، postgres:16-alpine به UID 70 تغییر میکند)، دایرکتوری میزبان شما متعلق به UID 1000 است و برنامه در هنگام شروع با خطای permission denied در لاگها متوقف میشود. Named volumes این دسته از باگها را تا حد زیادی از بین میبرند، به قیمت اینکه دادهها در مسیری که توسط Docker مدیریت میشود قرار میگیرند (که در ادامه پوشش داده شده است).
محیط (environment) و .env، اسرار را از git دور نگه دارید
Compose مقدار ${POSTGRES_PASSWORD} را از فایلی به نام .env که در کنار compose.yml قرار دارد پر میکند، اما تنها زمانی که shell شما آن متغیر را تعریف نکرده باشد: مقداری که در محیط shell شما export شده باشد، بر فایل .env اولویت دارد. بنابراین یک export POSTGRES_PASSWORD=... فراموششده از یک آزمایش قبلی، بیسروصدا فایل را نادیده میگیرد و docker compose config (در زیر) نشان میدهد که کدام مقدار برنده شده است. فایل را ایجاد کنید:
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 قرار بگیرد، و هر رازی که به تاریخچه git راه پیدا کند، رازی است که باید آن را تغییر دهید (rotate کنید). اگر 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، یک کانتینر دیگر قبلاً آن پورت میزبان را اشغال کرده است. دستور docker ps نشان میدهد کدام کانتینر این کار را کرده است؛ معمولاً مقصر، یک کانتینر قدیمی از یک 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 تبدیل میکند و آنچه در سال 2026 ارزش self-hosting دارد لیست خریدی است که میتوانید از آن استفاده کنید. هنگامی که چندین مورد از این stackها اجرا شدند و هر کدام فرم ورود اختصاصی خود را ساختند، یک سرور SSO خودمیزبان مانند Authentik آنها را به یک حساب کاربری واحد در پشت همان پروکسی تجمیع میکند.
یک سرور بازی مانند سرور Minecraft روی VPS یک پروژه Compose دوستانه برای تمرین است. اگر ترجیح میدهید با چیزی که هر روز باز میکنید یاد بگیرید، openGym، یک ردیاب تمرین خودمیزبان یک stack کوچک است که به یک git tag متصل شده (نه یک image tag) و پیش از ثبت اولین passkey، به TLS در لایه جلویی نیاز دارد. عکسها معمولاً اولین چیزی هستند که افراد میخواهند از فضای ابری دیگران خارج کنند، و مقایسه PhotoPrism و Immich حداقل رم مورد نیاز و روتین پشتیبانگیری که باید متعهد به آن شوید را پیش از اختصاص volume به هر کدام، مشخص میکند. وقتی دو سرویس دیگر کافی به نظر نرسیدند، راهاندازی AFFiNE به عنوان یک فضای کاری مشابه Notion همان الگوها را در چهار کانتینر پیاده میکند و آزمون خوبی است تا ببینید آیا استفاده از pinned tags، healthchecks و named volumes برای شما به عادت تبدیل شده است یا خیر.
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 (با خط تیره) روش قدیمی فراخوانی Compose است. در سرورهای قدیمی، این معمولاً همان Compose v1 است؛ نسخه اصلی مبتنی بر Python که Docker پایان عمر آن را اعلام کرده است. Docker هنوز یک فایل اجرایی مستقل به نام docker-compose که با کد Go فعلی ساخته شده منتشر میکند، اما نصب آن توصیه نمیشود و فقط برای سازگاری با نسخههای قدیمی پشتیبانی میشود، بنابراین در سرورهای جدید از آن صرفنظر کنید. docker compose (با فاصله) نسخه فعلی Compose است؛ از v2 به بعد و از دسامبر 2025 با شماره نسخه v5، این یک پلاگین 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 بین میزبان و کانتینر است.