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 Architecture and Prerequisites
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.
Option A: PostgreSQL (Recommended)
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.
- Register for a Free License Key:
- Go to MaxMind Signup.
- Once registered, navigate to Manage License Keys and generate a new license key.
- Environment Variable Configuration:
- Assign the license key to
GEOLITE2_LICENSE_KEY. - Set
UPDATE_GEO_LITE_DB_ON_STARTUPto"true"to ensure that your local GeoLite2 database is automatically updated whenever the container starts. - 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.
Connecting to the Shlink Web Client
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.