> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# تخزين الكائنات S3 على سيرفرك: التثبيت والبنية
- URL: https://arabroot.io/articles/تخزين-الكائنات-s3-على-سيرفرك-التثبيت-والبنية/
- Published: 2026-10-12T09:15:00.000Z
- Updated: 2026-10-11T11:14:28.000Z
- Description: كيف تثبت ثلاث أدوات تخزين S3 مفتوحة المصدر في إعداد صغير يتحمل الأعطال، وماذا يحدث فعلاً عندما يتوقف سيرفر أو يتلف قرص.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, الاستضافة الذاتية

في مقال [أنواع التخزين وواجهة S3](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-block-%D9%88-file-%D9%88-object-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#self-hosted) تعرفنا على أشهر أدوات تخزين الكائنات التي تعمل على سيرفرك، وشغلنا Garage على عقدة واحدة لنفهم فكرة ال Bucket والمفتاح، ثم رأينا في [S3 من داخل تطبيقك](https://arabroot.io/articles/s3-%D9%85%D9%86-%D8%AF%D8%A7%D8%AE%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B1%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D9%85%D9%88%D9%82%D8%B9%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%83%D9%84%D9%81%D8%A9/) كيف يستخدم الكود هذه الواجهة. وعقدة واحدة لا تحمي شيئاً، فإذا تعطل قرصها ضاعت الملفات، والسؤال الحقيقي عندما تقرر أن تستضيف التخزين بنفسك هو: كيف تبني كل أداة الحماية من الأعطال، وكم سيرفراً تحتاج، وما الذي يحدث فعلاً عندما يتوقف سيرفر أو يتلف قرص، وما الذي لا تدعمه من واجهة S3\. وهذا ما سوف نجربه هنا على Garage وSeaweedFS وMinIO، كل واحد في إعداد صغير لكنه يشبه الإنتاج، ثم نكسره عمداً.

وسوف نناقش في هذا المقال ما يلي:

- Garage: ثلاث عقد في ثلاث مناطق، والنصاب Quorum، وماذا يحدث عندما تتوقف عقدة ثم عقدتان.
- SeaweedFS: البوابة وال Filer وال Master وخوادم ال Volumes كخدمات منفصلة، وقاعدة النسخ 001، وماذا يحدث عندما يتوقف خادم.
- MinIO: لماذا نبنيه من الكود اليوم، وترميز المحو Erasure Coding على أربعة أقراص، وإتلاف قرصين ثم الإصلاح Heal.
- سكربت واحد يسأل كل أداة عن ميزات S3، والفروق التي ظهرت فعلاً، والرخصة Licence لكل أداة.
- أساسيات الأمان والمراقبة والنسخ الاحتياطي المشتركة بين الثلاثة.

أما المقارنة بالأرقام، والتغييرات التي حدثت في MinIO بالتفصيل، ودليل الاختيار، فهي موضوع [أي تخزين S3 مستضاف ذاتياً يناسبك؟](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-s3-%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81-%D8%B0%D8%A7%D8%AA%D9%8A%D8%A7-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/).

## Garage: ثلاث مناطق ونسخة كاملة في كل منطقة

Garage من تطوير مجموعة Deuxfleurs الفرنسية، ومكتوب بلغة Rust، ورخصته AGPL-3.0 كما في [ملف الرخصة](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main-v2/LICENSE?ref=arabroot.io)، وصفحة [أهدافه](https://garagehq.deuxfleurs.fr/documentation/design/goals/?ref=arabroot.io) تقول بوضوح إنه مصمم لعدة مواقع متصلة عبر الإنترنت العادي، مثل مراكز بيانات أو مكاتب أو بيوت، وإن الأداء الأقصى وكثرة الميزات وترميز المحو ليست من أهدافه، فهو يكتفي بنسخ البيانات كاملة. والفكرة الأساسية فيه هي المنطقة Zone: تعطي كل عقدة اسم منطقة، ومع `replication_factor = 3` يضع Garage كل كائن على ثلاث عقد في مناطق مختلفة إن أمكن، وبحسب [توثيق الإعداد](https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/?ref=arabroot.io) يحتاج في الوضع الافتراضي consistent ردين من ثلاثة للكتابة وردين للقراءة، وبهذا يضمن أن ما كتبته تقرؤه مباشرة. والصورة التالية تبين ذلك مع نتائج التجربة:

![تطبيق يرسل PUT إلى أي عقدة، وثلاث عقد Garage في ثلاث مناطق riyadh وjeddah وdammam على كل منها نسخة من الكائن، وتحتها نتيجتان: عند توقف garage3 نجحت الكتابة والقراءة وبصمة md5 نفسها، وعند توقف garage2 أيضاً فشلت الكتابة بالخطأ ServiceUnavailable لأن النصاب 2 لم يتحقق](https://arabroot.io/content/images/2026/10/s3-self-hosted-01-garage.webp)

عقدة متوقفة لا تؤثر، وعقدتان توقفان الكتابة والقراءة حتى تعود إحداهما

### تشغيل ثلاث عقد

كل عقدة تحتاج ملف إعداد، والسر المشترك `rpc_secret` يجب أن يكون نفسه في الثلاثة، لأنه ما يسمح للعقد بالتواصل، والعنوان `rpc_public_addr` هو ما تستخدمه العقد الأخرى للوصول إليها:

```bash
mkdir -p ~/lab-s3servers/garage && cd ~/lab-s3servers/garage
SECRET=$(openssl rand -hex 32); TOKEN=$(openssl rand -base64 32)
for i in 1 2 3; do cat > garage$i.toml <<EOF
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 3

rpc_bind_addr = "[::]:3901"
rpc_public_addr = "lab-s3servers-garage$i:3901"
rpc_secret = "$SECRET"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "$TOKEN"
EOF
done
chmod 600 garage*.toml
```

ثم ملف Docker Compose بثلاث خدمات، والعقدة الأولى وحدها تنشر منفذ S3 على 127.0.0.1:

```yaml
name: lab-s3servers-garage
services:
  garage1:
    image: dxflrs/garage:v2.4.1
    container_name: lab-s3servers-garage1
    volumes: ["./garage1.toml:/etc/garage.toml:ro", "g1:/var/lib/garage"]
    ports: ["127.0.0.1:39001:3900"]
  garage2:
    image: dxflrs/garage:v2.4.1
    container_name: lab-s3servers-garage2
    volumes: ["./garage2.toml:/etc/garage.toml:ro", "g2:/var/lib/garage"]
  garage3:
    image: dxflrs/garage:v2.4.1
    container_name: lab-s3servers-garage3
    volumes: ["./garage3.toml:/etc/garage.toml:ro", "g3:/var/lib/garage"]
volumes: { g1: {}, g2: {}, g3: {} }
networks:
  default: { name: lab-s3servers-net }
```

نشغلها، ثم نعرف كل عقدة بالأخرى بمعرفها، لأن العقد لا تكتشف بعضها وحدها:

```bash
docker compose up -d
g() { docker exec lab-s3servers-garage$1 /garage "${@:2}" 2>&1 | grep -v INFO; }
g 1 node connect "$(g 2 node id -q)"
g 1 node connect "$(g 3 node id -q)"
g 1 status
```

والمخرج سوف يكون كما يلي:

```text
Success.
Success.
==== HEALTHY NODES ====
ID                Hostname      Address            Tags  Zone  Capacity          DataAvail  Version
979cc95a0f25848c  ?             192.168.48.4:3901              NO ROLE ASSIGNED
dfdb128c1838027d  4aeb27944388  192.168.48.3:3901              NO ROLE ASSIGNED             v2.4.1
e937c632dd949740  ?             192.168.48.2:3901              NO ROLE ASSIGNED
```

العقد متصلة ولكنها دون دور NO ROLE ASSIGNED، فلا تخزن شيئاً بعد، ونعطيها الأدوار في تخطيط ال Cluster Layout: لكل عقدة منطقة وسعة ووسم، ثم نطبق التخطيط برقم إصداره:

```bash
g 1 layout assign dfdb128c1838027d -z riyadh -c 10G -t g1
g 1 layout assign 979cc95a0f25848c -z jeddah -c 10G -t g2
g 1 layout assign e937c632dd949740 -z dammam -c 10G -t g3
g 1 layout apply --version 1
g 1 status
```

والمخرج سوف يكون كما يلي، بعد اختصار جدول الأقسام:

```text
==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====

Partitions are replicated 3 times on at least 3 distinct zones.

Optimal partition size:                     37.3 MiB
Usable capacity / total cluster capacity:   27.9 GiB / 27.9 GiB (100.0 %)
Effective capacity (replication factor 3):  9.3 GiB
...
New cluster layout with updated role assignment has been applied in cluster.
Data will now be moved around between nodes accordingly.
==== HEALTHY NODES ====
ID                Hostname      Address            Tags  Zone    Capacity  DataAvail          Version
979cc95a0f25848c  1ae43995db71  192.168.48.4:3901  [g2]  jeddah  9.3 GiB   840.5 GiB (83.5%)  v2.4.1
dfdb128c1838027d  4aeb27944388  192.168.48.3:3901  [g1]  riyadh  9.3 GiB   840.5 GiB (83.5%)  v2.4.1
e937c632dd949740  a998b4d1f936  192.168.48.2:3901  [g3]  dammam  9.3 GiB   840.5 GiB (83.5%)  v2.4.1
```

في المخرج أعلاه لاحظ التالي:

- البيانات مقسمة إلى 256 قسماً Partition، وكل قسم منسوخ ثلاث مرات على ثلاث مناطق مختلفة، كما قال السطر Partitions are replicated 3 times on at least 3 distinct zones.
- السعة الفعلية 9.3 GiB من 27.9 GiB، لأن كل بايت يحفظ ثلاث مرات، فاحسب ذلك عندما تشتري الأقراص.
- كل تغيير في التخطيط يجهز أولاً ثم يطبق برقم الإصدار التالي، والتطبيق ينقل البيانات بين العقد، فلا تطبقه في وقت الذروة إذا كان ال Cluster ممتلئاً.

### Bucket ومفتاح، ثم نوقف عقدة ثم عقدتين

```bash
g 1 bucket create media
g 1 key create media-app
g 1 bucket allow --read --write media --key media-app
```

والمخرج سوف يكون كما يلي، وقد اختصرنا المفتاح السري:

```text
Key ID:              GK881e53fc5126df0b15f57bdf
Secret key:          9bae69fb…(مختصر)
==== KEYS FOR THIS BUCKET ====
Permissions  Access key                             Local aliases
RW           GK881e53fc5126df0b15f57bdf  media-app
```

نضع المفتاح في ملف بيئة، ونشغل Container فيه aws CLI على الشبكة نفسها، وهو ال Image التي بنيناها في [مقال S3 من داخل تطبيقك](https://arabroot.io/articles/s3-%D9%85%D9%86-%D8%AF%D8%A7%D8%AE%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B1%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D9%85%D9%88%D9%82%D8%B9%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%83%D9%84%D9%81%D8%A9/#lab-start)، ثم نرفع ملفاً من 20 MiB، ونوقف garage3، ونكتب ونقرأ:

```bash
cd ~/lab-s3servers
cat > garage.env <<'EOF'
AWS_ACCESS_KEY_ID=GK881e53fc5126df0b15f57bdf
AWS_SECRET_ACCESS_KEY=<the secret key>
AWS_DEFAULT_REGION=garage
AWS_ENDPOINT_URL=http://lab-s3servers-garage1:3900
EOF
docker run -d --name lab-s3servers-tools --network lab-s3servers-net -v "$PWD:/lab" lab/s3-toolbox:1
gs() { docker exec --env-file garage.env lab-s3servers-tools sh -c "$1"; }

gs 'head -c 20M /dev/urandom > /tmp/video.mp4; aws s3 cp /tmp/video.mp4 s3://media/videos/intro.mp4 --only-show-errors; aws s3 ls s3://media --recursive'
docker stop lab-s3servers-garage3
gs 'md5sum /tmp/video.mp4; echo hello > /tmp/n.txt; aws s3 cp /tmp/n.txt s3://media/notes/n1.txt --only-show-errors && aws s3 cp s3://media/videos/intro.mp4 /tmp/back.mp4 --only-show-errors && md5sum /tmp/back.mp4'
```

والمخرج سوف يكون كما يلي:

```text
2026-10-11 06:51:49   20971520 videos/intro.mp4
adc759211f03f6f4e4b638f021ee0fb0  /tmp/video.mp4
adc759211f03f6f4e4b638f021ee0fb0  /tmp/back.mp4
```

الكتابة نجحت والملف رجع بالبصمة نفسها مع عقدة متوقفة. والآن نوقف garage2 أيضاً، ونجعل aws CLI يحاول مرة واحدة فقط حتى نرى الخطأ بدل أن يعيد المحاولة:

```bash
docker stop lab-s3servers-garage2
gs 'AWS_MAX_ATTEMPTS=1 aws s3 cp /tmp/n.txt s3://media/notes/n2.txt'
gs 'AWS_MAX_ATTEMPTS=1 aws s3 cp s3://media/videos/intro.mp4 /tmp/back2.mp4'
```

```text
upload failed: ../tmp/n.txt to s3://media/notes/n2.txt An error occurred (ServiceUnavailable) when calling the PutObject operation (reached max retries: 0): Internal error: Could not reach quorum of 2 (sets=None). 1 of 3 request succeeded, others returned errors: ["Network error: Not connected: 979cc95a0f25848c", "Network error: Not connected: e937c632dd949740"]
fatal error: An error occurred (503) when calling the HeadObject operation (reached max retries: 0): Service Unavailable
```

الرسالة تشرح نفسها: العقدة الباقية كتبت عندها، ولكن النصاب اثنان، فرفضت الكتابة كلها بدل أن تقبل كتابة لا تضمن قراءتها لاحقاً، والقراءة رفضت للسبب نفسه. وقد يتساءل البعض: هل أستطيع أن أقرأ ملفاتي في هذه الحالة؟ والإجابة نعم إذا غيرت `consistency_mode` إلى degraded، فيصبح نصاب القراءة واحداً، أو dangerous فيصبح نصاب الكتابة واحداً أيضاً، وكلاهما حل طوارئ مؤقت كما يحذر التوثيق، والسبب أنك تتنازل عن ضمان أن القراءة ترى آخر كتابة. وعندما أعدنا تشغيل العقدتين بالأمر `docker start` عادتا إلى قائمة HEALTHY NODES دون أي تدخل.

وفي التجربة استخدمت العقد الثلاث بعد الاختبار 32.76 MiB و5.3 MiB و7.3 MiB من الذاكرة بحسب `docker stats`، وهذا ما يجعل Garage مناسباً لأجهزة صغيرة في مواقع متفرقة. ولاحظ أن منفذ الإدارة 3903 ومنفذ التواصل 3901 لم ننشرهما، فالأول يحمي بتوكن الإدارة، والثاني بالسر المشترك، وكلاهما لا يجب أن يصل إليه أحد من الإنترنت، وبين المواقع الحقيقية ضعهما داخل VPN مثل [WireGuard](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AE%D8%A7%D8%B5%D8%A9-%D9%85%D8%B9-wireguard-%D9%88-wg-easy/).

## SeaweedFS: البوابة والفهرس والخريطة والأقراص

SeaweedFS مكتوب بلغة Go، ورخصته Apache-2.0 كما في [مستودعه](https://github.com/seaweedfs/seaweedfs?ref=arabroot.io)، وله نسخة تجارية Enterprise فيها ميزات إضافية مثل الإصلاح الذاتي وترميز المحو القابل للتخصيص بحسب الملف نفسه. وبحسب [وصف البنية](https://github.com/seaweedfs/seaweedfs?ref=arabroot.io#architecture) فهو أربعة أدوار: خادم ال Master يعرف أي Volume على أي خادم ويعطي معرفات الملفات، ولا يقع في مسار القراءة، وخوادم ال Volumes تحفظ الكتل في ملفات كبيرة تضيف إليها فقط، وال Filer يضيف الأسماء والمجلدات وقوائم الأجزاء ويحفظها في قاعدة بيانات تختارها، وبوابة S3 تتكلم S3 وتتحقق من المفاتيح ولا تحفظ حالة، والصورة التالية تبين ذلك مع ما رأيناه في التجربة:

![التطبيق يتصل ببوابة S3 على المنفذ 8333، والبوابة تتصل بال Filer الذي يحفظ الأسماء وقوائم الأجزاء، وال Filer يسأل ال Master عن مكان الكتابة، والملف intro.mp4 بحجم 20 MiB مقسم إلى خمسة أجزاء يكتب كل منها على خادمين volume1 وvolume2 بقاعدة 001، وvolume2 بإطار أحمر متقطع لأنه أوقف في التجربة: القراءة نجحت والكتابة رفضت](https://arabroot.io/content/images/2026/10/s3-self-hosted-02-seaweedfs.webp)

كل دور خدمة مستقلة، ولذلك تستطيع أن تزيد البوابات أو خوادم الأقراص وحدها

### تشغيل كل دور في Container

نبدأ بملف الهويات، فبدونه تقبل البوابة أي مفتاح كما رأينا في [تجربة المقال الأول](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-block-%D9%88-file-%D9%88-object-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#lab-versioning):

```bash
mkdir -p ~/lab-s3servers/seaweed && cd ~/lab-s3servers/seaweed
cat > s3.json <<'EOF'
{
  "identities": [
    {
      "name": "admin",
      "credentials": [{ "accessKey": "SWADMIN00001", "secretKey": "sw-admin-secret-change-me" }],
      "actions": ["Admin", "Read", "List", "Tagging", "Write"]
    },
    {
      "name": "media-app",
      "credentials": [{ "accessKey": "SWMEDIA00001", "secretKey": "sw-media-secret-change-me" }],
      "actions": ["Read:media", "Write:media", "List:media"]
    }
  ]
}
EOF
chmod 600 s3.json
```

ثم Compose بخمس خدمات: Master، وخادما Volumes في الرف نفسه، وFiler، وبوابة S3، وهي النسخة المبسطة من [ملف Compose الرسمي](https://github.com/seaweedfs/seaweedfs/blob/master/docker/seaweedfs-compose.yml?ref=arabroot.io) مع إصدار مثبت وقاعدة نسخ:

```yaml
name: lab-s3servers-seaweed
x-weed: &weed
  image: chrislusf/seaweedfs:4.48
  restart: unless-stopped
services:
  master:
    <<: *weed
    container_name: lab-s3servers-sw-master
    command: master -ip=lab-s3servers-sw-master -ip.bind=0.0.0.0 -mdir=/data -defaultReplication=001 -volumeSizeLimitMB=1024
    volumes: ["master:/data"]
  volume1:
    <<: *weed
    container_name: lab-s3servers-sw-volume1
    command: volume -ip=lab-s3servers-sw-volume1 -ip.bind=0.0.0.0 -port=8080 -master=lab-s3servers-sw-master:9333 -dir=/data -max=20 -dataCenter=dc1 -rack=rack1
    volumes: ["vol1:/data"]
    depends_on: [master]
  volume2:
    <<: *weed
    container_name: lab-s3servers-sw-volume2
    command: volume -ip=lab-s3servers-sw-volume2 -ip.bind=0.0.0.0 -port=8080 -master=lab-s3servers-sw-master:9333 -dir=/data -max=20 -dataCenter=dc1 -rack=rack1
    volumes: ["vol2:/data"]
    depends_on: [master]
  filer:
    <<: *weed
    container_name: lab-s3servers-sw-filer
    command: filer -ip=lab-s3servers-sw-filer -ip.bind=0.0.0.0 -master=lab-s3servers-sw-master:9333
    volumes: ["filer:/data"]
    depends_on: [volume1, volume2]
  s3:
    <<: *weed
    container_name: lab-s3servers-sw-s3
    command: s3 -filer=lab-s3servers-sw-filer:8888 -ip.bind=0.0.0.0 -config=/etc/seaweedfs/s3.json
    volumes: ["./s3.json:/etc/seaweedfs/s3.json:ro"]
    ports: ["127.0.0.1:38333:8333"]
    depends_on: [filer]
volumes: { master: {}, vol1: {}, vol2: {}, filer: {} }
networks:
  default: { name: lab-s3servers-net, external: true }
```

في الإعداد أعلاه لاحظ التالي:

- القاعدة `-defaultReplication=001` مكونة من ثلاثة أرقام: نسخ في مراكز بيانات أخرى، ثم في رفوف أخرى، ثم في خوادم أخرى في الرف نفسه، فالقيمة 001 تعني نسخة إضافية واحدة على خادم آخر في الرف نفسه، كما في [توثيق النسخ](https://github.com/seaweedfs/seaweedfs/wiki/Replication?ref=arabroot.io).
- الخيار `-volumeSizeLimitMB=1024` يصغر حجم ال Volume الواحد من القيمة الافتراضية حتى يكفي القرص الصغير في التجربة.
- البوابة وحدها منشورة على 127.0.0.1، أما المنافذ 9333 و8080 و8888 فداخل الشبكة فقط، لأنها لا تتحقق من مفاتيح S3.

نشغلها، وننشئ ال Bucket بالمدير، ونرفع بمفتاح التطبيق، ثم نسأل ال Master أين ذهبت البيانات بأداة `weed shell`:

```bash
docker compose up -d
cd ~/lab-s3servers
printf 'AWS_ACCESS_KEY_ID=SWADMIN00001\nAWS_SECRET_ACCESS_KEY=sw-admin-secret-change-me\nAWS_DEFAULT_REGION=us-east-1\nAWS_ENDPOINT_URL=http://lab-s3servers-sw-s3:8333\n' > sw-admin.env
printf 'AWS_ACCESS_KEY_ID=SWMEDIA00001\nAWS_SECRET_ACCESS_KEY=sw-media-secret-change-me\nAWS_DEFAULT_REGION=us-east-1\nAWS_ENDPOINT_URL=http://lab-s3servers-sw-s3:8333\n' > sw-media.env
docker exec --env-file sw-admin.env lab-s3servers-tools aws s3 mb s3://media
docker exec --env-file sw-media.env lab-s3servers-tools sh -c 'head -c 20M /dev/urandom > /tmp/video.mp4; md5sum /tmp/video.mp4; aws s3 cp /tmp/video.mp4 s3://media/videos/intro.mp4 --only-show-errors; aws s3 ls s3://media --recursive'
echo "volume.list" | docker exec -i lab-s3servers-sw-master weed shell -master=lab-s3servers-sw-master:9333
```

والمخرج سوف يكون كما يلي، بعد حذف سطور السجل وبعض ال Volumes الفارغة:

```text
make_bucket: media
ee8af303819f92d7f0c55f76fff52cf0  /tmp/video.mp4
2026-10-11 06:59:18   20971520 videos/intro.mp4
Topology volumeSizeLimit:1024 MB hdd(volume:12/40 active:12 free:28 remote:0)
  DataCenter dc1 hdd(volume:12/40 active:12 free:28 remote:0)
    Rack rack1 hdd(volume:12/40 active:12 free:28 remote:0)
      DataNode lab-s3servers-sw-volume1:8080 hdd(volume:6/20 active:6 free:14 remote:0)
        Disk hdd(volume:6/20 active:6 free:14 remote:0) id:0
          volume Id:1, Size:12583064, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:3, DeleteCount:0, DeletedByteCount:0, ReadOnly:false, ModifiedAtSecond:1791701958
          volume Id:2, Size:4194360, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:1, DeleteCount:0, DeletedByteCount:0, ReadOnly:false, ModifiedAtSecond:1791701958
          volume Id:3, Size:4194360, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:1, DeleteCount:0, DeletedByteCount:0, ReadOnly:false, ModifiedAtSecond:1791701958
        ...
      DataNode lab-s3servers-sw-volume1:8080 {Size:20971808 FileCount:5 DeletedFileCount:0 DeletedBytes:0}
      DataNode lab-s3servers-sw-volume2:8080 hdd(volume:6/20 active:6 free:14 remote:0)
          volume Id:1, Size:12583064, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:3, ...
          volume Id:2, Size:4194360, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:1, ...
          volume Id:3, Size:4194360, ReplicaPlacement:001, Collection:media, Version:3, Ttl:, FileCount:1, ...
        ...
      DataNode lab-s3servers-sw-volume2:8080 {Size:20971808 FileCount:5 DeletedFileCount:0 DeletedBytes:0}
total size:41943616 file_count:10
```

في المخرج أعلاه لاحظ التالي:

- الملف من 20 MiB أصبح خمسة أجزاء FileCount:5 بحجم 4 MiB تقريباً، موزعة على ثلاثة Volumes، والبوابة هي التي قسمته.
- ال Volumes نفسها برقمها نفسه موجودة على الخادمين، فالنسخ في SeaweedFS على مستوى ال Volume كله وليس على مستوى الملف، والمجموع 41,943,616 بايت لملف واحد من 20 MiB.
- ال Bucket أصبح مجموعة Collection باسم media، فكل Bucket له Volumes خاصة به، ولذلك أنشأ ستة Volumes دفعة واحدة.

### نوقف خادماً

```bash
docker stop lab-s3servers-sw-volume2
docker exec --env-file sw-media.env lab-s3servers-tools sh -c 'aws s3 cp s3://media/videos/intro.mp4 /tmp/back.mp4 --only-show-errors && md5sum /tmp/back.mp4; echo hi > /tmp/n.txt; AWS_MAX_ATTEMPTS=1 aws s3 cp /tmp/n.txt s3://media/notes/n1.txt'
docker logs lab-s3servers-sw-s3 2>&1 | grep "chunked upload failed" | tail -1
docker logs lab-s3servers-sw-master 2>&1 | grep "not enough" | tail -1
```

والمخرج سوف يكون كما يلي:

```text
ee8af303819f92d7f0c55f76fff52cf0  /tmp/back.mp4
upload failed: ../tmp/n.txt to s3://media/notes/n1.txt An error occurred (InternalError) when calling the PutObject operation (reached max retries: 0): We encountered an internal error, please try again.
E1011 07:00:27.942810 upload_chunked.go:281 chunked upload failed: assign volume: assign volume: assign volume: rpc error: code = ResourceExhausted desc = no writable volumes for {"collection":"media","replication":{"node":1},"ttl":{"Count":0,"Unit":0},"version":3}, volume growth in progress (returning 0 partial chunks for cleanup)
dc1:Only has 0 racks with more than 2 free data nodes, not enough for 1.
```

القراءة نجحت من النسخة الباقية بالبصمة نفسها، أما الكتابة فرفضت، والعميل لم ير إلا InternalError، والسبب الحقيقي في سجل البوابة وال Master: القاعدة 001 تحتاج خادمين، ولا يوجد إلا خادم واحد يقبل الكتابة، وهذا بالضبط ما يقوله [توثيق النسخ](https://github.com/seaweedfs/seaweedfs/wiki/Replication?ref=arabroot.io): القراءة ممكنة ما دامت نسخة واحدة متاحة، والكتابة لا تسمح إلا إذا أمكن تحقيق عدد النسخ المطلوب. وهذا فرق مهم عن Garage، فالكتابة هنا تتوقف عند تعطل خادم واحد من اثنين، لذلك في الإنتاج اجعل عدد الخوادم أكبر من عدد النسخ، فمع 001 تحتاج ثلاثة خوادم على الأقل حتى تبقى الكتابة ممكنة إذا تعطل واحد. وعندما أعدنا تشغيل volume2 قبلت الكتابة مباشرة.

⚠️

عند تشغيل البوابة بملف الهويات طبعت في سجلها السطر Failed to load IAM configuration: no signing key found for STS service، ومع ذلك عملت المفاتيح والصلاحيات في التجربة، لأن الرسالة تخص خدمة التوكنات المؤقتة STS وليس المفاتيح الثابتة. لا تتجاهل سجل البوابة عند أول تشغيل، واقرأ كل خطأ فيه، ولكن لا تفترض أن كل خطأ يعني أن المفاتيح لا تعمل، وجرب مفتاحاً خاطئاً لتتأكد أنه يرفض.

وبعد الاختبار كانت الذاكرة بحسب `docker stats` للبوابة 162.8 MiB، وال Filer 66.9 MiB، وال Master 66.7 MiB، وخادمي ال Volumes 126.9 MiB و84 MiB، أي حوالي نصف GiB للإعداد كله.

## MinIO: من الكود، وعلى أربعة أقراص

كان MinIO لسنوات أسهل طريق إلى S3 على سيرفرك، ورخصته AGPL-3.0، ولكن [مستودعه](https://github.com/minio/minio?ref=arabroot.io) اليوم مؤرشف للقراءة فقط، وملف README يقول إنه لم يعد يصان THIS REPOSITORY IS NO LONGER MAINTAINED، وإن النسخة المجتمعية توزع ككود مصدري فقط، وتاريخ هذه التغييرات وما يعنيه لك فصلناه في [مقال المقارنة](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-s3-%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81-%D8%B0%D8%A7%D8%AA%D9%8A%D8%A7-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/#minio-changes). والنتيجة العملية للتثبيت أنك لن تجد Docker Image رسمية تسحبها، فعندما حاولنا في 11 أكتوبر 2026 سحب `minio/minio` من Docker Hub و`quay.io/minio/minio` رفض السجلان الطلب، لذلك نبني آخر إصدار مجتمعي RELEASE.2025-10-15T17-29-55Z من الكود كما يطلب README، وهو إصدار لإصلاح ثغرة كما في [ملاحظات الإصدار](https://github.com/minio/minio/releases/tag/RELEASE.2025-10-15T17-29-55Z?ref=arabroot.io):

```bash
mkdir -p ~/lab-s3servers/minio && cd ~/lab-s3servers/minio
cat > Dockerfile <<'EOF'
# Build the last community release of MinIO from source, as its README asks (no official image any more)
FROM golang:1.24-bookworm AS build
ARG MINIO_TAG=RELEASE.2025-10-15T17-29-55Z
RUN CGO_ENABLED=0 go install -v github.com/minio/minio@${MINIO_TAG} && go install github.com/minio/mc@latest

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
COPY --from=build /go/bin/minio /go/bin/mc /usr/local/bin/
EXPOSE 9000 9001
ENTRYPOINT ["minio"]
EOF
docker build -t lab/minio:RELEASE.2025-10-15T17-29-55Z .
```

البناء استغرق حوالي 15 دقيقة على جهاز التجربة، والسبب أن Go يبني MinIO ومكتباته كلها. ثم نشغله على أربعة أقراص، وكل قرص هنا Volume منفصل:

```yaml
name: lab-s3servers-minio
services:
  minio:
    image: lab/minio:RELEASE.2025-10-15T17-29-55Z
    container_name: lab-s3servers-minio
    command: server /data1 /data2 /data3 /data4 --console-address :9001
    environment:
      MINIO_ROOT_USER: minioadmin-lab
      MINIO_ROOT_PASSWORD: change-me-long-password-01
    volumes: ["d1:/data1", "d2:/data2", "d3:/data3", "d4:/data4"]
    ports: ["127.0.0.1:39000:9000", "127.0.0.1:39090:9001"]
volumes: { d1: {}, d2: {}, d3: {}, d4: {} }
networks:
  default: { name: lab-s3servers-net, external: true }
```

```bash
docker compose up -d
docker logs lab-s3servers-minio 2>&1 | head -7
```

```text
INFO: Formatting 1st pool, 1 set(s), 4 drives per set.
INFO: WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.
MinIO Object Storage Server
Copyright: 2015-2026 MinIO, Inc.
License: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html
Version: DEVELOPMENT.GOGET (go1.24.13 linux/amd64)
```

لاحظ التحذير في السطر الثاني: الأقراص الأربعة في جهاز واحد، فإذا تعطل الجهاز توقف كل شيء، وهذا صحيح، فما سوف نراه حماية من تلف الأقراص وليس من تعطل السيرفر. ولاحظ أيضاً أن الإصدار يظهر DEVELOPMENT.GOGET، لأن البناء بالأمر go install لا يضع رقم الإصدار في الملف التنفيذي، فاحفظ رقم الإصدار الذي بنيته في اسم ال Image كما فعلنا.

### ترميز المحو: نتلف قرصين ثم نصلحهما

MinIO لا ينسخ الملف كاملاً، وإنما يقسمه إلى أجزاء بيانات وأجزاء تماثل Parity موزعة على أقراص مجموعة المحو Erasure Set، فإذا ضاع قرص أعاد حساب ما عليه من الباقي، والصورة التالية تبين ما حدث في التجربة:

![ثلاث مراحل: كتابة ملف من 20 MiB يقسم إلى جزأي بيانات D1 وD2 وجزأي تماثل P1 وP2 على أربعة أقراص data1 إلى data4 بحجم 11M لكل قرص، ثم حذف بيانات قرصين فتبقى D1 وD2 والقراءة تنجح بالبصمة نفسها، ثم الأمر mc admin heal يعيد بناء الجزأين ويظهر Healed 1/1 objects](https://arabroot.io/content/images/2026/10/s3-self-hosted-03-minio.webp)

مع EC:2 على أربعة أقراص يكفي أي قرصين للقراءة، والإصلاح يعيد بناء الباقي

```bash
m() { docker exec lab-s3servers-minio mc "$@"; }
m alias set lab http://127.0.0.1:9000 minioadmin-lab change-me-long-password-01
m admin info lab
m mb lab/media
docker exec lab-s3servers-minio sh -c 'head -c 20M /dev/urandom > /tmp/video.mp4; md5sum /tmp/video.mp4'
m cp -q /tmp/video.mp4 lab/media/videos/intro.mp4
docker exec lab-s3servers-minio sh -c 'ls /data3/media/videos/intro.mp4/*/; du -sh /data1/media /data2/media /data3/media /data4/media'
```

والمخرج سوف يكون كما يلي، بعد حذف سطر التسجيل:

```text
●  127.0.0.1:9000
   Uptime: 43 seconds
   Version: <development>
   Network: 1/1 OK
   Drives: 4/4 OK
   Pool: 1

┌──────┬────────────────────────┬─────────────────────┬──────────────┐
│ Pool │ Drives Usage           │ Erasure stripe size │ Erasure sets │
│ 1st  │ 12.3% (total: 1.9 TiB) │ 4                   │ 1            │
└──────┴────────────────────────┴─────────────────────┴──────────────┘

4 drives online, 0 drives offline, EC:2
Bucket created successfully `lab/media`.
5e12f72eaa2338a93f843524b06b1671  /tmp/video.mp4
part.1
part.2
11M	/data1/media
11M	/data2/media
11M	/data3/media
11M	/data4/media
```

في المخرج أعلاه لاحظ التالي:

- EC:2 تعني جزأي تماثل في كل مجموعة من أربعة أقراص، فالمساحة المستخدمة ضعف حجم البيانات، 44M لملف من 20 MiB، وهذا مثل نسختين، ولكن الحماية من ضياع أي قرصين، وليس قرصاً واحداً كما في نسختين عاديتين.
- كل قرص فيه مجلد باسم الكائن، وفيه ملف وصف xl.meta وأجزاء part.1 وpart.2، فلا تقرأ الملفات من القرص مباشرة ولا تعدلها، لأنها ليست الملف الأصلي.

والآن نحذف بيانات قرصين كاملين، ونقرأ الملف، ثم نطلب الإصلاح:

```bash
docker exec lab-s3servers-minio rm -rf /data3/media /data4/media
m cp -q lab/media/videos/intro.mp4 /tmp/back.mp4; docker exec lab-s3servers-minio md5sum /tmp/back.mp4
m admin heal -r lab/media
docker exec lab-s3servers-minio sh -c 'du -sh /data*/media'
```

```text
5e12f72eaa2338a93f843524b06b1671  /tmp/back.mp4
[Yellow ->  Green] media/
[Red    ->  Green] media/videos/intro.mp4
Healed:	1/1 objects; 20 MiB in 1s
11M	/data1/media
11M	/data2/media
11M	/data3/media
11M	/data4/media
```

الملف رجع بالبصمة نفسها من نصف الأقراص، والإصلاح أعاد بناء الجزأين المفقودين في ثانية. وفي الإنتاج يكون ال Erasure Set على عدة سيرفرات، فيصبح ضياع سيرفر مثل ضياع قرص هنا، ولذلك يطلب MinIO عدداً من السيرفرات والأقراص من البداية، ولا يحب أن تضيف قرصاً واحداً لاحقاً، وإنما مجموعة كاملة Pool.

### ماذا بقي في الواجهة؟

الواجهة على المنفذ 9001 ما زالت موجودة في هذا الإصدار، ولكنها بعد [إصدار مايو 2025](https://github.com/minio/minio/releases/tag/RELEASE.2025-05-24T17-08-30Z?ref=arabroot.io) أصبحت متصفحاً للملفات فقط، كما في الصورة التالية من تجربتنا، فلا توجد فيها إدارة المستخدمين ولا السياسات ولا الإعدادات، وكل ذلك يتم بأداة mc:

![واجهة MinIO Community Edition في المتصفح، في القائمة الجانبية إنشاء Bucket وقائمة ال Buckets وفيها media فقط والتوثيق والرخصة والخروج، وفي الوسط Object Browser يعرض المسار media/videos وفيه الملف intro.mp4 بحجم 20 MiB](https://arabroot.io/content/images/2026/10/s3-self-hosted-04-minio-console.webp)

الواجهة المجتمعية بعد مايو 2025: Buckets وملفات فقط

وإذا أردت MinIO بإصدارات مبنية جاهزة وواجهة كاملة، فهناك نسخ مشتقة Forks من مجتمعات أخرى، أشهرها Silo من فريق Pigsty، وقد ذكرناها مع تقييمها في [مقال المقارنة](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-s3-%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81-%D8%B0%D8%A7%D8%AA%D9%8A%D8%A7-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/#forks). وكانت ذاكرة MinIO بعد الاختبار 152.1 MiB بحسب `docker stats`.

## ما الذي تدعمه كل أداة من واجهة S3؟

صفحات التوافق مفيدة، ولكن الأفضل أن تسأل الأداة نفسها، فكتبنا سكربتاً ينشئ Bucket ويجرب ثماني ميزات، ويطبع رد الأداة كما هو، وفي ميزة Object Lock لا يكتفي بقبول الإعداد، وإنما يحاول حذف نسخة مقفلة ويتأكد أنها رفضت، ونحفظه باسم `compat.py` في المجلد `~/lab-s3servers`:

```python
# compat.py: ask the server for each S3 feature and print what it answers.
import os, io, json, uuid, boto3, requests
from botocore.config import Config
from botocore.exceptions import ClientError

s3 = boto3.client("s3", endpoint_url=os.environ["AWS_ENDPOINT_URL"],
                  config=Config(signature_version="s3v4", retries={"max_attempts": 1}))
B = "compat-" + uuid.uuid4().hex[:6]
s3.create_bucket(Bucket=B)

def check(name, fn):
    try:
        fn(); print(f"{name:<24} OK")
    except ClientError as e:
        err = e.response["Error"]; print(f"{name:<24} {err.get('Code')}: {err.get('Message', '')[:60]}")

def presigned():
    s3.put_object(Bucket=B, Key="a.txt", Body=b"hi")
    r = requests.get(s3.generate_presigned_url("get_object", Params={"Bucket": B, "Key": "a.txt"}, ExpiresIn=60))
    assert r.status_code == 200 and r.content == b"hi"

def multipart():
    up = s3.create_multipart_upload(Bucket=B, Key="big.bin")
    parts = []
    for n in (1, 2):
        p = s3.upload_part(Bucket=B, Key="big.bin", UploadId=up["UploadId"], PartNumber=n, Body=b"x" * (5 * 1024 * 1024))
        parts.append({"PartNumber": n, "ETag": p["ETag"]})
    s3.complete_multipart_upload(Bucket=B, Key="big.bin", UploadId=up["UploadId"], MultipartUpload={"Parts": parts})

def versioning():
    s3.put_bucket_versioning(Bucket=B, VersioningConfiguration={"Status": "Enabled"})
    s3.put_object(Bucket=B, Key="v.txt", Body=b"1"); s3.put_object(Bucket=B, Key="v.txt", Body=b"2")
    assert len(s3.list_object_versions(Bucket=B, Prefix="v.txt")["Versions"]) == 2

def object_lock():
    lb = B + "-lock"
    s3.create_bucket(Bucket=lb, ObjectLockEnabledForBucket=True)
    s3.put_object_lock_configuration(Bucket=lb, ObjectLockConfiguration={
        "ObjectLockEnabled": "Enabled", "Rule": {"DefaultRetention": {"Mode": "GOVERNANCE", "Days": 1}}})
    v = s3.put_object(Bucket=lb, Key="l.txt", Body=b"locked")["VersionId"]
    try:
        s3.delete_object(Bucket=lb, Key="l.txt", VersionId=v)
    except ClientError:
        return                                  # refused: the lock works
    raise ClientError({"Error": {"Code": "NotEnforced", "Message": "locked version was deleted"}}, "DeleteObject")

def lifecycle_expire():
    s3.put_bucket_lifecycle_configuration(Bucket=B, LifecycleConfiguration={"Rules": [
        {"ID": "tmp", "Status": "Enabled", "Filter": {"Prefix": "tmp/"}, "Expiration": {"Days": 1}}]})

def lifecycle_noncurrent():
    s3.put_bucket_lifecycle_configuration(Bucket=B, LifecycleConfiguration={"Rules": [
        {"ID": "old", "Status": "Enabled", "Filter": {"Prefix": ""}, "NoncurrentVersionExpiration": {"NoncurrentDays": 30}}]})

def bucket_policy():
    s3.put_bucket_policy(Bucket=B, Policy=json.dumps({"Version": "2012-10-17", "Statement": [{
        "Effect": "Deny", "Principal": "*", "Action": "s3:DeleteObject", "Resource": f"arn:aws:s3:::{B}/keep/*"}]}))

def cors():
    s3.put_bucket_cors(Bucket=B, CORSConfiguration={"CORSRules": [
        {"AllowedOrigins": ["https://app.example"], "AllowedMethods": ["GET", "PUT"], "AllowedHeaders": ["*"]}]})

for name, fn in [("presigned URL", presigned), ("multipart upload", multipart), ("versioning", versioning),
                 ("object lock", object_lock), ("lifecycle: expiration", lifecycle_expire),
                 ("lifecycle: old versions", lifecycle_noncurrent), ("bucket policy", bucket_policy), ("CORS", cors)]:
    check(name, fn)
```

شغلناه على كل أداة بمفتاح يستطيع إنشاء Buckets، وفي Garage أعطينا المفتاح هذه الصلاحية أولاً بالأمر `garage key allow --create-bucket media-app`:

```bash
cd ~/lab-s3servers
printf 'AWS_ACCESS_KEY_ID=minioadmin-lab\nAWS_SECRET_ACCESS_KEY=change-me-long-password-01\nAWS_DEFAULT_REGION=us-east-1\nAWS_ENDPOINT_URL=http://lab-s3servers-minio:9000\n' > minio.env
docker exec --env-file garage.env    lab-s3servers-tools python3.11 /lab/compat.py
docker exec --env-file sw-admin.env  lab-s3servers-tools python3.11 /lab/compat.py
docker exec --env-file minio.env     lab-s3servers-tools python3.11 /lab/compat.py
```

والمخرج لكل أداة كما يلي، Garage أولاً ثم SeaweedFS ثم MinIO:

```text
presigned URL            OK
multipart upload         OK
versioning               NotImplemented: Unimplemented action: PutBucketVersioning
object lock              NotImplemented: Unimplemented action: PutObjectLockConfiguration
lifecycle: expiration    OK
lifecycle: old versions  OK
bucket policy            NotImplemented: Unimplemented action: PutBucketPolicy
CORS                     OK

presigned URL            OK
multipart upload         OK
versioning               OK
object lock              OK
lifecycle: expiration    OK
lifecycle: old versions  OK
bucket policy            OK
CORS                     OK

presigned URL            OK
multipart upload         OK
versioning               OK
object lock              OK
lifecycle: expiration    OK
lifecycle: old versions  OK
bucket policy            OK
CORS                     NotImplemented: A header you provided implies functionality that is not impl
```

في المخرج أعلاه لاحظ التالي:

- Garage رفض النسخ وقفل الكائنات وسياسات ال Bucket، وهذا يطابق [صفحة التوافق](https://garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility/?ref=arabroot.io)، فالصلاحيات فيه لكل مفتاح على كل Bucket بنظامه الخاص، وقبوله قاعدة «النسخ القديمة» لا يعني شيئاً لأنه لا يحفظ نسخاً أصلاً.
- SeaweedFS قبل الميزات الثماني، ورفض حذف النسخة المقفلة، وهذا يطابق [صفحة واجهة S3](https://github.com/seaweedfs/seaweedfs/wiki/Amazon-S3-API?ref=arabroot.io) فيه، مع أن قواعد النقل بين الفئات Transition غير مدعومة.
- MinIO قبل كل شيء ما عدا إعداد CORS على ال Bucket، فهذه الواجهة غير موجودة في النسخة المجتمعية، فإذا كان المتصفح يرفع مباشرة إليه فاضبط CORS في ال Reverse Proxy أمامه.

والخلاصة أن التطبيق الذي يرفع ويقرأ ويستخدم روابط موقعة يعمل على الثلاثة، أما النسخ الاحتياطي الذي يعتمد على Versioning أو Object Lock فلا يعمل على Garage، وهذا موضوع [النسخ الاحتياطي إلى S3 مع Object Lock](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A7%D9%84%D9%89-s3-%D9%85%D8%B9-object-lock-%D8%B6%D8%AF-%D8%A7%D9%84%D9%81%D8%AF%D9%8A%D8%A9/).

## الأمان والمراقبة والنسخ الاحتياطي للثلاثة

- **المنافذ:** انشر منفذ S3 وحده، وعلى 127.0.0.1 خلف Reverse Proxy بشهادة TLS مثل [Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)، أما منافذ الإدارة والتواصل بين العقد فداخل شبكة خاصة أو VPN فقط.
- **المفاتيح:** مفتاح لكل تطبيق على ال Bucket الخاص به، والمفتاح الرئيسي Root أو Admin لا يغادر السيرفر، كما شرحنا في [قسم الصلاحيات](https://arabroot.io/articles/s3-%D9%85%D9%86-%D8%AF%D8%A7%D8%AE%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B1%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D9%85%D9%88%D9%82%D8%B9%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%83%D9%84%D9%81%D8%A9/#access).
- **المراقبة:** الثلاثة تعطي مقاييس بصيغة Prometheus، Garage على منفذ الإدارة بحسب [توثيق المراقبة](https://garagehq.deuxfleurs.fr/documentation/cookbook/monitoring/?ref=arabroot.io)، وSeaweedFS بالخيار `-metricsPort` لكل خدمة كما في ملف Compose الرسمي، وراقب أولاً المساحة الحرة وعدد العقد أو الأقراص المتصلة.
- **النسخ ليس نسخة احتياطية:** النسخ بين العقد والتماثل يحميان من تعطل قرص أو سيرفر، ولكنهما ينسخان الحذف أيضاً، فإذا حذف سكربت كل الملفات حذفها من كل النسخ، لذلك تحتاج نسخة احتياطية خارجية كما في [قاعدة 3-2-1](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).
- **البيانات الوصفية:** في Garage قاعدة ال Metadata في `metadata_dir`، وفي SeaweedFS قاعدة ال Filer، وفقدانها أسوأ من فقدان قرص بيانات، لأن الأجزاء تبقى دون أسماء، فضعها على قرص سريع ومحمي.

### التنظيف

```bash
cd ~/lab-s3servers/garage  && docker compose down -v
cd ~/lab-s3servers/seaweed && docker compose down -v
cd ~/lab-s3servers/minio   && docker compose down -v
docker rm -f lab-s3servers-tools
docker network rm lab-s3servers-net
docker image rm lab/minio:RELEASE.2025-10-15T17-29-55Z
```

## أخطاء شائعة

- **عقدة واحدة في الإنتاج:** replication\_factor 1 أو قاعدة 000 تعني نسخة واحدة، والقرص الذي يتعطل يأخذ ملفاته معه.
- **عدد خوادم يساوي عدد النسخ:** في SeaweedFS مع 001 وخادمين توقفت الكتابة عند تعطل أحدهما، فاجعل الخوادم أكثر من النسخ.
- **نسيان layout apply في Garage:** العقد المتصلة دون دور لا تخزن شيئاً.
- **بوابة SeaweedFS دون ملف هويات:** تقبل أي مفتاح.
- **أقراص MinIO كلها في سيرفر واحد مع ظن أنها توفر عالي:** هي حماية من تلف الأقراص فقط.
- **الاعتماد على Image قديمة لم تعد تصلها إصلاحات:** احفظ الإصدار الذي بنيته، وتابع ثغراته، وخطط للانتقال.
- **اعتبار النسخ بين العقد نسخة احتياطية:** الحذف ينسخ أيضاً.

## الخلاصة

- Garage خفيف ومصمم لمواقع متفرقة، بنسخة كاملة في كل منطقة ونصاب 2 من 3، تحمل توقف عقدة ورفض الكتابة والقراءة عند توقف عقدتين، واستخدم أقل من 50 MiB من الذاكرة للعقد الثلاث.
- SeaweedFS أربعة أدوار منفصلة، ينسخ على مستوى ال Volume، وقرأ الملف مع خادم متوقف ولكنه رفض الكتابة لأن القاعدة 001 احتاجت خادمين، ويدعم أوسع مجموعة من ميزات S3 في تجربتنا.
- MinIO يبنى اليوم من الكود، وترميز المحو EC:2 على أربعة أقراص قرأ الملف بعد إتلاف قرصين وأصلحهما في ثانية، ولكن مستودعه لم يعد يصان.
- Garage لا يدعم Versioning ولا Object Lock ولا سياسات ال Bucket، وMinIO المجتمعي لا يدعم إعداد CORS على ال Bucket.
- الرخص: Garage وMinIO برخصة AGPL-3.0، وSeaweedFS برخصة Apache-2.0.

[![](https://arabroot.io/content/images/2026/10/storage-types-cover-cover.webp)دليلأنواع التخزين وواجهة S3: أين تضع بيانات تطبيقك؟أين تضع قاعدة البيانات وملفات المستخدمين والنسخ الاحتياطية، ولماذا يناسب كلاً منها نوع مختلف من التخزين.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-block-%D9%88-file-%D9%88-object-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/s3-basics-cover-cover.webp)دليلS3 من داخل تطبيقك: الصلاحيات والروابط الموقعة والتكلفةكيف يرفع تطبيقك الملفات إلى S3 بمفتاح محدود وروابط موقعة، ويحمي الملفات من الكتابة فوقها، ويتجنب البنود التي تضخم الفاتورة.](https://arabroot.io/articles/s3-%D9%85%D9%86-%D8%AF%D8%A7%D8%AE%D9%84-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B1%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D9%85%D9%88%D9%82%D8%B9%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%83%D9%84%D9%81%D8%A9/)[![](https://arabroot.io/content/images/2026/10/s3-servers-compare-cover-cover.webp)دليلأي تخزين S3 مستضاف ذاتياً يناسبك؟ وما الذي تغير في MinIOمقارنة عملية بين أشهر أدوات تخزين S3 المستضافة ذاتياً بقياسات على جهاز واحد، لتعرف أيها يناسب سيرفرك وماذا حدث فعلاً ل MinIO.](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-s3-%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81-%D8%B0%D8%A7%D8%AA%D9%8A%D8%A7-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/)[![](https://arabroot.io/content/images/2026/10/s3-object-lock-cover-cover.webp)دليلالنسخ الاحتياطي إلى S3 مع Object Lock ضد الفديةكيف تقفل نسخك الاحتياطية في S3 حتى لا يمحوها من يسرق مفاتيحها، وكيف تستعيدها إلى ما قبل الهجوم.](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A7%D9%84%D9%89-s3-%D9%85%D8%B9-object-lock-%D8%B6%D8%AF-%D8%A7%D9%84%D9%81%D8%AF%D9%8A%D8%A9/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، والتجربة على Garage v2.4.1 بثلاث عقد، وSeaweedFS 4.48 بخمس خدمات، وMinIO RELEASE.2025-10-15T17-29-55Z مبني من الكود بأداة Go 1.24.13، مع aws-cli 2.37.12 وboto3 1.43.111 في Docker Engine 29.8.0، وحالة مستودع MinIO والسجلات كما رأيناها في 11 أكتوبر 2026.