Shlink দিয়ে নিজের URL shortener তৈরি করার নিয়ম
Shlink এবং Docker Compose ব্যবহার করে VPS-এ নিজস্ব URL shortener তৈরির পূর্ণাঙ্গ গাইড। এতে DNS কনফিগারেশন, Postgres ডাটাবেস, API কি এবং ক্লিক স্ট্যাটাস ট্র্যাক করার পদ্ধতি রয়েছে।
আপনি যা তৈরি করছেন
একটি self-hosted URL shortener হলো ছোট একটি সার্ভার, যা দীর্ঘ লিঙ্ককে আপনার নিজস্ব ছোট লিঙ্কে রূপান্তর করে এবং প্রতিটি ক্লিকের হিসাব রাখে। এক্ষেত্রে Shlink বেছে নেওয়া বুদ্ধিমানের কাজ: এটি open source, Docker image হিসেবে পাওয়া যায় এবং একটি container ও database-এর মাধ্যমেই পুরো কাজটি সম্পন্ন করে। এই গাইডে আমরা এটিকে একটি VPS-এ স্থাপন করব, যেখানে একটি আসল short domain, HTTPS, API key, QR code এবং click stats যুক্ত থাকবে।
দুটি অংশ এটিকে একটি বাণিজ্যিক shortener-এর মতো কার্যকর করে তোলে। API server রিডাইরেক্টের উত্তর দেয় এবং ডেটা সংরক্ষণ করে। Web client হলো একটি আলাদা static app, যা আপনার browser থেকে সেই API-এর সাথে যোগাযোগ করে। আপনি চাইলে দুটিই চালাতে পারেন, অথবা শুধু API চালিয়ে command line থেকে তা নিয়ন্ত্রণ করতে পারেন।
এখানে ব্যবহৃত version নম্বরগুলো জুলাই 2026 অনুযায়ী বর্তমান: Shlink 5.1 এবং shlink-web-client 4.8।
প্রথমে সার্ভারের দিকে একটি ছোট ডোমেইন নির্দেশ করুন
ডোমেইনটিই হলো মূল পণ্য। s.example.com/abc123 হলো সেই লিঙ্ক যা মানুষ দেখতে পায়, তাই কোনো কিছু ইনস্টল করার আগেই একটি ছোট নাম বেছে নিন। Shlink প্রতিটি ছোট URL-এর সাথে ডোমেইনটি সংরক্ষণ করে, তাই পরে এটি পরিবর্তন করলে আপনার বিতরণ করা প্রতিটি লিঙ্ক কাজ করা বন্ধ করে দেবে।
আপনার ছোট ডোমেইনটির জন্য একটি DNS A record তৈরি করুন, যা আপনার VPS-এর পাবলিক IPv4 ঠিকানাকে নির্দেশ করবে। সার্ভারে IPv6 থাকলে একটি AAAA record-ও যোগ করুন। এরপর পরবর্তী ধাপে যাওয়ার আগে নিশ্চিত করুন যে এটি সঠিকভাবে resolve হচ্ছে।
dig +short s.example.com Aআউটপুট হিসেবে আপনার সার্ভারের ঠিকানা আসা উচিত। যদি এটি খালি থাকে, তবে বুঝতে হবে record এখনো propagate হয়নি। সেক্ষেত্রে পরবর্তী প্রতিটি ধাপ বিভ্রান্তিকরভাবে ব্যর্থ হবে, কারণ যে নামের বিপরীতে কোনো IP resolve হয় না, তার জন্য TLS (transport layer security) সার্টিফিকেট ইস্যু করা সম্ভব নয়।
Compose ফাইল
Shlink-এর জন্য একটি ডেটাবেস প্রয়োজন। পরীক্ষার জন্য SQLite কাজ করলেও, দীর্ঘমেয়াদী ব্যবহারের জন্য Postgres ব্যবহার করাই সঠিক সিদ্ধান্ত। কারণ ভিজিটের সংখ্যা বাড়লে Postgres ইনডেক্স এবং কনকারেন্ট রাইট (concurrent write) অনেক ভালোভাবে সামলাতে পারে। এটি /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-এ বাইন্ড করা হয়েছে, তাই পরবর্তী সেকশনে রিভার্স প্রক্সি সেটআপ না করা পর্যন্ত ইন্টারনেট থেকে কিছুই অ্যাক্সেস করা যাবে না। Docker হোস্ট ফায়ারওয়ালের আগেই নিজস্ব ফরওয়ার্ডিং রুল লিখে ফেলে, যার মানে হলো একটি সাধারণ 8080:8080 লাইন ব্যবহার করলে ফায়ারওয়াল বন্ধ থাকা সত্ত্বেও অ্যাপটি ইন্টারনেটে উন্মুক্ত হয়ে যেতে পারে। লুপব্যাক অ্যাড্রেসে (loopback address) বাইন্ড করলে এই ঝুঁকি এড়ানো যায়। এই একই পদ্ধতি যেকোনো অ্যাপের ক্ষেত্রে প্রযোজ্য এবং এটি VPS-এ Docker Compose ব্যবহারের নির্দেশিকা-তে বিস্তারিত আলোচনা করা হয়েছে।
ডেটাবেসের পাসওয়ার্ডটি compose ফাইলের পাশে থাকা একটি .env ফাইল থেকে আসে, তাই এটি কখনোই 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 আসার অর্থ প্রায় সবসময়ই ডেটাবেস সংক্রান্ত সমস্যা: .env-এর মধ্যে থাকা DB_PASSWORD-এর সাথে Postgres তৈরির সময়কার তথ্যের মিল নেই। কারণ Postgres ইমেজ শুধুমাত্র একটি খালি ডেটা ডিরেক্টরি ইনিশিয়ালাইজ করার সময় POSTGRES_PASSWORD ফাইলটি পড়ে। পরবর্তীতে পাসওয়ার্ড পরিবর্তন করলে তা কার্যকর হয় না যতক্ষণ না আপনি ভলিউমটি মুছে ফেলে পুনরায় শুরু করছেন।
এর সামনে HTTPS টার্মিনেশন করুন
Shlink পোর্ট 8080-এ সাধারণ HTTP পরিবেশন করে। TLS-এর দায়িত্ব একটি reverse proxy-র, এবং এক্ষেত্রে সবচেয়ে গুরুত্বপূর্ণ সেটিংস হলো মূল host name-টি সঠিকভাবে পাস করা। Shlink কোন short code কোন ডোমেইনের অন্তর্গত তা নির্ধারণ করে Host হেডারটি পড়ে। তাই যে প্রক্সি এই হেডারটি পরিবর্তন করে দেয়, সেটি বিদ্যমান লিঙ্কের ক্ষেত্রে 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;
}
}এরপর সার্টিফিকেট ইস্যু করুন। রিনিউয়াল টাইমারসহ সম্পূর্ণ নির্দেশিকাটি Ubuntu 24.04-এ Nginx-এর জন্য Certbot গাইড-এ দেওয়া আছে।
sudo certbot --nginx -d s.example.comcompose ফাইলে থাকা IS_HTTPS_ENABLED: "true" হলো সেই সেটিংস যা Shlink-কে তার রিটার্ন করা short URL-গুলোতে https:// প্রিন্ট করতে বলে। এটি নিজে থেকে TLS সক্রিয় করে না। এটিকে একটি HTTPS প্রক্সির পেছনে false অবস্থায় রেখে দিলে API থেকে পাওয়া প্রতিটি লিঙ্ক একটি http:// লিঙ্ক হয়ে যায়, যা পরবর্তীতে রিডাইরেক্ট হয়। এতে একটি অতিরিক্ত রাউন্ড ট্রিপের প্রয়োজন হয় এবং ওয়েব ক্লায়েন্টে এটি ভুল দেখায়।
একটি API key তৈরি করুন
একটি key ছাড়া কোনো কিছুই API-এর সাথে যোগাযোগ করতে পারে না। কন্টেইনারের ভেতর থেকে CLI ব্যবহার করে একটি key তৈরি করুন।
sudo docker compose exec shlink shlink api-key:generate --name "web client"docker exec -it <container_name> ./app-cli key-generate
এই কমান্ডটি একবারই key-টি প্রদর্শন করে। এখনই এটি কপি করে নিন, কারণ এটি হ্যাশ (hashed) অবস্থায় সংরক্ষিত থাকে এবং পরবর্তীতে আর দেখানো সম্ভব নয়। shlink api-key:list কমান্ডটি প্রতিটি key-এর নাম এবং সেগুলো সক্রিয় কি না তা দেখায়, কিন্তু কখনোই মূল key-টি দেখায় না। shlink api-key:disable এবং key-এর নাম ব্যবহার করে যেকোনো key বাতিল (revoke) করা যায়।
প্রতিটি REST কলে X-Api-Key হেডারে key-টি পাঠাতে হয়।
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlscurl -H "X-API-Key: <your_key>" https://api.example.com/status
shortUrls কী (key) সম্বলিত একটি JSON অবজেক্ট মানে হলো key-টি সঠিকভাবে কাজ করছে। INVALID_API_KEY বার্তা বহনকারী একটি 401 মানে হলো 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 ব্যবহার করলে আপনি জেনারেট করা কোডের পরিবর্তে একটি পাঠযোগ্য লিঙ্ক পাবেন। প্রতিটি ডোমেইনের জন্য স্লাগ (slug) অনন্য হয়, তাই কোনো স্লাগ আগে থেকে নেওয়া থাকলে দ্বিতীয়বার সেটি ব্যবহারের চেষ্টা করলে তা ব্যর্থ হবে; এটি আগের লিঙ্কটিকে নীরবে ওভাররাইট করবে না। --tag একাধিকবার ব্যবহার করা যায় এবং ট্যাগ ব্যবহার করে আপনি সেই লিঙ্কগুলোকে গ্রুপ করতে পারেন যেগুলোর সম্মিলিত পরিসংখ্যান আপনি পরে দেখতে চান।
বিদ্যমান লিঙ্কগুলোর তালিকা দেখুন এবং তারপর একটি লিঙ্কের ট্রাফিক পরীক্ষা করুন।
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits প্রতিটি ক্লিকের জন্য একটি করে সারি প্রিন্ট করে, যেখানে তারিখ, রেফারার (referrer) এবং ইউজার এজেন্ট (user agent) থাকে। দেশ এবং শহরের কলামগুলো খালি থাকে যদি না আপনি একটি GEOLITE_LICENSE_KEY এনভায়রনমেন্ট ভেরিয়েবল সেট করেন। এটি একটি ফ্রি MaxMind কি (key), যা Shlink ব্যবহার করে GeoLite2 ডেটাবেস ডাউনলোড করে। এটি ছাড়া ভিজিট রেকর্ড করা হলেও সেগুলোর ভৌগোলিক অবস্থান নির্ণয় করা সম্ভব হয় না।
ওয়েব ক্লায়েন্ট এবং QR কোড
ওয়েব ক্লায়েন্ট এখন 127.0.0.1:8081-এ রয়েছে এবং এর জন্য নিজস্ব প্রক্সি এন্ট্রি প্রয়োজন। আপনি যদি এটি পাবলিক করতে না চান, তবে একটি SSH tunnel ব্যবহার করতে পারেন। প্রথমবার লোড হওয়ার সময় এটি একটি সার্ভার URL এবং একটি API key চাইবে। সেখানে https://s.example.com এবং আপনার তৈরি করা কী (key) প্রদান করুন। ক্লায়েন্ট এই দুটি তথ্য ব্রাউজারের স্টোরেজে রাখে এবং সরাসরি আপনার API-কে কল করে, তাই কোনো তথ্যই অন্য কারো মাধ্যমে আদান-প্রদান হয় না। ইন্টারফেসকে API থেকে আলাদা করার এই পদ্ধতিটি লক্ষ্য করার মতো, কারণ একই পদ্ধতি ব্যবহার করে Halcyon কোনো পরিবর্তন ছাড়াই একটি Jellyfin লাইব্রেরিকে 1990-এর দশকের রেন্টাল স্টোরের রূপ দেয়।
QR কোডের জন্য কোনো কনফিগারেশনের প্রয়োজন নেই। যেকোনো শর্ট URL-এর শেষে /qr-code যোগ করুন, API স্বয়ংক্রিয়ভাবে ইমেজটি রিটার্ন করবে।
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size হলো পিক্সেলের এককে প্রস্থ, যা 50 থেকে 1000 পর্যন্ত হতে পারে এবং ডিফল্ট মান হলো 300। format হলো png অথবা svg। margin হলো কোডের চারপাশের খালি জায়গা (পিক্সেলের এককে), এবং চূড়ান্ত ইমেজের পরিমাপ হলো কোডের আকার যোগ দুই গুণ মার্জিন। ছোট আকারে প্রিন্ট করলে বা আংশিক ঢাকা থাকলেও যেন স্ক্যান করা যায়, সেজন্য errorCorrection=Q যোগ করুন।
সার্ভিস সচল রাখা
একটি শর্টেনার কোনো বার্তা না দিয়েই কাজ করা বন্ধ করে দিতে পারে। লিঙ্কগুলো রিডাইরেক্ট করা বন্ধ হয়ে যায় এবং কেউ আপনাকে জানায় না, কারণ ব্যবহারকারী মনে করেন লিঙ্কটি হয়তো নষ্ট। আপনার হোম পেজের পরিবর্তে একটি কার্যকর শর্ট URL-এর ওপর uptime চেক সেট করুন এবং রিডাইরেক্ট ছাড়া অন্য যেকোনো রেসপন্স পেলে অ্যালার্ট পাওয়ার ব্যবস্থা রাখুন। একটি self-hosted Uptime Kuma instance এই কাজটি খুব ভালোভাবে করতে পারে এবং এটি নির্দিষ্ট status code-এর ওপর নজর রাখতে সক্ষম।
কন্টেইনার নয়, বরং ডেটাবেস ব্যাকআপ নিন। একটি কমান্ডের মাধ্যমেই এর ডাম্প নেওয়া সম্ভব।
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 স্টার্ট হওয়ার সময় যেকোনো নতুন মাইগ্রেশন স্বয়ংক্রিয়ভাবে সম্পন্ন করে। ইমেজ পুল করার আগেই ডাম্প ফাইলটি নিয়ে রাখুন, কারণ একবার মাইগ্রেশন হয়ে গেলে তা আর রোল ব্যাক করা যায় না।
FAQ
রিভার্স প্রক্সি যোগ করার পর আমার শর্ট লিঙ্কগুলো কেন 404 এরর দেখাচ্ছে?
Shlink Host হেডারে থাকা ডোমেইনের সাথে শর্ট কোড মিলিয়ে দেখে। প্রক্সি যদি নিজের নাম বা ইন্টারনাল অ্যাড্রেস পাঠায়, তবে Shlink এমন একটি ডোমেইনের অধীনে কোডটি খোঁজে যেখানে কোনো লিঙ্ক নেই, তাই এটি 404 উত্তর দেয়। nginx লোকেশন ব্লকে proxy_set_header Host $host; সেট করুন এবং প্রক্সি রিলোড করুন। কন্টেইনার রিস্টার্ট ছাড়াই লিঙ্কগুলো সাথে সাথে কাজ করা শুরু করবে।
আমার কি Postgres প্রয়োজন, নাকি SQLite যথেষ্ট?
Shlink পরীক্ষা করে দেখার জন্য SQLite ঠিক আছে এবং এর জন্য আলাদা কোনো কন্টেইনারের প্রয়োজন হয় না। গুরুত্বপূর্ণ লিঙ্ক পাবলিশ করার আগে Postgres-এ চলে যান, কারণ প্রতিটি ক্লিকের সাথে ভিজিট রো (row) বাড়তে থাকে এবং SQLite রাইট অপারেশনগুলোকে সিরিয়ালাইজ করে। পরে সুইচ করতে চাইলে লিঙ্কগুলো এক্সপোর্ট এবং পুনরায় ইমপোর্ট করতে হবে, তাই শুরুতেই Postgres বেছে নিলে এই মাইগ্রেশনের ঝামেলা এড়ানো যায়।
আমি কি ভুলে যাওয়া API কি (key) পুনরুদ্ধার করতে পারি?
না। 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 রেকর্ড পরিবর্তন করুন। শর্ট কোড এবং সেগুলোর ভিজিট হিস্ট্রি অক্ষত থাকবে, কারণ সবকিছু ডাটাবেসের মধ্যেই থাকে।