Shlink URL Shortener: Deploy Your Own Link Management Hub in Docker

Learn how to deploy Shlink URL shortener using Docker Compose. Create, manage, and track short links with a fast, self-hosted REST API server.

Shlink is a self-hosted, self-contained URL shortener that exposes a robust REST API and a CLI. Unlike other URL shorteners, the Shlink server does not serve a web GUI by default; it is designed to run headlessly. Administrative operations are performed via the official Shlink Web Client (which runs entirely in the user's browser, communicating directly with your server's API) or via CLI commands within the container.

To host Shlink, you need: 1. A VPS running a Linux distribution (e.g., Ubuntu 22.04 LTS/24.04 LTS). 2. Docker and the Docker Compose plugin installed. 3. A registered domain or subdomain (e.g., s.example.com) pointed to your VPS IP via an A record. 4. A MaxMind GeoLite2 license key for IP address geolocation tracking.


Production Docker Compose Configuration

The following production-ready docker-compose.yml configures Shlink with a PostgreSQL database backend. It isolates the database inside the Docker network, exposes Shlink on localhost port 8080 (ready for a reverse proxy), and includes environment variables for geo-targeting, SSL configuration, and domain management.

Create a directory (e.g., /opt/shlink) and place the following configuration in a file named docker-compose.yml:

version: '3.8'

services:
  shlink-db:
    image: postgres:15-alpine
    container_name: shlink-db
    restart: always
    environment:
      POSTGRES_DB: shlink
      POSTGRES_USER: shlink_user
      POSTGRES_PASSWORD: HyrweWcX1y5w8OIRORqziPf1
    volumes:
      - shlink_db_data:/var/lib/postgresql/data
    networks:
      - shlink-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shlink_user -d shlink"]
      interval: 10s
      timeout: 5s
      retries: 5

  shlink-server:
    image: shlink/shlink:stable
    container_name: shlink-server
    restart: always
    depends_on:
      shlink-db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      # Database settings
      DB_DRIVER: postgres
      DB_HOST: shlink-db
      DB_PORT: 5432
      DB_NAME: shlink
      DB_USER: shlink_user
      DB_PASSWORD: HyrweWcX1y5w8OIRORqziPf1

      # Server settings
      DEFAULT_DOMAIN: s.example.com
      IS_HTTPS_ENABLED: "true"
      API_KEY_IN_SUBREQUESTS: "true"

      # Geolocation tracking
      GEOLITE2_LICENSE_KEY: YOUR_MAXMIND_LICENSE_KEY
      UPDATE_GEO_LITE_DB_ON_STARTUP: "true"

      # URL redirection options
      REDIRECT_STATUS_CODE: 301 # 301 Permanent or 302 Temporary
      REDIRECT_TO_INVALID_URL: https://example.com/404
      REDIRECT_TO_EMPTY_URL: https://example.com

      # Privacy settings
      ANONYMIZE_REMOTE_ADDR: "true" # Compliance with GDPR
      TRACK_ORPHAN_VISITS: "true"
      TRACK_PARAM_EXTRACTION: "true"
    volumes:
      - shlink_data:/etc/shlink
    networks:
      - shlink-network

networks:
  shlink-network:
    driver: bridge

volumes:
  shlink_db_data:
  shlink_data:

Detailed Database Configuration: PostgreSQL vs. MariaDB

Shlink supports multiple database engines, but PostgreSQL and MariaDB are the industry standards for production deployments.

PostgreSQL is the preferred database backend for Shlink due to its performance with indexes and concurrency. - DB_DRIVER: postgres - Port: 5432 - Volume persistence: /var/lib/postgresql/data

Option B: MariaDB

If your existing infrastructure is built around MySQL/MariaDB, modify the docker-compose.yml services as follows:

  shlink-db:
    image: mariadb:10.11
    container_name: shlink-db
    restart: always
    environment:
      MYSQL_DATABASE: shlink
      MYSQL_USER: shlink_user
      MYSQL_PASSWORD: HyrweWcX1y5w8OIRORqziPf1
      MYSQL_ROOT_PASSWORD: MubchwNYoCBkyUN5c5wEzZeq
    volumes:
      - shlink_db_data:/var/lib/mysql
    networks:
      - shlink-network
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

And update the shlink-server env:

      DB_DRIVER: maria
      DB_PORT: 3306

GeoLite2 Integration for Visitor Geolocation

Shlink automatically tracks visitor metrics, including country and city of origin. To make this work, it integrates with MaxMind's GeoLite2 databases.

  1. Register for a Free License Key:
  2. Go to MaxMind Signup.
  3. Once registered, navigate to Manage License Keys and generate a new license key.
  4. Environment Variable Configuration:
  5. Assign the license key to GEOLITE2_LICENSE_KEY.
  6. Set UPDATE_GEO_LITE_DB_ON_STARTUP to "true" to ensure that your local GeoLite2 database is automatically updated whenever the container starts.
  7. Cron Job Updates: Shlink handles periodic downloads of the GeoLite2 database automatically in the background using an internal schedule.

Reverse Proxy Configuration (Nginx & SSL)

Since Shlink is bound to 127.0.0.1:8080, you must use a reverse proxy to handle HTTPS traffic, SSL termination, and HTTP/2.

Below is a complete, production-ready Nginx configuration block.

Step 1: Obtain a Let's Encrypt SSL Certificate

Run Certbot to obtain a certificate for your domain:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --nginx -d s.example.com

Step 2: Nginx Server Block Configuration

Create an Nginx configuration file (e.g., /etc/nginx/sites-available/shlink) and symlink it:

server {
    listen 80;
    listen [::]:80;
    server_name s.example.com;

    # Redirect all HTTP traffic to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name s.example.com;

    # SSL Certificates from Certbot
    ssl_certificate /etc/letsencrypt/live/s.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/s.example.com/privkey.pem;

    # Security Protocols and Ciphers
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Proxy to Shlink Server
    location / {
        proxy_pass http://127.0.0.1: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;

        # Disable buffering to allow real-time response streams if needed
        proxy_buffering off;
    }

    # Custom 404 page redirect rules handled by Nginx or passed to Shlink
    error_page 404 /index.php;
}

Enable the configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/shlink /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Deployment and Administration

Running the Stack

In the directory containing your docker-compose.yml, start the services in detached mode:

docker compose up -d

Verify that the containers are healthy:

docker compose ps

Generating an Admin API Key

To interact with the Shlink REST API or connect a web client, you must generate an API key. Run this command inside the running shlink-server container:

docker compose exec shlink-server shlink api-key:generate

Copy the generated key immediately. You will use it to authenticate external clients.

Because the Shlink server has no built-in UI, you can manage links using the official React-based frontend: 1. Open the hosted client at app.shlink.io (Note: No data is sent to Shlink's servers; the client runs entirely locally in your browser and calls your API directly). 2. Click Add server. 3. Input your short domain name (e.g., https://s.example.com) and the API key generated in the step above. 4. If you prefer to self-host the web client as well, add the following service block to your docker-compose.yml:

  shlink-web-client:
    image: shlink/shlink-web-client:stable
    container_name: shlink-web-client
    restart: always
    ports:
      - "127.0.0.1:8081:80"

You can then proxy a separate subdomain (e.g., shlink-admin.example.com) to 127.0.0.1:8081.