← شروع آموزش

WIUNIX ACADEMY

راه‌اندازی رجیستری خصوصی Docker با HTTPS و Nginx

صفحهٔ 2 از 5؛ مراحل قبلی و پیش‌نیازها را پیش از ادامه مرور کن.

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

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

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

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

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

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

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

آی‌پی، دامنه یا پورت مثال‌ها با محیط تو فرق دارد؟ دکمهٔ زیر را باز کن، مقادیر را تغییر بده و «اعمال در آموزش» را بزن؛ سپس دستورهای به‌روز‌شده را کپی کن.

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

قدم بعدی

فایل bcrypt بومی رجیستری را آماده کنید

رمز از stdin به htpasswd می‌رسد و در argv قرار نمی‌گیرد. خروجی bcrypt مستقیم در فایل تازه و محافظت‌شده نوشته می‌شود؛ رمز و هش چاپ نمی‌شوند. ورودی خالی یا نامعتبر متوقف می‌شود. حذف فایل موقت تضمین پاک‌شدن فیزیکی از SSD یا اسنپشات نیست. رجیستری فایل کاربران را هنگام شروع می‌خواند؛ تعویض رمز به restart کنترل‌شده نیاز دارد.

bash

VM رجیستری

sudo python3 - <<'PY'
import os, stat, subprocess
from pathlib import Path
p=Path('/srv/academy-registry/auth/password-input')
s=p.lstat(); assert stat.S_ISREG(s.st_mode) and s.st_uid==0 and stat.S_IMODE(s.st_mode)==0o600
secret=p.read_bytes(); assert secret.endswith(b'\n') and secret.count(b'\n')==1
assert 20 <= len(secret.rstrip(b'\n')) <= 72, 'Use 20..72 ASCII bytes'
assert all(33<=c<=126 for c in secret.rstrip(b'\n')), 'Printable ASCII, no spaces'
result=subprocess.run(['htpasswd','-nBi','-C','12','academy-publisher'],input=secret,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True)
assert result.stdout.startswith(b'academy-publisher:$2')
f=Path('/srv/academy-registry/auth/htpasswd')
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o640)
with os.fdopen(fd,'wb') as out: out.write(result.stdout)
os.chown(f,0,23153); os.chmod(f,0o640)
p.unlink()
print('bcrypt file created; plaintext input removed (not a secure-erasure guarantee)')
PY
از کجا بدانم درست انجام شد؟

فایل bcrypt با مجوز 0640 ساخته و ورودی موقت حذف می‌شود.

05

قدم بعدی

فایل‌های پیکربندی را در ویرایشگر باز کنید

دو فایل تازه روی VM باز می‌شوند. دو پنل بعدی محتوای فایل‌اند و باید در فایل متناظر ذخیره شوند؛ فرمان ترمینال را داخل YAML ننویسید. secret امضای وضعیت آپلود در گام بعد، بدون نمایش، اضافه خواهد شد.

bash

VM رجیستری

sudo python3 - <<'PY'
import os
for name in ('config/config.yml','config/nginx.conf'):
    f='/srv/academy-registry/'+name
    fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
    os.close(fd)
PY
sudo nano '/srv/academy-registry/config/config.yml' '/srv/academy-registry/config/nginx.conf'
از کجا بدانم درست انجام شد؟

محتوای هر دو پنل در فایل درست ذخیره می‌شود.

فایل تنظیمات Distribution

بک‌اند داخل شبکهٔ کانتینر گوش می‌دهد و فقط نگاشت loopback میزبان منتشر می‌شود. host ثابت باعث می‌شود نشانی ادامهٔ آپلود به دامنهٔ HTTPS عمومی اشاره کند. حذف و پاک‌سازی خودکار آپلود ناقص در این پایلوت خاموش است؛ فضای مصرف‌شده را پایش کنید.

yaml · /srv/academy-registry/config/config.yml

VM رجیستری

version: 0.1
log:
  level: error
storage:
  filesystem:
    rootdirectory: /var/lib/registry
  delete:
    enabled: false
  maintenance:
    uploadpurging:
      enabled: false
    readonly:
      enabled: false
http:
  addr: 0.0.0.0:5000
  host: https://registry.example.net
  relativeurls: false
  draintimeout: 60s
auth:
  htpasswd:
    realm: academy-registry
    path: /auth/htpasswd
از کجا بدانم درست انجام شد؟

در نام فایل نمایش‌داده‌شده ذخیره کنید؛ تست syntax و شروع سرویس در گام‌های بعد استفادهٔ آن را بررسی می‌کند.

فایل پراکسی HTTPS

احراز هویت فقط در Distribution انجام می‌شود؛ Nginx هدر Authorization را عبور می‌دهد، هدرهای اعتماد کلاینت را جایگزین می‌کند و Host دیگر را رد می‌کند. اگر یک پراکسی دیگر جلوی آن قرار می‌دهید، مرز اعتماد IP را جدا طراحی کنید.

حد 1024 مگابایت برای هر درخواست HTTP است، نه مجموع اندازهٔ یک ایمیج چندلایه. خاموش‌بودن buffering همراه HTTP/1.1 برای ارسال chunked مهم است. مقدار 900 ثانیه زمان بی‌کاری است، نه مهلت کل انتقال. آپلود قطع‌شده را با کلاینت دوباره امتحان کنید؛ پراکسی نباید بدنهٔ نیمه‌ارسال‌شده را خودکار به بک‌اند دیگری بفرستد.

nginx · /srv/academy-registry/config/nginx.conf

VM رجیستری

server {
    listen 443 ssl;
    server_name registry.example.net;
    if ($host != registry.example.net) { return 421; }
    ssl_certificate /srv/academy-registry/tls/fullchain.pem;
    ssl_certificate_key /srv/academy-registry/tls/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    client_max_body_size 1024m;
    client_body_timeout 900s;
    access_log off;
    error_log /var/log/nginx/academy-registry-error.log error;
    location /v2/ {
        proxy_pass http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header Host registry.example.net;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host registry.example.net;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Forwarded "";
        proxy_set_header Connection "";
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_connect_timeout 5s;
        proxy_read_timeout 900s;
        proxy_send_timeout 900s;
        proxy_next_upstream off;
    }
    location / { return 404; }
}
از کجا بدانم درست انجام شد؟

در نام فایل نمایش‌داده‌شده ذخیره کنید؛ تست syntax و شروع سرویس در گام‌های بعد استفادهٔ آن را بررسی می‌کند.

06

قدم بعدی

secret داخلی آپلود را بدون نمایش اضافه کنید

پس از ذخیرهٔ YAML فقط یک بار اجرا کنید. مقدار تصادفی مستقیم وارد فایل محافظت‌شده می‌شود و چاپ نمی‌شود. این فایل باید همراه بکاپ تنظیمات نگهداری شود. تغییر secret وضعیت آپلودهای در جریان را نامعتبر می‌کند؛ آن را در لاگ یا Git قرار ندهید.

bash

VM رجیستری

sudo python3 - <<'PY'
import os, secrets
from pathlib import Path
p=Path('/srv/academy-registry/config/config.yml')
text=p.read_text(encoding='utf-8')
assert text.count('http:\n')==1 and '  secret:' not in text
text=text.replace('http:\n','http:\n  secret: '+secrets.token_hex(32)+'\n',1)
p.write_text(text,encoding='utf-8')
os.chown(p,0,23153); os.chmod(p,0o640)
print('Persistent upload-state signing secret generated privately')
PY
از کجا بدانم درست انجام شد؟

secret پایدار در فایل 0640 ثبت می‌شود و در خروجی دیده نمی‌شود.

07

قدم بعدی

تصویر پین‌شده را فقط روی loopback اجرا کنید

بازبینی ۲۰۲۶-۱۰-۲۱ بر پایهٔ Distribution 3.1.2 است. digest فهرست چندمعماری تصویر رسمی از Docker Hub خوانده و دریافت آن بررسی شد. RepoDigests دریافتی را با مرجع کامل پنل مقایسه کنید؛ latest جایگزین آن نیست. پین‌بودن، به‌روزرسانی امنیتی خودکار ایجاد نمی‌کند.

پردازش با شناسهٔ عددی 23153 اجرا می‌شود، ریشهٔ کانتینر فقط‌خواندنی است و capability و سوکت Docker ندارد. فقط داده‌ها قابل نوشتن‌اند. برای این پایلوت لاگ کانتینر خاموش است تا به‌ویژه رفتار ساخت کاربر در نبود htpasswd رمزی چاپ نکند. قبل از بهره‌برداری، مانیتورینگ محافظت‌شده اضافه کنید. نشانی HTTP محلی فقط برای عیب‌یابی روی همین VM است.

bash

VM رجیستری

set -eu
sudo test -s '/srv/academy-registry/auth/htpasswd'
sudo test -s '/srv/academy-registry/config/config.yml'
sudo docker pull 'docker.io/library/registry:3.1.2@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8'
sudo docker image inspect 'docker.io/library/registry:3.1.2@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8' --format '{{json .RepoDigests}}'
sudo docker run -d --name 'academy-registry' --restart unless-stopped \
  --user '23153:23153' \
  --read-only --cap-drop ALL --security-opt no-new-privileges \
  --log-driver none \
  -p '127.0.0.1:5000:5000' \
  --mount 'type=bind,src=/srv/academy-registry/data,dst=/var/lib/registry' \
  --mount 'type=bind,src=/srv/academy-registry/auth,dst=/auth,readonly' \
  --mount 'type=bind,src=/srv/academy-registry/config,dst=/etc/distribution,readonly' \
  'docker.io/library/registry:3.1.2@sha256:ddf754342cfc8acc51a56d5d0ab6af06826461864460636d8bd5c546dab2a7b8' /etc/distribution/config.yml
ready=no
for attempt in $(seq 1 30); do
  status=$(curl --silent --show-error --max-time 2 --output /dev/null --write-out '%{http_code}' 'http://127.0.0.1:5000/v2/' || true)
  if test "$status" = 401; then ready=yes; break; fi
  sleep 1
done
test "$ready" = yes
printf 'Loopback /v2/: 401\n'
sudo ss -lntp
از کجا بدانم درست انجام شد؟

پاسخ محلی 401 است و نگاشت میزبان فقط روی 127.0.0.1:5000 دیده می‌شود.

همهٔ آموزش‌های آکادمی ↗