WIUNIX ACADEMY
انتشار blue/green با Docker Compose؛ از healthcheck تا برگشت مسیر
دو نسخهٔ بدون state رو در پروژههای جدا بالا میآریم، سلامت نسخهٔ تازه رو از مسیر پراکسی میسنجیم و مسیر رو با امکان برگشت عوض میکنیم؛ بدون ادعای انتشار بیقطعی یا بازیابی داده.
محیطت رو انتخاب کنآموزش متناسب با محیط تو
کجا اجرا میکنی؟
سیستمعامل و نسخهٔ سرورت رو انتخاب کن تا مراحل، دستورها و راهحلهای همون محیط رو ببینی.
آموزش متناسب با مقادیر تو
تنظیمات این آموزش
مقادیر نمونه رو با مشخصات سرورت عوض کن تا متن و دستورها با هم تغییر کنن. رمز، کلید خصوصی و توکن اینجا وارد نکن. مقدارها فقط توی همین زبانه میمونن و به سرور یا لینک اشتراکگذاری فرستاده نمیشن. قبل از اجرا، 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 در مرحلهٔ بعد یعنی توقف، نه اجازهٔ کشتن اون فرایند.
قدم بعدی
Engine و افزونهٔ Compose موجود رو بشناسیم
از یک ماشین مجازی آزمایشی شروع کن که Docker daemon محلی آن از قبل با مجوز تیم و با کاربر root اجرا میشه؛ به این حالت rootful میگیم. نسخهٔ Engine باید ۲۸ یا جدیدتر باشه و افزونهٔ Compose هم --wait و --wait-timeout رو داشته باشه. تابع پایین به socket محلی وصل میشه و نمیگذاره انتخاب context یا میزبان از متغیرهای محیط، مقصد رو عوض کنه. همین نشست Bash رو برای پنلهای بعدی نگه دار. دسترسی به این daemon در حد مدیریت سیستمه؛ این مسیر برای rootless، Docker Desktop، daemon دوردست یا Podman نوشته نشده.
در مسیر rolling که در ۲۰۲۶-۱۱-۰۵ بررسی شده، بستهٔ خود توزیع docker و docker-compose رو تأیید کن. بستهٔ Arch فعلی افزونهٔ CLI رو هم نصب میکنه، ولی همچنان launcher همین docker compose رو بررسی میکنیم، نه نام فایل رو. نسخهٔ بستهٔ Manjaro و شاخهٔ خودش ممکنه فرق کنه؛ بستهٔ Arch رو وارد نکن و برای این تمرین شاخه یا daemon رو عوض نکن. ابزار کمشده فقط در نگهداری مصوب با ارتقای کامل همان توزیع آماده میشه؛ نصب جزئی وسط حادثه نداریم.
ماشین آزمایشی لینوکس؛ همان نشست Bash مجاز
cat /etc/os-release
pacman -Q docker docker-compose 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، نسخهٔ ۲۸ یا جدیدتر و گزینههای لازم افزونه تأیید شدن؛ نصب متفاوت یا پشتیبانینشده یعنی توقف.
قدم بعدی
مالکیت نمونه رو کنترل کنیم و پوشهٔ تازه بسازیم
شرط نام کوچکِ پروژه از فیلد عمومی فرم محدودتره. همهٔ کانتینرها، حتی متوقفشدهها، و شبکههایی رو که برچسب یکی از دو پروژه رو دارن بررسی میکنیم. شکستِ پرسوجوی Docker رو فهرست خالی حساب نمیکنیم. اگر نام برخورد داشت، مقدار نمونه رو عوض کن؛ بار شخص دیگری رو حذف نکن.
یک پوشهٔ تازه با مالکیت root و زیرپوشههای عمومیِ همین نمونه میسازیم. مسیر برنامهٔ موجود رو اینجا نگذار؛ برچسب خصوصی SELinux در ادامه فقط روی همین پوشههای تازه اعمال میشه. سه پورت شنونده رو هم میبینیم، ولی daemon یا برنامهٔ دیگری هنوز میتونه همزمان پورت رو بگیره. کانتینر یا شنوندهٔ موجودی متوقف نمیشه.
ماشین آزمایشی لینوکس؛ همان نشست 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در زمان بررسی، هر دو نام پروژه و سه پورت آزاد بودن؛ فقط پوشه و زیرپوشههای تازهٔ همین نمونه ساخته شدن.
قدم بعدی
فایلهای نسخه و تنظیم رو آماده کنیم
متنهای نسخه عمداً فایلهای ثابتِ همین نمایشن. فقط داخل پوشهٔ تازه میسازیمشون و بعد چهار فایل تنظیم رو بدون جایگزینی فایل موجود رزرو میکنیم. محتوای هر پنل بعدی رو با sudoedit در مسیر خودش بگذار. اگر ساخت فایلها وسط کار متوقف شد، فایلهای خالیِ نامبرده رو بررسی کن؛ کنترل برخورد رو حذف نکن.
این فایلها secret ندارن؛ دادهٔ برنامه، پایگاه داده و تنظیم پراکسی موجود رو وارد نمونه نکن. دو قالب مسیر رو از اول در پوشهٔ پراکسی میسازیم تا موقع mount، برچسب خصوصی SELinux بگیرن. بعد برای انتخاب مسیر، از hard link استفاده میکنیم؛ یعنی نام دیگری برای همان فایل میسازیم و آن نام رو با rename جای active.conf میذاریم. این کار برچسب فایلِ از قبل آمادهشده رو نگه میداره، بهجای اینکه وقت جابهجایی یک فایل تازه و بدون برچسب بسازیم.
ماشین آزمایشی لینوکس؛ همان نشست 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 آزمایشیِ مصوب استفاده کن و تنظیم مدیر فایروالش رو دست نزن.
محتوای فایل؛ در ترمینال اجرا نکن
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 نه پایگاه داده راه میندازه و نه مسیر ترافیک رو عوض میکنه.
محتوای فایل؛ در ترمینال اجرا نکن
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 کنترلشدهٔ دیگری لازم باشه.
محتوای فایل؛ در ترمینال اجرا نکن
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 در پوشهٔ بارگذاریشده نگذار.
محتوای فایل؛ در ترمینال اجرا نکن
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;
}
}