SSD Nodes Learn 8GB RAM — $66/rok
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-01

Shlink: własny skracacz URL na VPS z Docker Compose

Wdrożenie Shlink na VPS z Docker Compose: krótka domena DNS, Postgres, klucze API, klient webowy, kody QR i statystyki kliknięć. Wersje: 5.1 i 4.8.

Co jest wdrażane

Samodzielnie utrzymywana usługa skracania adresów URL to niewielki serwer, który zamienia długi adres na krótki adres należący do użytkownika i zlicza każde jego użycie. W tym celu należy wybrać Shlink: jest to oprogramowanie open source, udostępniane jako obraz Docker, które realizuje całe zadanie w jednym kontenerze i z użyciem bazy danych. W tym przewodniku usługa jest wdrażana na VPS za rzeczywistą krótką domeną, z HTTPS, kluczem API, kodami QR i statystykami kliknięć.

Dwa elementy zapewniają funkcje typowe dla komercyjnej usługi skracania adresów. Serwer API obsługuje przekierowania i przechowuje dane. Klient webowy jest oddzielną aplikacją statyczną, która komunikuje się z tym API w przeglądarce. Można uruchomić oba elementy albo korzystać wyłącznie z API i sterować nim z wiersza poleceń.

Podane numery wersji odpowiadają stanowi aktualnemu w lipcu 2026: Shlink 5.1 i shlink-web-client 4.8.

Najpierw skieruj krótką domenę na serwer

Domena jest produktem. s.example.com/abc123 to odnośnik widoczny dla użytkowników, dlatego należy wybrać krótką nazwę przed rozpoczęciem instalacji. Shlink zapisuje domenę przy każdym skróconym adresie URL. Jej późniejsza zmiana sprawi, że wszystkie wcześniej udostępnione odnośniki przestaną działać.

Należy utworzyć jeden rekord DNS A dla krótkiej domeny i wskazać w nim publiczny adres IPv4 serwera VPS. Jeśli serwer obsługuje IPv6, należy dodać także rekord AAAA. Następnie należy potwierdzić poprawne rozwiązywanie nazwy przed kontynuowaniem.

dig +short s.example.com A

Wynik musi zawierać adres serwera. Jeśli wynik jest pusty, rekord nie został jeszcze rozpropagowany. Każdy kolejny krok zakończy się wtedy niejasnym błędem, ponieważ nie można wystawić certyfikatu TLS (transport layer security) dla nazwy, która nie jest rozwiązywana.

Plik Compose

Shlink wymaga bazy danych. SQLite sprawdza się podczas testów, ale w przypadku danych przeznaczonych do zachowania właściwym wyborem jest Postgres, ponieważ liczba rekordów wizyt rośnie, a Postgres lepiej obsługuje indeksy i równoczesne zapisy. Umieść poniższą zawartość w /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:

Oba opublikowane porty są dowiązane do 127.0.0.1, dlatego nic nie będzie dostępne z Internetu do czasu skonfigurowania serwera reverse proxy w następnej sekcji. Docker zapisuje własne reguły przekierowania przed zaporą hosta, co oznacza, że zwykły wpis 8080:8080 udostępni aplikację nawet na serwerze, którego zapora wydaje się zamknięta. Dowiązanie do adresu interfejsu loopback temu zapobiega. Ten sam wzorzec dotyczy każdej aplikacji uruchamianej w ten sposób. Został on dokładniej opisany w przewodniku po Docker Compose na VPS.

Hasło do bazy danych pochodzi z pliku .env znajdującego się obok pliku Compose, dlatego nie trafia do 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

Uruchom usługę i monitoruj uruchamianie API.

cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlink

Przy pierwszym uruchomieniu wykonywane są migracje bazy danych, dlatego trwa ono dłużej niż kolejne uruchomienia. Po zakończeniu sprawdź lokalnie, czy usługa odpowiada.

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/health

Wynik 200 oznacza, że API działa, a połączenie z bazą danych jest poprawne. Wynik 500 w tym miejscu niemal zawsze wskazuje na problem z bazą danych: wartość DB_PASSWORD w .env nie jest zgodna z wartością używaną podczas tworzenia Postgresa, ponieważ obraz Postgresa odczytuje POSTGRES_PASSWORD tylko podczas inicjalizacji pustego katalogu danych. Późniejsza zmiana hasła nie przyniesie skutku, dopóki wolumin nie zostanie usunięty i usługa nie zostanie uruchomiona ponownie.

Shlink udostępnia zwykły HTTP na porcie 8080. Obsługę TLS należy skonfigurować w odwrotnym proxy. Najważniejsze jest przekazywanie oryginalnej nazwy hosta. Shlink określa, do której domeny należy skrócony kod, odczytując nagłówek Host. Jeśli proxy go zmieni, dla istniejących odnośników będą zwracane odpowiedzi 404, a statystyki odwiedzin zostaną przypisane do niewłaściwej domeny.

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;
    }
}

Następnie należy wystawić certyfikat. Pełna instrukcja, w tym konfiguracja automatycznego odnowienia, znajduje się w przewodniku Certbot dla nginx w Ubuntu 24.04.

sudo certbot --nginx -d s.example.com

Wartość IS_HTTPS_ENABLED: "true" w pliku compose powoduje, że Shlink umieszcza https:// w zwracanych krótkich adresach URL. Sama nie włącza TLS. Należy pozostawić ją jako false za proxy HTTPS. W przeciwnym razie każdy odnośnik zwrócony przez API będzie odnośnikiem http://, który następnie wykona przekierowanie. Powoduje to dodatkowe żądanie i wygląda nieprawidłowo w kliencie internetowym.

Utwórz klucz API

Bez klucza żaden element nie może komunikować się z API. Wygeneruj klucz za pomocą interfejsu CLI wewnątrz kontenera.

sudo docker compose exec shlink shlink api-key:generate --name "web client"

Polecenie wyświetla klucz tylko raz. Skopiuj go teraz, ponieważ jest przechowywany w postaci skrótu i nie można go ponownie wyświetlić. shlink api-key:list wyświetla nazwy kluczy oraz informację, czy każdy z nich jest włączony, ale nigdy nie wyświetla samego klucza. Klucz można unieważnić za pomocą shlink api-key:disable i jego nazwy.

Każde żądanie REST zawiera klucz w nagłówku X-Api-Key.

curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urls

Obiekt JSON zawierający klucz shortUrls oznacza, że klucz działa. Odpowiedź 401 zawierająca INVALID_API_KEY oznacza, że klucz jest nieprawidłowy, wyłączony lub wygasł.

CLI to najszybszy sposób tworzenia linków. Dobrze współpracuje ze skryptami.

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 umożliwia utworzenie czytelnego linku zamiast wygenerowanego kodu. Slugi są unikatowe w obrębie domeny. Druga próba użycia zajętego sluga kończy się niepowodzeniem i nie powoduje cichego nadpisania pierwszego linku. --tag można powtarzać. Tagi służą do grupowania linków, dla których później mają być wyświetlane łączne statystyki.

Najpierw wyświetl istniejące elementy, a następnie sprawdź ruch jednego linku.

sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docs

short-url:visits wyświetla jeden wiersz dla każdego kliknięcia. Zawiera datę, stronę odsyłającą i user agent. Kolumny kraju i miasta pozostają puste, jeśli nie zostanie ustawiona zmienna środowiskowa GEOLITE_LICENSE_KEY. Jest to bezpłatny klucz MaxMind używany przez Shlink do pobierania bazy danych GeoLite2. Bez tego klucza wizyty nadal są rejestrowane, ale nie jest dla nich określana lokalizacja.

Klient internetowy i kody QR

Klient internetowy jest teraz dostępny pod adresem 127.0.0.1:8081 i wymaga osobnego wpisu proxy albo tunelu SSH, jeśli nie ma być publicznie udostępniany. Przy pierwszym uruchomieniu żąda adresu URL serwera i klucza API. Należy podać https://s.example.com oraz wygenerowany klucz. Klient przechowuje obie wartości w pamięci przeglądarki i wywołuje API bezpośrednio, dlatego żadne dane nie przechodzą przez inne podmioty.

Kody QR nie wymagają żadnej konfiguracji. Wystarczy dodać /qr-code do dowolnego krótkiego adresu URL, aby API zwróciło obraz.

https://s.example.com/docs/qr-code?size=500&format=svg&margin=20

size oznacza szerokość w pikselach i przyjmuje wartości od 50 do 1000; wartość domyślna to 300. format oznacza png lub svg. margin oznacza wolną przestrzeń wokół kodu w pikselach, a gotowy obraz ma rozmiar równy rozmiarowi kodu powiększonemu o dwukrotność marginesu. Należy dodać errorCorrection=Q, aby kod nadal można było odczytać po wydrukowaniu w małym rozmiarze lub częściowym zasłonięciu.

Utrzymanie działania

Skracacz może ulec cichej awarii. Odnośniki przestają przekierowywać, a nikt o tym nie informuje, ponieważ osoba, która kliknęła odnośnik, zakłada, że jest on nieaktywny. Należy skierować monitorowanie dostępności na rzeczywisty krótki adres URL, a nie na stronę główną, i generować alert dla każdej odpowiedzi, która nie jest przekierowaniem. Samodzielnie hostowana instancja Uptime Kuma dobrze się do tego nadaje i może monitorować określony kod statusu.

Należy tworzyć kopię zapasową bazy danych, a nie kontenera. Jedno polecenie wykonuje jej zrzut.

sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gz

Ten plik wraz z plikiem compose umożliwia odtworzenie całej usługi na nowym serwerze. Aktualizacje polegają na wykonaniu sudo docker compose pull, a następnie sudo docker compose up -d, a Shlink uruchamia wszystkie nowe migracje podczas startu. Zrzut należy utworzyć przed wykonaniem pull, ponieważ migracji nie można wycofać.

FAQ

Dlaczego moje krótkie linki zwracają błąd 404 po dodaniu reverse proxy?

Shlink dopasowuje krótki kod do domeny z nagłówka Host. Proxy, które przekazuje własną nazwę lub adres wewnętrzny, powoduje, że Shlink szuka tego kodu w domenie, w której nie ma linków. W rezultacie zwraca błąd 404. Należy ustawić proxy_set_header Host $host; w bloku lokalizacji nginx i przeładować proxy. Linki zaczną działać od razu, bez ponownego uruchamiania kontenera.

Czy potrzebuję Postgres, czy wystarczy SQLite?

SQLite wystarcza do testowania Shlink i nie wymaga drugiego kontenera. Przed opublikowaniem istotnych linków należy przejść na Postgres, ponieważ liczba rekordów wizyt rośnie przy każdym kliknięciu, a SQLite serializuje operacje zapisu. Późniejsza zmiana wymaga wyeksportowania i ponownego zaimportowania linków. Wybór Postgres na początku pozwala uniknąć tej migracji.

Czy można odzyskać klucz API, którego nie udało się skopiować?

Nie. Shlink przechowuje skrót klucza, dlatego api-key:list wyświetla jego nazwę i status, ale nigdy samą wartość. Należy wygenerować nowy klucz za pomocą shlink api-key:generate, wkleić go do klienta internetowego, a następnie wyłączyć stary za pomocą shlink api-key:disable, aby przestał działać.

Dlaczego kolumny krajów w statystykach wizyt są puste?

Geolokalizacja wymaga bazy danych GeoLite2, którą Shlink pobiera tylko po przekazaniu GEOLITE_LICENSE_KEY. Klucz jest dostępny bezpłatnie w MaxMind. Należy dodać go do sekcji środowiska i ponownie utworzyć kontener. Nowe wizyty zostaną wtedy zlokalizowane. Wizyty zarejestrowane wcześniej pozostaną bez danych do czasu uruchomienia shlink visit:locate.

Należy zachować domenę i przenieść dane. Bazę danych należy zrzucić za pomocą pg_dump, skopiować zrzut oraz plik compose na nowy serwer, uruchomić stos, a następnie odtworzyć zrzut w pustej bazie danych przed pojawieniem się rzeczywistego ruchu. Rekord DNS należy zmienić na końcu. Krótkie kody i historia ich wizyt zostaną zachowane, ponieważ wszystkie dane znajdują się w bazie danych.

#shlink#url-shortener#self-hosting#docker#postgres