محیط آموزشیِ تصویرسازی‌شدهٔ آکادمی ویونیکس با دو مسیر blue و green، شرط سلامت و پراکسی قابل‌برگشت، بدون حضور افراد

WIUNIX ACADEMY

انتشار blue/green با Docker Compose؛ از healthcheck تا برگشت مسیر

دو نسخهٔ بدون state رو در پروژه‌های جدا بالا می‌آریم، سلامت نسخهٔ تازه رو از مسیر پراکسی می‌سنجیم و مسیر رو با امکان برگشت عوض می‌کنیم؛ بدون ادعای انتشار بی‌قطعی یا بازیابی داده.

متوسط تا پیشرفته45 دقیقه مطالعهبه‌روزرسانی
محیطت رو انتخاب کن

آموزش متناسب با محیط تو

کجا اجرا می‌کنی؟

سیستم‌عامل و نسخهٔ سرورت رو انتخاب کن تا مراحل، دستورها و راه‌حل‌های همون محیط رو ببینی.

Ubuntu 24.04 LTSبررسی‌شده با مستندات رسمی ·

آموزش متناسب با مقادیر تو

تنظیمات این آموزش

مقادیر نمونه رو با مشخصات سرورت عوض کن تا متن و دستورها با هم تغییر کنن. رمز، کلید خصوصی و توکن اینجا وارد نکن. مقدارها فقط توی همین زبانه می‌مونن و به سرور یا لینک اشتراک‌گذاری فرستاده نمی‌شن. قبل از اجرا، DNS، گواهی‌ها و تنظیمات دسترسی شبکه رو روی سرورت بررسی کن.

اگر آی‌پی، دامنه یا پورت‌های نمونه با سرورت فرق دارن، بخش تنظیمات پایین رو باز کن. مقدارها رو عوض کن و «اعمال در آموزش» رو بزن؛ بعد دستورهای تازه رو کپی کن.

تنظیم مقادیر برای محیط من5 مقدار قابل‌تغییر

از اینجا شروع کن

آخر این آموزش چی خواهیم داشت؟

دو نسخهٔ بدون state رو در پروژه‌های جدا بالا می‌آریم، سلامت نسخهٔ تازه رو از مسیر پراکسی می‌سنجیم و مسیر رو با امکان برگشت عوض می‌کنیم؛ بدون ادعای انتشار بی‌قطعی یا بازیابی داده.

سیستم‌عامل‌ها و نسخه‌های پوشش‌داده‌شده

مسیرهای نصب موجود Docker CE و افزونهٔ Compose در Ubuntu 26.10/26.04/24.04، Debian 13/12 و خانوادهٔ RHEL/Rocky/AlmaLinux 10/9، و بسته‌های خود Arch/Manjaro در ۲۰۲۶-۱۱-۰۵ با مستندات رسمی بررسی شدن. این مسیر به Engine محلیِ rootful نسخهٔ ۲۸ یا جدیدتر نیاز داره. تفاوت مخزن، لانچر و سیاست میزبان در گزینهٔ هر محیط آمده؛ نصب، اجرای کانتینر یا آزمون عملی همهٔ توزیع‌ها ادعا نشده.

قبل از شروع

  • VM آزمایشی لینوکس با Bash و sudo مجاز، daemon محلیِ rootful از قبل موجود و ظرفیت هم‌زمان دو نسخه و پراکسی.
  • Docker Engine نسخهٔ ۲۸ یا جدیدتر، افزونهٔ docker compose با --wait و --wait-timeout، curl و ابزارهای بستهٔ iproute و coreutils.
  • مسیر و نام پروژهٔ تازه، سه پورت loopback آزاد و هیچ دادهٔ برنامه یا secret در پوشه‌های نمونه.
  • دسترسی مجاز به Docker Official Image برای pull digest؛ اختلال رجیستری مجوز حذف pin یا استفاده از mirror ناشناس نیست.

healthy شدن، مسیر ترافیک رو عوض نمی‌کنه

Compose می‌تونه برای موفق‌شدن healthcheck صبر کنه، ولی این کار نه مسیر reverse proxy رو عوض می‌کنه و نه درست‌کارکردن نسخه برای کاربر رو ثابت می‌کنه. در یک ماشین آزمایشی، دو نسخهٔ کوچک و بدون state رو در دو پروژهٔ Compose می‌گذاریم؛ یعنی خود نسخه‌ها داده یا نشست ماندگارِ برنامه تولید نمی‌کنن. هرکدوم شبکهٔ bridge و پورت آزمایش loopback خودش رو داره. پراکسی داخل پروژهٔ blue به هر دو شبکه وصل می‌شه و پورت سوم رو می‌گیره؛ مسیرش رو صریحاً به green می‌بریم و بعد به blue برمی‌گردونیم.

دو نسخه فقط متن کوتاه متفاوتی رو با یک Nginx ثابت‌شده به digest ارائه می‌دن. موضوع این تمرین جداسازی نسخه‌ها، شرط سلامت و اثبات مسیر ترافیکه، نه ساخت image برنامه. پایگاه داده، migration، دادهٔ مشترکِ قابل‌تغییر، ترافیک کاربر واقعی و TLS نداریم. تا پایان مهلت برگشت، هر دو نسخه روشن می‌مونن.

اینجا وعدهٔ انتشار بدون قطعی نمی‌دیم. بعد از reload ممکنه workerهای تازه و قدیمی مدتی کنار هم باشن؛ اتصال طولانی، کار پس‌زمینه، cache و سازگاری schema به طراحی جداگانهٔ برنامه نیاز دارن. یک probe محلیِ موفق روی green اون رفتارها رو تأیید نمی‌کنه.

نام، پورت و پوشهٔ تازه انتخاب کنیم

فرم مقادیر نمونه رو باز کن و یک پیشوند تازه با حروف کوچک، مسیر کاملِ پوشهٔ تازه و سه پورت TCP متفاوت بالاتر از ۱۰۲۳ رو اعمال کن. نام پروژه‌ها با پسوندهای -blue و -green ساخته می‌شن. آدرس همهٔ پورت‌های منتشرشده روی 127.0.0.1 ثابت می‌مونه؛ این آموزش فیلدی برای بازکردن سرویس روی اینترنت نداره.

مقدارها در همین تب و آموزش و زبان و محیط انتخاب‌شده می‌مونن. تغییر نامعتبر یا اعمال‌نشده تا زمان اعمال یا بازنشانی، کپی رو متوقف می‌کنه. رمز رجیستری و secret برنامه رو وارد نکن. بررسی پایین پروژه و پوشهٔ از قبل موجود رو رد می‌کنه، ولی ممکنه بعدش فرایند دیگری پورت رو بگیره؛ خطای bind در مرحلهٔ بعد یعنی توقف، نه اجازهٔ کشتن اون فرایند.

01

قدم بعدی

Engine و افزونهٔ Compose موجود رو بشناسیم

از یک ماشین مجازی آزمایشی شروع کن که Docker daemon محلی آن از قبل با مجوز تیم و با کاربر root اجرا می‌شه؛ به این حالت rootful می‌گیم. نسخهٔ Engine باید ۲۸ یا جدیدتر باشه و افزونهٔ Compose هم --wait و --wait-timeout رو داشته باشه. تابع پایین به socket محلی وصل می‌شه و نمی‌گذاره انتخاب context یا میزبان از متغیرهای محیط، مقصد رو عوض کنه. همین نشست Bash رو برای پنل‌های بعدی نگه دار. دسترسی به این daemon در حد مدیریت سیستمه؛ این مسیر برای rootless، Docker Desktop، daemon دوردست یا Podman نوشته نشده.

مسیر DEB انتخاب‌شده از بسته‌های stable مربوط به Docker CE و docker-compose-plugin استفاده می‌کنه؛ لانچر قدیمیِ پایتونی docker-compose رو به‌جاش فرض نمی‌کنیم و docker.io یا Moby موجود رو هم بی‌سروصدا عوض نمی‌کنیم. اگر نصب تو فرق داره، توقف کن و محیط آزمایش مجاز رو با مرجع همون روش آماده کن. این آموزش daemon نصب یا ارتقا نمی‌ده، مخزن رو عوض نمی‌کنه و سرویس میزبان رو ری‌استارت نمی‌کنه.

Docker در فهرست فعلی Ubuntu نسخه‌های 26.10، 26.04 و 24.04 رو دارد. نام codename و کانال بستهٔ نصب موجود باید با میزبان یکی باشه؛ مخزن نسخهٔ دیگری رو برای عبور از این تمرین استفاده نکن.

bash

ماشین آزمایشی لینوکس؛ همان نشست Bash مجاز

cat /etc/os-release
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin curl iproute2 coreutils
docker_lab() { sudo env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock "$@"; }
docker_lab version --format '{{.Server.Version}}'
docker_lab info --format '{{.OperatingSystem}} {{json .SecurityOptions}}'
docker_lab compose version
compose_help=$(docker_lab compose up --help) || exit 1
printf '%s\n' "$compose_help" | grep -E -- '--wait([[:space:]]|$)' || exit 1
printf '%s\n' "$compose_help" | grep -F -- '--wait-timeout' || exit 1
engine_version=$(docker_lab version --format '{{.Server.Version}}') || exit 1
engine_major=${engine_version%%.*}
case "$engine_major" in ''|*[!0-9]*) exit 1;; esac
test "$engine_major" -ge 28 || exit 1
از کجا بفهمم درست انجام شده؟

کانال بسته، daemon محلیِ rootful، نسخهٔ ۲۸ یا جدیدتر و گزینه‌های لازم افزونه تأیید شدن؛ نصب متفاوت یا پشتیبانی‌نشده یعنی توقف.

02

قدم بعدی

مالکیت نمونه رو کنترل کنیم و پوشهٔ تازه بسازیم

شرط نام کوچکِ پروژه از فیلد عمومی فرم محدودتره. همهٔ کانتینرها، حتی متوقف‌شده‌ها، و شبکه‌هایی رو که برچسب یکی از دو پروژه رو دارن بررسی می‌کنیم. شکستِ پرس‌وجوی Docker رو فهرست خالی حساب نمی‌کنیم. اگر نام برخورد داشت، مقدار نمونه رو عوض کن؛ بار شخص دیگری رو حذف نکن.

یک پوشهٔ تازه با مالکیت root و زیرپوشه‌های عمومیِ همین نمونه می‌سازیم. مسیر برنامهٔ موجود رو اینجا نگذار؛ برچسب خصوصی SELinux در ادامه فقط روی همین پوشه‌های تازه اعمال می‌شه. سه پورت شنونده رو هم می‌بینیم، ولی daemon یا برنامهٔ دیگری هنوز می‌تونه هم‌زمان پورت رو بگیره. کانتینر یا شنوندهٔ موجودی متوقف نمی‌شه.

bash

ماشین آزمایشی لینوکس؛ همان نشست Bash مجاز

prefix='academybg'
case "$prefix" in ''|[!a-z0-9]*|*[!a-z0-9_-]*) printf 'Use a lowercase Compose prefix\n' >&2; exit 1;; esac
for project in "$prefix-blue" "$prefix-green"; do
  containers=$(docker_lab ps -aq --filter "label=com.docker.compose.project=$project") || exit 1
  networks=$(docker_lab network ls -q --filter "label=com.docker.compose.project=$project") || exit 1
  test -z "$containers$networks" || exit 1
done
listeners=$(ss -H -ltn '( sport = :18081 or sport = :18082 or sport = :18080 )') || exit 1
test -z "$listeners" || exit 1
sudo test ! -e '/srv/academy-bluegreen-demo' || exit 1
sudo test ! -L '/srv/academy-bluegreen-demo' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo/blue' '/srv/academy-bluegreen-demo/green' '/srv/academy-bluegreen-demo/proxy' || exit 1
sudo mkdir -m 0755 -- '/srv/academy-bluegreen-demo/proxy/versions' || exit 1
از کجا بفهمم درست انجام شده؟

در زمان بررسی، هر دو نام پروژه و سه پورت آزاد بودن؛ فقط پوشه و زیرپوشه‌های تازهٔ همین نمونه ساخته شدن.

03

قدم بعدی

فایل‌های نسخه و تنظیم رو آماده کنیم

متن‌های نسخه عمداً فایل‌های ثابتِ همین نمایشن. فقط داخل پوشهٔ تازه می‌سازیمشون و بعد چهار فایل تنظیم رو بدون جایگزینی فایل موجود رزرو می‌کنیم. محتوای هر پنل بعدی رو با sudoedit در مسیر خودش بگذار. اگر ساخت فایل‌ها وسط کار متوقف شد، فایل‌های خالیِ نام‌برده رو بررسی کن؛ کنترل برخورد رو حذف نکن.

این فایل‌ها secret ندارن؛ دادهٔ برنامه، پایگاه داده و تنظیم پراکسی موجود رو وارد نمونه نکن. دو قالب مسیر رو از اول در پوشهٔ پراکسی می‌سازیم تا موقع mount، برچسب خصوصی SELinux بگیرن. بعد برای انتخاب مسیر، از hard link استفاده می‌کنیم؛ یعنی نام دیگری برای همان فایل می‌سازیم و آن نام رو با rename جای active.conf می‌ذاریم. این کار برچسب فایلِ از قبل آماده‌شده رو نگه می‌داره، به‌جای اینکه وقت جابه‌جایی یک فایل تازه و بدون برچسب بسازیم.

bash

ماشین آزمایشی لینوکس؛ همان نشست Bash مجاز

sudo sh -c 'set -euC
printf "BLUE\n" > "$1/blue/release"
printf "BLUE_READY\n" > "$1/blue/healthz"
printf "GREEN\n" > "$1/green/release"
printf "GREEN_READY\n" > "$1/green/healthz"
: > "$1/blue.yml"
: > "$1/green.yml"
: > "$1/proxy/versions/blue.route"
: > "$1/proxy/versions/green.route"
chmod 0644 "$1"/blue/* "$1"/green/* "$1/blue.yml" "$1/green.yml" "$1"/proxy/versions/*
' sh '/srv/academy-bluegreen-demo' || exit 1
sudoedit '/srv/academy-bluegreen-demo/blue.yml' '/srv/academy-bluegreen-demo/green.yml' '/srv/academy-bluegreen-demo/proxy/versions/blue.route' '/srv/academy-bluegreen-demo/proxy/versions/green.route'
از کجا بفهمم درست انجام شده؟

متن‌های نسخه و چهار فایل تنظیم تازه برای محتوای پنل‌های بعدی آماده‌ان.

پروژهٔ blue؛ نسخهٔ قبلی و ورودی مشترک

نسخهٔ blue و پراکسی در یک پروژه هستن، ولی فقط پراکسی به شبکهٔ خارجیِ پروژهٔ green هم وصل می‌شه. نام مشخص blue-app جلوی اشتباه‌گرفتن سرویس‌های هم‌نام در چند شبکه رو می‌گیره. healthcheck پراکسی فقط می‌گه خود Nginx مسیر آمادگی محلیش رو پاسخ می‌ده؛ وابستگی به سلامت blue هم شرط شروعه، نه کنترل دائمیِ مسیر ترافیک.

هر پوشهٔ bindشده فقط در اختیار یک کانتینره و در حالت فعال SELinux برچسب خصوصی Z می‌گیره. پوشه‌ای بین دو نسخه مشترک نیست. Z رو روی پوشه‌های سیستم یا محتوای موجود اعمال نکن. انتشار پورت Docker حتی با bind روی loopback می‌تونه در قواعد فایروال میزبان اثر بذاره؛ از daemon آزمایشیِ مصوب استفاده کن و تنظیم مدیر فایروالش رو دست نزن.

yaml · /srv/academy-bluegreen-demo/blue.yml

محتوای فایل؛ در ترمینال اجرا نکن

services:
  blue:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18081:80"
    volumes:
      - ./blue:/usr/share/nginx/html:ro,Z
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/healthz | grep -qx BLUE_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks:
      default:
        aliases: [blue-app]
  gateway:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18080:80"
    volumes:
      - ./proxy:/etc/nginx/conf.d:ro,Z
    depends_on:
      blue:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/gateway-ready | grep -qx PROXY_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks: [default, green]
networks:
  default: {}
  green:
    external: true
    name: academybg-green_default

پروژهٔ green؛ نسخهٔ تازه روی شبکهٔ خودش

green پروژهٔ Compose، شبکهٔ bridge، پوشهٔ محتوا و پورت loopback جدا داره. healthcheck همون endpoint کوچکِ محلی رو می‌خونه، نه مسیر ورودیِ پراکسی رو. بین این دو پروژه depends_on نداریم؛ بعداً سلامت green رو صریحاً هم از میزبان و هم از شبکهٔ پراکسی می‌سنجیم.

اول با create پروژه و شبکه رو آماده می‌کنیم، بدون اینکه green شروع بشه. این‌طوری پراکسیِ blue می‌تونه به شبکهٔ خارجی وصل بشه، در حالی که هنوز blue پاسخ می‌ده. create نه پایگاه داده راه می‌ندازه و نه مسیر ترافیک رو عوض می‌کنه.

yaml · /srv/academy-bluegreen-demo/green.yml

محتوای فایل؛ در ترمینال اجرا نکن

services:
  green:
    image: nginx:stable-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
    restart: "no"
    ports:
      - "127.0.0.1:18082:80"
    volumes:
      - ./green:/usr/share/nginx/html:ro,Z
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/healthz | grep -qx GREEN_READY"]
      interval: 2s
      timeout: 2s
      retries: 10
    networks:
      default:
        aliases: [green-app]
networks:
  default: {}

مسیر قبلی به blue

فایل فعال اول از این قالب میاد. هدر مسیر نشون می‌ده کدوم تنظیم پراکسی انتخاب شده و پاسخ /release نسخهٔ backend رو مشخص می‌کنه. هیچ‌کدوم تراکنش واقعیِ برنامه نیستن. نام upstream موقع بارگذاری تنظیم Nginx به آدرس تبدیل می‌شه؛ اگر بعداً IP کانتینر عوض بشه، ممکنه reload کنترل‌شدهٔ دیگری لازم باشه.

nginx · /srv/academy-bluegreen-demo/proxy/versions/blue.route

محتوای فایل؛ در ترمینال اجرا نکن

upstream demo_release {
    server blue-app:80;
}
server {
    listen 80;
    location = /gateway-ready {
        default_type text/plain;
        return 200 "PROXY_READY\n";
    }
    location / {
        proxy_pass http://demo_release;
        proxy_set_header Host $host;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        add_header X-Demo-Route blue always;
    }
}

مسیر پیشنهادی به green

فقط upstream و برچسب نمایشیِ مسیر فرق دارن. قالب‌ها با پسوند .route داخل versions می‌مونن و وارد الگوی *.conf سطح بالای Nginx نمی‌شن. فایل server دوم یا فایل پشتیبان ویرایشگر با پسوند .conf در پوشهٔ بارگذاری‌شده نگذار.

nginx · /srv/academy-bluegreen-demo/proxy/versions/green.route

محتوای فایل؛ در ترمینال اجرا نکن

upstream demo_release {
    server green-app:80;
}
server {
    listen 80;
    location = /gateway-ready {
        default_type text/plain;
        return 200 "PROXY_READY\n";
    }
    location / {
        proxy_pass http://demo_release;
        proxy_set_header Host $host;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        add_header X-Demo-Route green always;
    }
}
همهٔ آموزش‌های آکادمی ↗