วิธีติดตั้ง Shlink ทำ URL Shortener ส่วนตัวบน Docker
สร้างบริการย่อลิงก์ใช้งานเองบน VPS ด้วย Shlink และ Docker Compose ครบทุกขั้นตอนตั้งแต่ตั้งค่า DNS, ฐานข้อมูล Postgres, API keys ไปจนถึงการดูสถิติคลิกและ QR code
สิ่งที่คุณกำลังสร้าง
บริการย่อลิงก์แบบ self-hosted คือเซิร์ฟเวอร์ขนาดเล็กที่เปลี่ยนลิงก์ยาวให้เป็นลิงก์สั้นที่คุณเป็นเจ้าของ พร้อมทั้งนับจำนวนการคลิกในแต่ละลิงก์ Shlink เป็นตัวเลือกที่แนะนำเนื่องจากเป็นโอเพนซอร์ส มีการปล่อยซอฟต์แวร์ในรูปแบบ Docker image และสามารถทำงานได้ครบถ้วนภายในคอนเทนเนอร์เดียวร่วมกับฐานข้อมูล คู่มือนี้จะแนะนำการติดตั้งบน VPS โดยใช้โดเมนสั้นจริง พร้อมรองรับ HTTPS, API key, QR code และสถิติการคลิก
องค์ประกอบ 2 ส่วนที่ทำให้บริการนี้ทำงานเหมือนบริการย่อลิงก์เชิงพาณิชย์ ได้แก่ API server ซึ่งทำหน้าที่ตอบสนองการเปลี่ยนเส้นทาง (redirect) และจัดเก็บข้อมูล ส่วนที่สองคือ web client ซึ่งเป็นแอปพลิเคชันแบบ static ที่แยกออกมาเพื่อสื่อสารกับ API ผ่านเบราว์เซอร์ของคุณ คุณสามารถเลือกใช้งานทั้งสองส่วน หรือจะรันเฉพาะ API แล้วสั่งงานผ่าน command line ก็ได้
หมายเลขเวอร์ชันที่ระบุในที่นี้เป็นเวอร์ชันล่าสุด ณ เดือนกรกฎาคม 2026 คือ Shlink 5.1 และ shlink-web-client 4.8
กำหนดโดเมนสั้นให้ชี้มาที่เซิร์ฟเวอร์ก่อน
โดเมนคือตัวผลิตภัณฑ์ s.example.com/abc123 คือลิงก์ที่ผู้ใช้งานจะมองเห็น ดังนั้นควรเลือกชื่อที่สั้นและกำหนดให้เรียบร้อยก่อนเริ่มติดตั้ง Shlink จะจัดเก็บโดเมนไว้พร้อมกับทุก URL ย่อ หากเปลี่ยนโดเมนในภายหลัง ลิงก์ทั้งหมดที่คุณเคยแจกจ่ายไปจะใช้งานไม่ได้ทันที
สร้าง DNS A record หนึ่งรายการสำหรับโดเมนสั้น โดยให้ชี้ไปยัง public IPv4 address ของ VPS ของคุณ หากเซิร์ฟเวอร์รองรับ IPv6 ให้เพิ่ม AAAA record เข้าไปด้วย จากนั้นตรวจสอบให้แน่ใจว่าโดเมนสามารถ resolve ได้ก่อนดำเนินการต่อ
dig +short s.example.com Aผลลัพธ์ที่ได้ควรเป็นที่อยู่ของเซิร์ฟเวอร์ของคุณ หากไม่มีผลลัพธ์แสดงออกมา แสดงว่า record ยังไม่กระจายตัว (propagate) และทุกขั้นตอนหลังจากนี้จะล้มเหลวโดยหาสาเหตุได้ยาก เนื่องจากใบรับรอง TLS (transport layer security) ไม่สามารถออกให้แก่ชื่อที่ไม่สามารถ resolve ได้
ไฟล์ 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 จะเขียนกฎการส่งต่อข้อมูลของตนเองไว้ก่อน firewall ของโฮสต์ ซึ่งหมายความว่าบรรทัด 8080:8080 แบบปกติจะเปิดเผยแอปพลิเคชันออกสู่สาธารณะ แม้บนเครื่องที่ดูเหมือนว่า firewall ปิดอยู่ก็ตาม การผูกไว้กับ loopback address จะช่วยป้องกันปัญหานี้ รูปแบบเดียวกันนี้ใช้ได้กับทุกแอปที่คุณรันด้วยวิธีนี้ และมีรายละเอียดเพิ่มเติมอยู่ใน คู่มือการใช้งาน 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การเริ่มทำงานครั้งแรกจะดำเนินการย้ายข้อมูลฐานข้อมูล (migrations) จึงใช้เวลานานกว่าครั้งถัดไป เมื่อระบบทำงานเสถียรแล้ว ให้ตรวจสอบว่าบริการตอบสนองในระดับ local หรือไม่
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 และเริ่มการทำงานใหม่ทั้งหมด
การทำ TLS termination ไว้ด้านหน้า
Shlink ให้บริการผ่าน HTTP ปกติบนพอร์ต 8080 การจัดการ TLS ควรทำที่ reverse proxy โดยการตั้งค่าที่สำคัญที่สุดคือการส่งต่อชื่อโฮสต์ต้นทาง Shlink จะตัดสินว่ารหัสย่อ (short code) อยู่ภายใต้โดเมนใดโดยการอ่านค่าจาก header 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;
}
}จากนั้นจึงออกใบรับรอง (certificate) ขั้นตอนการดำเนินการทั้งหมด รวมถึงการตั้งเวลาต่ออายุ สามารถดูได้ที่ คู่มือ 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:// ซึ่งจะทำให้เกิดการ redirect ซ้ำซ้อน ส่งผลให้เสียเวลาในการรับส่งข้อมูล (round trip) และแสดงผลไม่ถูกต้องในเว็บไคลเอนต์
การสร้าง API key
ไม่มีบริการใดสามารถสื่อสารกับ API ได้หากไม่มี key ให้สร้าง key ผ่าน CLI ภายใน container
sudo docker compose exec shlink shlink api-key:generate --name "web client"คำสั่งจะแสดง key ออกมาเพียงครั้งเดียว ให้คัดลอกเก็บไว้ทันทีเนื่องจากระบบจะจัดเก็บในรูปแบบ hash และไม่สามารถเรียกดูซ้ำได้ shlink api-key:list จะแสดงเฉพาะชื่อและสถานะการเปิดใช้งานของแต่ละ key เท่านั้น ไม่แสดงตัว key หากต้องการยกเลิกการใช้งานให้ใช้ shlink api-key:disable ตามด้วยชื่อของ key
การเรียกใช้งาน REST ทุกครั้งจะต้องแนบ key ไปใน header ที่ชื่อ X-Api-Key
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsหากได้รับ JSON object ที่มี key เป็น shortUrls แสดงว่า key นั้นใช้งานได้ หากได้รับ 401 พร้อมข้อความ INVALID_API_KEY แสดงว่า 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 จะต้องไม่ซ้ำกันในแต่ละโดเมน ดังนั้นหากพยายามใช้ 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 แบบฟรีที่ Shlink ใช้สำหรับดาวน์โหลดฐานข้อมูล GeoLite2 หากไม่มีคีย์นี้ ระบบจะยังคงบันทึกการเข้าชมไว้ เพียงแต่จะไม่ระบุตำแหน่งที่ตั้งเท่านั้น
เว็บไคลเอนต์และ QR code
เว็บไคลเอนต์ขณะนี้อยู่ที่ 127.0.0.1:8081 และจำเป็นต้องมีรายการ proxy ของตนเอง หรือใช้ SSH tunnel หากคุณไม่ต้องการเปิดเผยสู่สาธารณะ ในการโหลดครั้งแรก ระบบจะถามหา URL ของเซิร์ฟเวอร์และ API key ให้กรอก https://s.example.com และคีย์ที่คุณสร้างขึ้น ไคลเอนต์จะเก็บข้อมูลทั้งสองอย่างไว้ใน browser storage และเรียกใช้ API ของคุณโดยตรง จึงไม่มีข้อมูลใดผ่านไปยังบุคคลอื่น การแยกอินเทอร์เฟซออกจาก API เป็นรูปแบบที่น่าสังเกต เพราะเป็นรูปแบบเดียวกับที่ช่วยให้ Halcyon ปรับโฉมคลังสื่อ Jellyfin ให้เหมือนร้านเช่าวิดีโอในยุค 1990 โดยไม่ต้องแก้ไขตัว media server ที่อยู่เบื้องหลัง
QR code ไม่จำเป็นต้องตั้งค่าใดๆ เพียงต่อท้าย /qr-code เข้ากับ URL แบบย่อใดๆ แล้ว API จะส่งคืนรูปภาพกลับมาให้
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size คือความกว้างในหน่วยพิกเซล โดยรับค่าตั้งแต่ 50 ถึง 1000 และมีค่าเริ่มต้นที่ 300 ส่วน format คือ png หรือ svg สำหรับ margin คือพื้นที่ว่างรอบ QR code ในหน่วยพิกเซล ซึ่งขนาดของรูปภาพที่เสร็จสมบูรณ์จะเท่ากับขนาดที่กำหนดบวกด้วยระยะขอบสองเท่า ให้เพิ่ม errorCorrection=Q สำหรับโค้ดที่ยังคงสแกนได้แม้จะพิมพ์ในขนาดเล็กหรือถูกบดบังบางส่วน
การรักษาให้ระบบทำงานอยู่เสมอ
บริการย่อลิงก์อาจหยุดทำงานโดยไม่มีการแจ้งเตือน ลิงก์ต่างๆ จะไม่เปลี่ยนเส้นทางและไม่มีใครแจ้งให้คุณทราบ เนื่องจากผู้ที่คลิกมักเข้าใจว่าลิงก์นั้นเสียไปแล้ว ให้ตั้งค่าการตรวจสอบ uptime ไปที่ URL ย่อจริงแทนที่จะเป็นหน้าแรก และตั้งค่าการแจ้งเตือนหากผลลัพธ์ไม่ใช่การเปลี่ยนเส้นทาง Uptime Kuma ที่ติดตั้งเอง สามารถทำหน้าที่นี้ได้ดีและสามารถตรวจสอบรหัสสถานะ (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 จะดำเนินการ migration ใหม่ๆ โดยอัตโนมัติเมื่อเริ่มทำงาน ให้ทำการดัมพ์ข้อมูลก่อนที่คุณจะ pull อิมเมจใหม่เสมอ เพราะการ migration ไม่สามารถย้อนกลับได้
FAQ
ทำไมลิงก์สั้นของฉันถึงแสดงข้อผิดพลาด 404 หลังจากเพิ่ม reverse proxy?
Shlink จะจับคู่รหัสลิงก์สั้นกับโดเมนที่อยู่ใน header Host หาก proxy ส่งชื่อของตัวเองหรือที่อยู่ภายในไปให้ Shlink จะพยายามค้นหารหัสนั้นภายใต้โดเมนที่ไม่มีลิงก์อยู่ จึงตอบกลับเป็น 404 ให้ตั้งค่า proxy_set_header Host $host; ในบล็อก location ของ nginx แล้วโหลด proxy ใหม่ ลิงก์จะกลับมาใช้งานได้ทันทีโดยไม่ต้อง restart container
ฉันจำเป็นต้องใช้ Postgres หรือใช้แค่ SQLite ก็เพียงพอแล้ว?
SQLite เหมาะสำหรับการทดลองใช้งาน Shlink และไม่จำเป็นต้องใช้ container เพิ่มเติม แต่ควรย้ายไปใช้ Postgres ก่อนที่จะเผยแพร่ลิงก์ที่สำคัญ เนื่องจากแถวข้อมูลการเข้าชมจะเพิ่มขึ้นทุกครั้งที่มีการคลิก และ SQLite จะทำการเขียนข้อมูลแบบลำดับ (serialise) การย้ายในภายหลังหมายถึงการต้อง export และ import ลิงก์ของคุณใหม่ ดังนั้นการเลือกใช้ Postgres ตั้งแต่เริ่มต้นจะช่วยให้คุณไม่ต้องทำการย้ายข้อมูลในภายหลัง
ฉันสามารถกู้คืน API key ที่ลืมคัดลอกไว้ได้หรือไม่?
ไม่ได้ Shlink จะเก็บค่า hash ของ key ไว้ ดังนั้น api-key:list จะแสดงเพียงชื่อและสถานะ แต่จะไม่แสดงค่าของ key ให้สร้าง key ใหม่ด้วย shlink api-key:generate แล้วนำไปวางใน web client จากนั้นให้ปิดการใช้งาน key เดิมด้วย shlink api-key:disable เพื่อให้ key นั้นหยุดทำงาน
ทำไมคอลัมน์ประเทศในสถิติการเข้าชมถึงว่างเปล่า?
การระบุตำแหน่งทางภูมิศาสตร์จำเป็นต้องใช้ฐานข้อมูล GeoLite2 ซึ่ง Shlink จะดาวน์โหลดก็ต่อเมื่อคุณระบุ GEOLITE_LICENSE_KEY ให้เท่านั้น คุณสามารถขอ key ได้ฟรีจาก MaxMind ให้เพิ่ม key นี้ลงในส่วน environment แล้วสร้าง container ขึ้นมาใหม่ การเข้าชมครั้งใหม่จะถูกระบุตำแหน่งได้ ส่วนการเข้าชมที่บันทึกไว้ก่อนหน้านี้จะยังคงว่างเปล่าจนกว่าคุณจะรัน shlink visit:locate
ฉันจะย้าย Shlink ไปยังเซิร์ฟเวอร์อื่นได้อย่างไร?
ให้คงโดเมนเดิมไว้แล้วย้ายข้อมูล ให้ dump ฐานข้อมูลด้วย pg_dump จากนั้นคัดลอกไฟล์ dump และไฟล์ compose ไปยังเซิร์ฟเวอร์ใหม่ เริ่มการทำงานของ stack แล้ว restore ข้อมูลจาก dump ลงในฐานข้อมูลที่ว่างเปล่าก่อนที่จะมี traffic จริงเข้ามา ให้เปลี่ยนค่า DNS เป็นขั้นตอนสุดท้าย รหัสลิงก์สั้นและประวัติการเข้าชมจะยังคงอยู่ครบถ้วนเนื่องจากข้อมูลทั้งหมดถูกเก็บไว้ในฐานข้อมูล