اختصار الروابط ذاتياً باستخدام Shlink وDocker
أنشئ خدمة اختصار روابط خاصة بك على VPS باستخدام Shlink وDocker Compose، مع نطاق قصير وPostgres ومفتاح API وعميل ويب ورموز QR وإحصاءات النقرات.
ما الذي ستبنيه
خدمة اختصار الروابط المستضافة ذاتياً هي خادم صغير يحوّل رابطاً طويلاً إلى رابط قصير تملكه، ويحصي كل نقرة عليه. الخيار المناسب هو Shlink: فهو مفتوح المصدر، ويتوفر على هيئة صورة Docker، وينفّذ المهمة كاملة داخل حاوية واحدة مع قاعدة بيانات. يضع هذا الدليل الخدمة على VPS خلف نطاق قصير فعلي، مع HTTPS ومفتاح API ورموز QR وإحصاءات النقرات.
يتكوّن الحل من جزأين يجعلان استخدامه قريباً من خدمات الاختصار التجارية. يستجيب خادم API لعمليات إعادة التوجيه ويخزّن البيانات. أما عميل الويب فهو تطبيق ثابت منفصل يتصل بواجهة API هذه من متصفحك. يمكنك تشغيل الجزأين معاً، أو تشغيل API وحدها والتحكم بها من سطر الأوامر.
أرقام الإصدارات الواردة هنا هي الإصدارات الحالية حتى July 2026: Shlink 5.1 وshlink-web-client 4.8.
اجعل نطاقاً قصيراً يشير إلى الخادم أولاً
النطاق هو المنتج. يظهر s.example.com/abc123 في الروابط التي يراها المستخدمون، لذلك اختر اسماً قصيراً وحدده قبل تثبيت أي شيء. يخزّن Shlink النطاق مع كل عنوان URL مختصر، ويؤدي تغييره لاحقاً إلى توقف كل رابط سبق أن وزعته عن العمل.
أنشئ سجل DNS من نوع A للنطاق القصير، ووجّهه إلى عنوان IPv4 العام لخادم VPS. أضف أيضاً سجلاً من نوع AAAA إذا كان الخادم يدعم IPv6. ثم تأكد من أن النطاق يُحل قبل المتابعة.
dig +short s.example.com Aيجب أن يكون الناتج هو عنوان خادمك. إذا كان الناتج فارغاً، فهذا يعني أن السجل لم ينتشر بعد، وستفشل كل خطوة لاحقة بطريقة مربكة، لأن تعذر إصدار شهادة TLS (أمان طبقة النقل) لاسم لا يُحل.
ملف compose
يحتاج Shlink إلى قاعدة بيانات. يصلح SQLite للاختبار، لكن Postgres هو الخيار الصحيح لأي شيء تخطط للاحتفاظ به، لأن صفوف الزيارات تتراكم، ولأن Postgres يتعامل مع الفهارس وعمليات الكتابة المتزامنة بصورة أفضل. ضع ما يلي في /opt/shlink/compose.yaml.
services:
shlink:
image: shlinkio/shlink:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
DEFAULT_DOMAIN: s.example.com
IS_HTTPS_ENABLED: "true"
DB_DRIVER: postgres
DB_HOST: database
DB_NAME: shlink
DB_USER: shlink
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
- database
database:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: shlink
POSTGRES_USER: shlink
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- shlink_db:/var/lib/postgresql/data
web-client:
image: shlinkio/shlink-web-client:stable
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
volumes:
shlink_db:يرتبط كلا المنفذين المنشورين بـ127.0.0.1، لذلك لا يمكن الوصول إلى أي شيء من الإنترنت إلى أن يصبح الـreverse proxy في القسم التالي جاهزاً. يكتب Docker قواعد إعادة التوجيه الخاصة به قبل جدار حماية المضيف، ما يعني أن سطراً عادياً مثل 8080:8080 سيجعل التطبيق مكشوفاً حتى على خادم يبدو جدار حمايته مغلقاً. يؤدي الارتباط بعنوان loopback إلى تجنّب ذلك. وينطبق النمط نفسه على أي تطبيق تشغّله بهذه الطريقة، وتتناوله دليل Docker Compose على VPS بمزيد من التفصيل.
تأتي كلمة مرور قاعدة البيانات من ملف .env بجوار ملف compose، لذلك لا تُحفظ في YAML.
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.envشغّل الخدمة وراقب بدء API.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkينفّذ التشغيل الأول عمليات ترحيل قاعدة البيانات، لذلك يستغرق وقتاً أطول من عمليات التشغيل اللاحقة. بعد استقرار الخدمة، تحقّق من أن الخدمة تستجيب محلياً.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthيعني 200 أن API تعمل وأن اتصال قاعدة البيانات سليم. ويشير 500 هنا في الغالب إلى مشكلة في قاعدة البيانات: قيمة DB_PASSWORD في .env لا تطابق القيمة التي أُنشئت بها Postgres، لأن صورة Postgres تقرأ POSTGRES_PASSWORD فقط عند تهيئة مجلد بيانات فارغ. لا يؤثر تعديل كلمة المرور لاحقاً حتى تزيل volume وتبدأ الخدمة من جديد.
إنهاء HTTPS أمامه
يقدّم Shlink خدمة HTTP عادية على المنفذ 8080. يجب إنهاء TLS في reverse proxy، والإعداد الوحيد المهم هو تمرير اسم المضيف الأصلي. يحدد Shlink النطاق الذي ينتمي إليه الرمز المختصر بقراءة الرأس Host، لذلك فإن proxy يعيد كتابته ينتج استجابات 404 للروابط الموجودة، ويربط إحصاءات الزيارات بالنطاق الخطأ.
server {
server_name s.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}بعد ذلك، أصدِر الشهادة. يرد الشرح الكامل، بما في ذلك مؤقت التجديد، في دليل Certbot لـ nginx على Ubuntu 24.04.
sudo certbot --nginx -d s.example.comإنّ IS_HTTPS_ENABLED: "true" في ملف compose هو ما يجعل Shlink يطبع https:// في عناوين URL المختصرة التي يعيدها. لا يفعّل ذلك TLS بحد ذاته. اتركه false خلف HTTPS proxy، وستكون كل وصلة يعيدها API وصلة http:// ثم تعيد التوجيه، ما يضيف دورة ذهاب وإياب ويبدو غير صحيح في عميل الويب.
إنشاء مفتاح API
لا يمكن لأي جهة الاتصال بـAPI من دون مفتاح. أنشئ مفتاحاً عبر CLI داخل الحاوية.
sudo docker compose exec shlink shlink api-key:generate --name "web client"يعرض الأمر المفتاح مرة واحدة. انسخه الآن، لأنه يُخزَّن بعد تجزئته ولا يمكن عرضه مرة أخرى. يعرض shlink api-key:list الأسماء وما إذا كان كل مفتاح مفعّلاً، لكنه لا يعرض المفتاح نفسه مطلقاً. ألغِ مفتاحاً باستخدام shlink api-key:disable واسمه.
تتضمن كل مكالمة REST المفتاح في ترويسة X-Api-Key.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsيعني كائن JSON يحتوي على مفتاح shortUrls أن المفتاح يعمل. ويعني 401 الذي يحمل INVALID_API_KEY أن المفتاح غير صحيح، أو معطّل، أو تجاوز تاريخ انتهاء صلاحيته.
أنشئ روابط قصيرة من سطر الأوامر
يُعد CLI أسرع طريقة لإنشاء الروابط، كما أنه مناسب للبرامج النصية.
sudo docker compose exec shlink shlink short-url:create https://example.com/a/very/long/path
sudo docker compose exec shlink shlink short-url:create https://example.com/docs --custom-slug docs --tag referenceيمنحك --custom-slug رابطاً سهل القراءة بدلاً من رمز مُنشأ تلقائياً. تكون الأجزاء التعريفية فريدة لكل نطاق، لذلك تفشل المحاولة الثانية باستخدام جزء تعريفي مستخدم بدلاً من استبدال الرابط الأول بصمت. يمكنك تكرار --tag، وتُستخدم الوسوم لتجميع الروابط التي ستحتاج إلى إحصاءات مشتركة لها لاحقاً.
اعرض العناصر الموجودة، ثم اطّلع على حركة المرور الخاصة بأحد الروابط.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsيطبع short-url:visits صفاً واحداً لكل نقرة، متضمناً التاريخ، والمُحيل، ووكيل المستخدم. تبقى أعمدة الدولة والمدينة فارغة ما لم تضبط متغير البيئة GEOLITE_LICENSE_KEY، وهو مفتاح MaxMind مجاني يستخدمه Shlink لتنزيل قاعدة بيانات GeoLite2. من دونه، يستمر تسجيل الزيارات، لكن لا يجري تحديد مواقعها.
واجهة الويب ورموز QR
واجهة الويب متاحة الآن على 127.0.0.1:8081 وتحتاج إلى إدخال proxy خاص بها، أو إلى نفق SSH إذا كنت تفضّل عدم نشرها. تطلب الواجهة عنوان الخادم ومفتاح API عند التحميل الأول. أدخل https://s.example.com والمفتاح الذي أنشأته. تحفظ الواجهة القيمتين في مساحة تخزين المتصفح، وتتصل بـAPI لديك مباشرةً، لذلك لا تمر أي بيانات عبر جهة أخرى. يُعد فصل الواجهة عن API نمطاً جديراً بالملاحظة، لأنه النمط نفسه الذي يتيح لـHalcyon تحويل مكتبة Jellyfin إلى متجر تأجير من حقبة 1990 من دون تغيير خادم الوسائط الذي يعمل خلفها.
لا تحتاج رموز QR إلى أي إعداد. أضف /qr-code إلى أي URL قصير، وستعيد API الصورة.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20يمثل size العرض بالبكسل، ويقبل القيم من 50 إلى 1000، وتكون القيمة الافتراضية 300. يمثل format القيمة png أو svg. يمثل margin المساحة الخالية حول الرمز بالبكسل، ويكون قياس الصورة النهائية مساوياً للحجم مضافاً إليه ضعف الهامش. أضف errorCorrection=Q لإنشاء رمز يظل قابلاً للمسح عند طباعته بحجم صغير أو تغطية جزء منه.
التشغيل المستمر
قد يتعطل تطبيق اختصار الروابط بصمت. تتوقف الروابط عن إعادة التوجيه، ولا يخبرك أحد بذلك، لأن الشخص الذي نقر عليها افترض أن الرابط لم يعد صالحاً. وجّه فحص uptime إلى رابط مختصر فعلي بدلاً من الصفحة الرئيسية، وأرسل تنبيهاً عند ظهور أي استجابة لا تكون إعادة توجيه. تنفّذ نسخة Uptime Kuma مستضافة ذاتياً ذلك جيداً، ويمكنها مراقبة رمز حالة محدد.
انسخ قاعدة البيانات احتياطياً، وليس الحاوية. يفرّغها أمر واحد.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzيعيد هذا الملف، إلى جانب ملف compose، إنشاء الخدمة بأكملها على خادم جديد. يحتاج كل تطبيق على الخادم إلى نسخة خاصة به من هذين الملفين. وتُعد مكتبة الصور الحالة المعقدة، لأن PhotoPrism وImmich يحتفظان بالنسخ الأصلية على القرص، إلى جانب الصفوف في قاعدة البيانات. لذلك لا يعيد التفريغ وحده أي شيء. تُنفّذ الترقيات عبر sudo docker compose pull ثم sudo docker compose up -d، ويشغّل Shlink أي عمليات migration جديدة عند بدء التشغيل. أنشئ التفريغ قبل تنفيذ pull، لأن عملية migration لا يمكن التراجع عنها.
FAQ
لماذا تُرجع الروابط المختصرة رمز 404 بعد إضافة Reverse Proxy؟
يطابق Shlink الرمز المختصر مع النطاق الموجود في الترويسة Host. إذا أرسل الـProxy اسمه الخاص أو عنواناً داخلياً، فسيبحث Shlink عن ذلك الرمز ضمن نطاق لا يحتوي على روابط، ولذلك يُرجع 404. اضبط proxy_set_header Host $host; في كتلة الموقع في nginx، ثم أعد تحميل الـProxy. ستعمل الروابط فوراً، من دون إعادة تشغيل الحاوية.
هل أحتاج إلى Postgres، أم تكفي SQLite؟
تكفي SQLite لتجربة Shlink، ولا تحتاج إلى حاوية ثانية. انتقل إلى Postgres قبل نشر روابط مهمة، لأن صفوف الزيارات تزداد مع كل نقرة، ولأن SQLite تجعل عمليات الكتابة متسلسلة. يتطلب التبديل لاحقاً تصدير الروابط وإعادة استيرادها، لذلك يوفّر اختيار Postgres من البداية عملية الترحيل هذه.
هل يمكنني استعادة مفتاح API نسيت نسخه؟
لا. يخزّن Shlink تجزئة المفتاح، لذلك يعرض api-key:list الأسماء والحالة، لكنه لا يعرض قيمة المفتاح مطلقاً. أنشئ بديلاً باستخدام shlink api-key:generate، والصقه في عميل الويب، ثم عطّل المفتاح القديم باستخدام shlink api-key:disable حتى يتوقف عن العمل.
لماذا تكون أعمدة البلدان فارغة في إحصاءات الزيارات؟
تحتاج خدمة تحديد الموقع الجغرافي إلى قاعدة بيانات GeoLite2، ولا ينزّلها Shlink إلا عند تزويده بـ GEOLITE_LICENSE_KEY. المفتاح مجاني من MaxMind. أضفه إلى قسم البيئة، ثم أعد إنشاء الحاوية، وستُحدَّد مواقع الزيارات الجديدة. ستبقى الزيارات المسجّلة قبل ذلك فارغة إلى أن تشغّل shlink visit:locate.
كيف أنقل Shlink إلى خادم آخر؟
احتفظ بالنطاق وانقل البيانات. أنشئ نسخة تفريغ من قاعدة البيانات باستخدام pg_dump، وانسخ النسخة وملف compose إلى الخادم الجديد، ثم شغّل المكدس واستعد النسخة في قاعدة البيانات الفارغة قبل وصول حركة الشبكة الفعلية. غيّر سجل DNS أخيراً. ستبقى الرموز المختصرة وسجل زياراتها، لأن كل شيء مخزّن في قاعدة البيانات.