Tools beginner ⏱ 30 minutes

Shlink: Modern URL Shortener

Feature-rich URL shortener with analytics. API-first design with QR codes.

By Homelab Team • 2025-01-15
#shlink#url-shortener#analytics#api#docker

Introduction

In today’s digital landscape, URL shorteners have become essential tools for sharing links efficiently, tracking clicks, and managing marketing campaigns. While services like Bitly and TinyURL dominate the market, they come with limitations: paywalls for advanced features, data privacy concerns, and dependency on third-party services. Enter Shlink—a powerful, modern, open-source URL shortener that you can self-host in your homelab.

Shlink stands out in the self-hosted ecosystem with its polished REST API, real-time analytics, QR code generation, custom domain support, and comprehensive tracking capabilities. Whether you’re managing marketing campaigns, sharing links on social media, or simply want to create memorable short URLs for your services, Shlink provides enterprise-grade features without the enterprise price tag.

What you’ll learn:

  • Deploy Shlink with Docker Compose in under 15 minutes
  • Configure custom domains for branded short links
  • Use the REST API to automate URL creation
  • Set up the web interface for visual management
  • Track visits with detailed analytics and geolocation
  • Integrate QR codes for mobile-friendly sharing
  • Configure browser extensions for one-click shortening

Prerequisites:

  • Docker and Docker Compose installed
  • Basic understanding of Docker containers
  • A domain name (optional, but recommended for custom short domains)
  • 512MB+ RAM available
  • Basic familiarity with REST APIs

The URL Shortening Landscape

Traditional URL shorteners present several challenges:

  1. Privacy concerns: Your links and analytics data stored on third-party servers
  2. Feature limitations: Advanced analytics, custom domains, and API access locked behind paywalls
  3. Reliability: Services can shut down, taking your links with them
  4. Branding: Generic short domains don’t reinforce your brand

Shlink solves these issues elegantly:

  • Complete privacy: All data stays on your infrastructure
  • Free features: API, analytics, custom domains, QR codes—all included
  • Self-reliance: You control the service and data retention
  • Custom branding: Use your own short domain like go.yourdomain.com

Key Features at a Glance

FeatureDetails
REST APIComplete API for automation and integrations
Custom DomainsSupport unlimited custom short domains
QR CodesAutomatic QR code generation for every short URL
AnalyticsVisit tracking with geolocation, referrers, devices, browsers
Tags & OrganizationTag URLs for easy categorization and filtering
Real-time StatsWebSocket-powered real-time visit notifications
Import/ExportImport from Bitly, export data anytime
Multi-userAPI key management for multiple users/applications
Database SupportMySQL, MariaDB, PostgreSQL, SQL Server, or SQLite

Understanding URL Shortening Architecture

When someone accesses a short URL, here’s what happens:

  1. Request arrives: User visits https://go.yourdomain.com/abc123
  2. Lookup: Shlink queries database for short code abc123
  3. Tracking: Records visit metadata (IP, user agent, referrer, timestamp)
  4. Redirect: HTTP 302/301 redirect to original long URL
  5. Analytics: Visit data processed for real-time analytics

Tracking Capabilities

Shlink captures comprehensive visit data:

  • Geographic data: Country, city, region via IP geolocation
  • Device info: Operating system, browser, device type
  • Referrer tracking: Where visitors came from
  • Click timing: Date, time, timezone
  • Bot detection: Distinguishes humans from automated traffic

Installation: Docker Compose Method

Step 1: Create Project Directory

mkdir -p ~/shlink
cd ~/shlink

Step 2: Create docker-compose.yml

This configuration deploys Shlink with PostgreSQL database:

version: '3.8'

services:
  shlink:
    image: shlinkio/shlink:stable
    container_name: shlink
    restart: always
    ports:
      - "8080:8080"
    environment:
      DEFAULT_DOMAIN: go.yourdomain.com
      IS_HTTPS_ENABLED: "true"
      GEOLITE_LICENSE_KEY: YOUR_LICENSE_KEY_HERE
      DB_DRIVER: postgres
      DB_NAME: shlink
      DB_USER: shlink
      DB_PASSWORD: secure_password_here
      DB_HOST: database
      DB_PORT: 5432
      # Timezone configuration
      TIMEZONE: America/New_York
      # Short code generation settings
      DEFAULT_SHORT_CODES_LENGTH: 5
      # Redirect behavior
      REDIRECT_STATUS_CODE: 302
      # Tracking options
      ANONYMIZE_REMOTE_ADDR: "false"
    depends_on:
      - database
    networks:
      - shlink-net

  database:
    image: postgres:16-alpine
    container_name: shlink-db
    restart: always
    environment:
      POSTGRES_DB: shlink
      POSTGRES_USER: shlink
      POSTGRES_PASSWORD: secure_password_here
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - shlink-net

  # Optional: Web client for visual management
  shlink-web-client:
    image: shlinkio/shlink-web-client:stable
    container_name: shlink-web
    restart: always
    ports:
      - "8081:80"
    networks:
      - shlink-net

networks:
  shlink-net:
    driver: bridge

Configuration notes:

  • DEFAULT_DOMAIN: Your short URL domain (without https://)
  • GEOLITE_LICENSE_KEY: Free key from MaxMind for geolocation (see below)
  • DEFAULT_SHORT_CODES_LENGTH: Length of generated short codes (5 = abc12)
  • REDIRECT_STATUS_CODE: 302 (temporary) or 301 (permanent) redirects
  • ANONYMIZE_REMOTE_ADDR: Set to “true” for GDPR compliance (anonymizes last IP octet)

For geolocation tracking:

  1. Visit: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
  2. Sign up for free account
  3. Generate license key
  4. Replace YOUR_LICENSE_KEY_HERE in docker-compose.yml

Without GeoLite key, geolocation tracking will be disabled, but all other features work normally.

Step 4: Secure Your Configuration

# Update database password
sed -i 's/secure_password_here/YOUR_STRONG_PASSWORD/g' docker-compose.yml

# Set permissions
chmod 600 docker-compose.yml

Step 5: Start Services

docker compose up -d

Step 6: Verify Installation

# Check containers are running
docker ps | grep shlink

# View logs
docker logs shlink
docker logs shlink-db

# Test API endpoint
curl http://localhost:8080/rest/health

Expected health check response:

{
  "status": "pass",
  "version": "3.7.0",
  "links": {
    "about": "https://shlink.io"
  }
}

Initial Configuration

Generate API Key

API keys are required for all Shlink operations:

# Generate API key with full permissions
docker exec -it shlink shlink api-key:generate

# Expected output:
# Generated API key: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Save this API key securely - you’ll need it for all API calls and web client configuration.

Configure Web Client

  1. Navigate to: http://YOUR_SERVER_IP:8081
  2. Click “Add server”
  3. Configure connection:
    • Server name: My Homelab
    • Server URL: http://YOUR_SERVER_IP:8080
    • API key: Paste key from above
  4. Click “Add server”
  5. Select server from dropdown

You should now see the Shlink dashboard with analytics overview.


Creating Short URLs

Method 1: Web Interface

The easiest method for manual URL creation:

  1. Open web client at http://YOUR_SERVER_IP:8081
  2. Click “Create short URL”
  3. Configure URL:
    • Long URL: https://www.example.com/very/long/path/to/page
    • Custom slug (optional): my-custom-slug becomes /my-custom-slug
    • Tags: marketing, social-media (for organization)
    • Max visits: Leave empty for unlimited
    • Valid since/until: Set date restrictions if needed
    • Domain: Select from configured domains
  4. Click “Create”

Result: Short URL like https://go.yourdomain.com/abc12 created instantly.

Method 2: REST API

For automation and programmatic creation:

# Create basic short URL
curl -X POST http://localhost:8080/rest/v3/short-urls   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://www.example.com/very/long/path",
    "tags": ["api", "automation"]
  }'

Response:

{
  "shortCode": "abc12",
  "shortUrl": "http://go.yourdomain.com/abc12",
  "longUrl": "https://www.example.com/very/long/path",
  "dateCreated": "2025-01-15T10:30:00Z",
  "tags": ["api", "automation"],
  "meta": {
    "validSince": null,
    "validUntil": null,
    "maxVisits": null
  },
  "domain": null,
  "title": null,
  "crawlable": false,
  "forwardQuery": true
}

Method 3: Custom Short Codes

Create memorable short codes:

curl -X POST http://localhost:8080/rest/v3/short-urls   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://github.com/shlinkio/shlink",
    "customSlug": "gh-shlink",
    "tags": ["github", "project"]
  }'

Result: Creates https://go.yourdomain.com/gh-shlink

Advanced URL Options

Create URL with all advanced features:

curl -X POST http://localhost:8080/rest/v3/short-urls   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://example.com/limited-time-offer",
    "customSlug": "offer-jan",
    "tags": ["marketing", "january-2025"],
    "maxVisits": 1000,
    "validSince": "2025-01-15T00:00:00Z",
    "validUntil": "2025-01-31T23:59:59Z",
    "findIfExists": true,
    "title": "January Special Offer",
    "crawlable": false,
    "forwardQuery": true
  }'

Options explained:

  • maxVisits: URL expires after 1,000 clicks
  • validSince/validUntil: URL only works during January 2025
  • findIfExists: Returns existing URL if long URL already shortened
  • crawlable: Controls if search engines can index the short URL
  • forwardQuery: Forward query parameters to destination

Custom Domain Setup

Use your own domain for branded short links.

DNS Configuration

Add DNS record for your short domain:

Type: A
Name: go.yourdomain.com
Value: YOUR_SERVER_IP
TTL: 3600

Or for Cloudflare Tunnel users:

Type: CNAME
Name: go.yourdomain.com
Value: YOUR_TUNNEL_ID.cfargotunnel.com
TTL: Auto

Reverse Proxy Configuration

Caddy (recommended for simplicity):

# /etc/caddy/Caddyfile
go.yourdomain.com {
    reverse_proxy localhost:8080
}
sudo systemctl reload caddy

Nginx:

# /etc/nginx/sites-available/shlink
server {
    server_name go.yourdomain.com;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/go.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/go.yourdomain.com/privkey.pem;
}

server {
    listen 80;
    server_name go.yourdomain.com;
    return 301 https://$server_name$request_uri;
}
sudo ln -s /etc/nginx/sites-available/shlink /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Test Custom Domain

# Create short URL with custom domain
curl -X POST http://localhost:8080/rest/v3/short-urls   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://example.com",
    "domain": "go.yourdomain.com"
  }'

# Test redirect
curl -I https://go.yourdomain.com/abc12

Expected response headers:

HTTP/2 302
location: https://example.com

API Usage Examples

List All Short URLs

curl -X GET "http://localhost:8080/rest/v3/short-urls?page=1&itemsPerPage=10"   -H "X-Api-Key: YOUR_API_KEY"

Get Single Short URL Details

curl -X GET http://localhost:8080/rest/v3/short-urls/abc12   -H "X-Api-Key: YOUR_API_KEY"

Edit Existing Short URL

curl -X PATCH http://localhost:8080/rest/v3/short-urls/abc12   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://example.com/updated-destination",
    "maxVisits": 500
  }'

Delete Short URL

curl -X DELETE http://localhost:8080/rest/v3/short-urls/abc12   -H "X-Api-Key: YOUR_API_KEY"

Get Visit Statistics

# Get visits for specific short URL
curl -X GET http://localhost:8080/rest/v3/short-urls/abc12/visits   -H "X-Api-Key: YOUR_API_KEY"

# Get visits with date filtering
curl -X GET "http://localhost:8080/rest/v3/short-urls/abc12/visits?startDate=2025-01-01&endDate=2025-01-31"   -H "X-Api-Key: YOUR_API_KEY"

Python Automation Script

import requests
from datetime import datetime

class ShlinkAPI:
    def __init__(self, base_url, api_key):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.headers = {
            'X-Api-Key': api_key,
            'Content-Type': 'application/json'
        }

    def create_short_url(self, long_url, custom_slug=None, tags=None):
        """Create a short URL"""
        endpoint = f"{self.base_url}/rest/v3/short-urls"
        payload = {'longUrl': long_url}

        if custom_slug:
            payload['customSlug'] = custom_slug
        if tags:
            payload['tags'] = tags

        response = requests.post(endpoint, json=payload, headers=self.headers)
        return response.json()

    def get_visits(self, short_code):
        """Get visit statistics for short URL"""
        endpoint = f"{self.base_url}/rest/v3/short-urls/{short_code}/visits"
        response = requests.get(endpoint, headers=self.headers)
        return response.json()

    def list_short_urls(self, page=1, items_per_page=10, tags=None):
        """List short URLs with optional filtering"""
        endpoint = f"{self.base_url}/rest/v3/short-urls"
        params = {'page': page, 'itemsPerPage': items_per_page}

        if tags:
            params['tags[]'] = tags

        response = requests.get(endpoint, params=params, headers=self.headers)
        return response.json()

# Usage example
shlink = ShlinkAPI('http://localhost:8080', 'YOUR_API_KEY')

# Create short URL
result = shlink.create_short_url(
    'https://example.com/long-url',
    custom_slug='my-link',
    tags=['python', 'automation']
)
print(f"Short URL created: {result['shortUrl']}")

# Get statistics
visits = shlink.get_visits('my-link')
print(f"Total visits: {visits['visits']['pagination']['totalItems']}")

Analytics and Tracking

Accessing Analytics

Web Interface:

  1. Open web client
  2. Click on any short URL
  3. View detailed analytics:
    • Total visits over time (graph)
    • Geographic distribution (map)
    • Top referrers
    • Device breakdown
    • Browser statistics
    • Operating systems

API:

# Get comprehensive visit data
curl -X GET http://localhost:8080/rest/v3/short-urls/abc12/visits   -H "X-Api-Key: YOUR_API_KEY"

Understanding Visit Metadata

Each visit records:

{
  "referer": "https://twitter.com",
  "date": "2025-01-15T14:23:45+00:00",
  "userAgent": "Mozilla/5.0...",
  "visitLocation": {
    "countryCode": "US",
    "countryName": "United States",
    "regionName": "California",
    "cityName": "San Francisco",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "timezone": "America/Los_Angeles"
  },
  "potentialBot": false
}

Export Analytics Data

# Export to CSV for analysis
curl -X GET "http://localhost:8080/rest/v3/short-urls/abc12/visits?format=csv"   -H "X-Api-Key: YOUR_API_KEY"   -o visits.csv

QR Code Generation

Every short URL automatically has a QR code available.

Get QR Code

Via Web Interface:

  1. Open short URL details
  2. Click “QR Code” button
  3. Download or display QR code

Via API:

# Get QR code PNG image
curl -X GET http://localhost:8080/rest/v3/short-urls/abc12/qr-code   -H "X-Api-Key: YOUR_API_KEY"   -o qrcode.png

# Customize QR code size and format
curl -X GET "http://localhost:8080/rest/v3/short-urls/abc12/qr-code?size=500&format=svg"   -H "X-Api-Key: YOUR_API_KEY"   -o qrcode.svg

Options:

  • size: Pixel width/height (default: 300)
  • format: png or svg (default: png)
  • margin: Margin around QR code (default: 0)

Embed QR Code in Websites

<!-- Direct image embed -->
<img src="https://go.yourdomain.com/abc12/qr-code?size=300"
     alt="QR Code" />

<!-- With custom styling -->
<img src="https://go.yourdomain.com/abc12/qr-code?size=400&format=png"
     alt="Scan to visit"
     style="border: 2px solid #333; border-radius: 8px; padding: 10px;" />

Browser Extensions

Shlink offers official browser extensions for quick URL shortening.

Install Extension

Chrome/Edge:

  1. Visit: https://chrome.google.com/webstore
  2. Search: “Shlink URL Shortener”
  3. Click “Add to Chrome/Edge”

Firefox:

  1. Visit: https://addons.mozilla.org
  2. Search: “Shlink URL Shortener”
  3. Click “Add to Firefox”

Configure Extension

  1. Click extension icon in browser toolbar
  2. Click “Add server”
  3. Enter settings:
    • Server URL: https://go.yourdomain.com
    • API key: Your API key
    • Server name: My Homelab
  4. Click “Save”

Using Extension

Method 1: Right-click context menu

  1. Right-click any link on webpage
  2. Select “Shorten with Shlink”
  3. Short URL copied to clipboard

Method 2: Extension popup

  1. Navigate to page you want to shorten
  2. Click extension icon
  3. Click “Shorten current URL”
  4. Short URL copied automatically

Method 3: Selection shortening

  1. Highlight URL text on page
  2. Right-click selection
  3. Select “Shorten selected URL”

Advanced Features

Import from Bitly

Migrate existing short URLs from Bitly:

docker exec -it shlink shlink short-url:import bitly YOUR_BITLY_API_KEY

Custom 404 Pages

Configure custom page for invalid short codes:

# Update docker-compose.yml environment
NOT_FOUND_REDIRECT_TO: https://yourdomain.com/404

Restart container:

docker compose restart shlink

URL Validation

Enable strict URL validation:

# Add to docker-compose.yml environment
VALIDATE_URLS: "true"

This checks that destination URLs are reachable before creating short URLs.

Multi-Domain Support

Add additional short domains:

# Create short URL with specific domain
curl -X POST http://localhost:8080/rest/v3/short-urls   -H "X-Api-Key: YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{
    "longUrl": "https://example.com",
    "domain": "alt.yourdomain.com"
  }'

Configure DNS and reverse proxy for each domain following the same process as primary domain.


Troubleshooting

Issue 1: Short URLs Return 404

Symptoms: Accessing short URL shows “Not Found” error.

Diagnosis:

# Check database connection
docker logs shlink | grep -i database

# Verify short code exists
docker exec -it shlink shlink short-url:list

Solutions:

  1. Ensure database container is running: docker ps | grep shlink-db
  2. Check database password matches in both containers
  3. Verify short code spelling (case-sensitive)

Issue 2: Geolocation Not Working

Symptoms: Visit analytics show “Unknown location”.

Solutions:

  1. Verify GeoLite license key is valid
  2. Check GeoIP database downloaded:
    docker exec -it shlink ls -la /etc/shlink/data/GeoLite2-City.mmdb
  3. If missing, restart container to trigger download:
    docker compose restart shlink

Issue 3: API Returns 401 Unauthorized

Symptoms: API calls fail with authentication error.

Solutions:

  1. Verify API key is correct
  2. Check API key hasn’t been revoked:
    docker exec -it shlink shlink api-key:list
  3. Generate new API key if needed:
    docker exec -it shlink shlink api-key:generate

Issue 4: High Database Size

Symptoms: Database consuming excessive disk space.

Solutions:

# Check visit count
docker exec -it shlink shlink visit:stats

# Delete visits older than 6 months
docker exec -it shlink shlink visit:delete --older-than=6months

# Optimize database
docker exec -it shlink-db psql -U shlink -d shlink -c "VACUUM FULL;"

Issue 5: Slow Redirects

Symptoms: Noticeable delay when accessing short URLs.

Diagnosis:

# Test redirect time
time curl -I https://go.yourdomain.com/abc12

# Check container resources
docker stats shlink --no-stream

Solutions:

  1. Add Redis caching (see docker-compose addition below)
  2. Increase database connection pool
  3. Optimize database indexes

Redis caching configuration:

# Add to docker-compose.yml
services:
  redis:
    image: redis:7-alpine
    container_name: shlink-redis
    restart: always
    networks:
      - shlink-net

  shlink:
    # Add to environment section
    environment:
      CACHE_NAMESPACE: shlink
      REDIS_SERVERS: redis:6379

Backup and Maintenance

Automated Backup Script

#!/bin/bash
# backup-shlink.sh

BACKUP_DIR="/backups/shlink"
DATE=$(date +%Y%m%d-%H%M%S)

# Create backup directory
mkdir -p "$BACKUP_DIR"

# Backup database
docker exec shlink-db pg_dump -U shlink shlink | gzip > "$BACKUP_DIR/shlink-db-$DATE.sql.gz"

# Backup configuration
cp ~/shlink/docker-compose.yml "$BACKUP_DIR/docker-compose-$DATE.yml"

# Remove backups older than 30 days
find "$BACKUP_DIR" -name "shlink-db-*.sql.gz" -mtime +30 -delete

echo "Backup completed: $DATE"

Make executable and schedule:

chmod +x backup-shlink.sh

# Add to crontab (daily at 2 AM)
crontab -e
0 2 * * * /root/backup-shlink.sh >> /var/log/shlink-backup.log 2>&1
cd ~/shlink

# Pull latest images
docker compose pull

# Restart with new images
docker compose up -d

# Check version
docker logs shlink | grep version

Monitor Health

# Health check script
#!/bin/bash
HEALTH=$(curl -s http://localhost:8080/rest/health | jq -r '.status')

if [ "$HEALTH" != "pass" ]; then
    echo "Shlink health check failed!" | mail -s "Alert: Shlink Down" [email protected]
fi

Conclusion

You now have a production-ready URL shortener with:

  • Custom branded short domains
  • Comprehensive analytics and tracking
  • REST API for automation
  • QR code generation
  • Browser extensions for quick shortening
  • Privacy-focused self-hosted infrastructure

Next steps:

  1. Configure custom domain for professional short links
  2. Install browser extension for daily use
  3. Explore API integrations with your applications
  4. Set up automated backups
  5. Create short URLs for social media campaigns

Shlink provides enterprise-level URL shortening capabilities without compromising on privacy or features. Whether you’re tracking marketing campaigns, creating memorable links for documentation, or building URL shortening into your applications, Shlink delivers a robust, modern solution.


Additional Resources

Official:

Tools:

Community:


Last Updated: January 2025 Version Covered: Shlink 3.7.0+