Jak zainstalować LinkBreeze na własnym serwerze VPS
Wdrożenie LinkBreeze przy użyciu Docker Compose i Caddy. Poradnik konfiguracji śledzenia kliknięć bez ciasteczek, przypinania wersji obrazów oraz zarządzania wolumenem SQLite.
Czym jest LinkBreeze
LinkBreeze to samodzielnie hostowana alternatywa dla Linktree: pojedynczy kontener Docker, który udostępnia publiczną stronę z linkami oraz panel administracyjny, przechowujący wszystkie dane w jednym pliku SQLite. Projekt jest udostępniony na licencji MIT, napisany w TypeScript z wykorzystaniem Next.js i opublikowany jako ghcr.io/manak-hash/linkbreeze. Do jego uruchomienia wymagany jest VPS, domena z rekordem A wskazującym na ten VPS, otwarte porty 80 i 443 oraz Docker Engine z wtyczką Compose.
Niniejszy przewodnik opisuje wdrożenie wspierane przez repozytorium: Docker Compose za reverse proxy, które samodzielnie pobiera certyfikaty. Omówiono również potencjalne awarie, ponieważ strona typu link-in-bio to publiczny adres URL, w który klikają inni użytkownicy, a niedziałający link oznacza utratę ruchu.
Przed przystąpieniem do działań należy mieć świadomość, jak wczesnym projektem jest to oprogramowanie.
Czy LinkBreeze jest wystarczająco dojrzały do obsługi publicznego profilu z linkami?
Według stanu na sierpień 2026 r. repozytorium posiada 178 gwiazdek, 17 forków i jednego opiekuna. Pierwsze oznaczone wydanie, v1.0.0, pochodzi z 1 lipca 2026 r. Jest to projekt mający kilka tygodni, a nie kilka lat.
The data behind this chart
[
{
"week": "2026-06-29",
"releases": 3,
"cumulative": 3
},
{
"week": "2026-07-06",
"releases": 3,
"cumulative": 6
},
{
"week": "2026-07-13",
"releases": 1,
"cumulative": 7
},
{
"week": "2026-07-20",
"releases": 2,
"cumulative": 9
},
{
"week": "2026-07-27",
"releases": 3,
"cumulative": 12
},
{
"week": "2026-08-03",
"releases": 2,
"cumulative": 14
},
{
"week": "2026-08-10",
"releases": 3,
"cumulative": 17
}
]Od wersji v1.0.0 projekt doczekał się 17 oznaczonych wydań w ciągu 7 tygodni kalendarzowych. Ostatni tydzień ujęty w tym zestawieniu trwał w momencie pisania niniejszego przewodnika i zawierał już 3 z nich.
Należy to odczytywać jako dwa odrębne fakty. Opiekun jest aktywny, a błędy są usuwane w ciągu kilku dni. Schemat danych oraz ustawienia domyślne wciąż jednak ulegają zmianom, więc instancja wdrożona i pozostawiona bez opieki znacząco odbiegnie od aktualnie tworzonego kodu.
Licencja chroni przed najgorszym scenariuszem. Połączenie licencji MIT, obrazu kontenera oraz pliku SQLite na własnym dysku oznacza, że w przypadku zaprzestania prac nad projektem, posiadane rozwiązanie będzie nadal działać. Nie chroni to jednak przed sytuacją, w której publicznie dostępna aplikacja przestaje otrzymywać poprawki bezpieczeństwa, co z czasem staje się obciążeniem. Należy wdrażać to rozwiązanie z założeniem regularnych aktualizacji oraz utrzymywać poniższą procedurę tworzenia kopii zapasowych od pierwszego dnia.
Przypnij tag obrazu i nie używaj latest
Proces wydawniczy wypycha dokładnie dwa tagi na wersję: latest oraz numer wersji z usuniętym prefiksem v. Przypiętym tagiem dla wydania v1.2.7 jest zatem ghcr.io/manak-hash/linkbreeze:1.2.7. Wpisanie :v1.2.7 nie pobiera niczego, a Docker zgłasza manifest unknown, ponieważ ten tag nigdy nie został wypchnięty.
Przypnij go, ponieważ latest zmienia się. Przy częstotliwości zmian przedstawionej w powyższej tabeli, docker compose pull względem latest stanowi niezweryfikowaną aktualizację strony, z której korzystają użytkownicy. Dzięki przypiętemu tagowi aktualizacja następuje dopiero w momencie edycji pliku.
Jeszcze jedna uwaga dotycząca obrazu. Proces wydawniczy buduje go bez ustawienia platforms:, więc opublikowany obraz jest wyłącznie linux/amd64. Na hoście arm64 pobieranie kończy się błędem no matching manifest for linux/arm64/v8 in the manifest list entries. Jeśli korzystasz z serwera VPS z architekturą ARM zamiast x86, zbuduj obraz bezpośrednio na tej maszynie:
git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .Następnie użyj linkbreeze:1.2.7 jako nazwy obrazu w poniższym pliku compose.
Wdrożenie LinkBreeze za serwerem Caddy z automatycznym TLS
Caddy samodzielnie żąda i odnawia certyfikaty z Let's Encrypt, więc TLS (transport layer security) nie wymaga osobnej konfiguracji certyfikatów. Całe wdrożenie składa się z trzech plików w jednym katalogu.
Najpierw wygeneruj klucz tajny:
mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .envSECRET_KEY podpisuje ciasteczko sesji administratora i stanowi sól dla hasha odwiedzających w analityce. Plik compose opublikowany w repozytorium domyślnie ustawia go na ${SECRET_KEY:-changeme-in-production}, więc instancja, w której pominiesz ten krok, będzie działać z kluczem podpisywania sesji, który jest publicznie dostępny w serwisie GitHub. Ustaw go przed pierwszym uruchomieniem, ponieważ późniejsza zmiana wyloguje użytkownika i zresetuje sól analityki.
Utwórz plik docker-compose.yml:
services:
linkbreeze:
image: ghcr.io/manak-hash/linkbreeze:1.2.7
restart: unless-stopped
volumes:
- linkbreeze-data:/app/data
environment:
- DATABASE_PATH=/app/data/linkbreeze.db
- SECRET_KEY=${SECRET_KEY}
- BASE_URL=https://links.example.com
networks:
- linkbreeze-net
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- linkbreeze-net
networks:
linkbreeze-net:
volumes:
linkbreeze-data:
caddy-data:
caddy-config:BASE_URL jest opcjonalne, ale warto je ustawić: informuje aplikację o jej rzeczywistym publicznym adresie, dzięki czemu żądanie przychodzące z sfałszowanym nagłówkiem Host nie spowoduje wygenerowania przez aplikację linków do cudzej domeny.
Utwórz obok plik Caddyfile, wpisując własną domenę:
links.example.com {
encode zstd gzip
reverse_proxy linkbreeze:3000
}Caddy domyślnie ustawia X-Forwarded-For oraz X-Forwarded-Proto w żądaniach przekazywanych przez proxy, od których zależy analityka. Uruchom usługę:
docker compose up -d
docker compose ps
docker compose logs -f caddydocker compose ps powinno wykazać, że kontener LinkBreeze ma status healthy. Obraz zawiera własny mechanizm sprawdzania stanu, wget --spider -q http://127.0.0.1:3000/api/health, więc nie trzeba dodawać go ręcznie. Nie kopiuj konfiguracji healthcheck z przykładu Caddy znajdującego się w repozytorium: wywołuje ona curl, a obraz bazuje na node:22-alpine, który zawiera busybox wget i nie posiada curl. Taki kontener zgłasza unhealthy, mimo że poprawnie obsługuje strony.
Otwórz https://links.example.com w przeglądarce. Pierwsza wizyta przekieruje do kreatora konfiguracji pod adresem /setup, który tworzy pojedyncze konto administratora. Następnie pulpit nawigacyjny będzie dostępny pod /dashboard, a formularz logowania pod /login. Konto to jest lokalne dla danej instancji i aplikacja nie posiada mechanizmu single sign-on, więc jeśli pulpit nawigacyjny ma korzystać z tych samych danych logowania co inne hostowane usługi, należy zastosować przed nim proxy typu forward auth, na przykład własną instancję Authentik.
Zwróć uwagę na to, czego plik compose nie robi: nie publikuje portu 3000. Tylko Caddy nasłuchuje na interfejsie publicznym. Jeśli składnia pliku Compose jest nowością, podstawy Docker Compose dla VPS omawiają założenia przyjęte w tym pliku, a jeśli przed aplikacją działa już inne rozwiązanie, porównanie Nginx, Caddy i Traefik wyjaśnia wymagane zmiany. Repozytorium zawiera działające przykłady dla Nginx z Certbot, Traefik oraz tunelu Cloudflare.
Gdzie przechowywane są dane i co powinien zawierać backup
DATABASE_PATH wskazuje na /app/data/linkbreeze.db. Przesłane awatary oraz miniatury linków są zapisywane obok niego w /app/data/uploads. Oba elementy znajdują się w wolumenie nazwanym linkbreeze-data, dlatego jednostką backupu jest cały wolumen, a nie sam plik bazy danych. Przywrócenie pliku bez katalogu z przesłanymi plikami spowoduje, że każdy obraz na stronie będzie zwracał błąd 404.
Wszystkie pozostałe dane znajdują się w tej jednej bazie: strony, linki, ustawienia, motyw, subskrybenci e-mail oraz wiersze analityki.
Wykonaj kopię przy zatrzymanym kontenerze:
docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreezeZatrzymaj usługę przed kopiowaniem, ponieważ kopiowanie bazy danych SQLite w trakcie zapisu może przechwycić niedokończoną transakcję, co sprawi, że kopia będzie uszkodzonym plikiem. Strona jest niedostępna podczas trwania procesu kopiowania. Przywracanie danych odbywa się w odwrotnej kolejności:
docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreezePanel sterowania oferuje również eksport do formatu JSON, dostępny pod adresem /api/backup jako linkbreeze-backup-YYYY-MM-DD.json. Zawiera on profil, linki, ustawienia oraz zapisane motywy. Nie obejmuje on historii analityki, subskrybentów e-mail ani przesłanych obrazów, a jego przywrócenie usuwa bieżące wiersze w tych czterech tabelach przed wstawieniem danych z pliku. Traktuj to jako migawkę konfiguracji przydatną przy przenoszeniu hosta lub cofaniu błędów edycji. Kopia wolumenu stanowi właściwy backup.
Obowiązują tutaj dwie zasady przechowywania danych, tak samo jak w przypadku uruchamiania SQLite w środowisku produkcyjnym na VPS. Przechowuj bazę danych na lokalnym dysku, ponieważ mechanizm blokowania SQLite jest zawodny w systemach plików sieciowych, a o błędzie dowiesz się dopiero w momencie uszkodzenia strony. Jeśli zamieniasz wolumen nazwany na host bind mount, najpierw zmień właściciela katalogu na hoście za pomocą chown: kontener działa jako użytkownik niebędący rootem node, o identyfikatorze uid 1000 w node:22-alpine, a katalog utworzony przez roota nie będzie dla niego zapisywalny. W rezultacie aplikacja nie otworzy bazy danych, a kontener zakończy działanie przy starcie. Bind mounts a wolumeny nazwane w Compose szczegółowo omawia ten kompromis.
Analityka i baner zgody, których nie potrzebujesz
To funkcja, która uzasadnia samodzielne hostowanie strony, którą można uzyskać za darmo gdzie indziej.
Analityka nie wykorzystuje plików cookie. Żaden plik cookie nie jest ustawiany dla odwiedzającego, a na stronie publicznej nie ładuje się żaden skrypt zewnętrzny. Odwiedzający jest identyfikowany za pomocą skrótu SHA-256 adresu IP, ciągu user agent oraz soli, skróconego do 16 znaków szesnastkowych. Sól jest sama w sobie skrótem bieżącej daty UTC oraz Twojego SECRET_KEY, więc zmienia się o północy czasu UTC, a skróty z wczoraj nie mogą zostać dopasowane do dzisiejszych. Surowy adres IP nigdy nie jest zapisywany w bazie danych.
Kliknięcia są zliczane na serwerze. Każdy link http na stronie publicznej wskazuje na /go/<id> w Twojej własnej domenie, co rejestruje kliknięcie, a następnie odpowiada przekierowaniem 302 do właściwego miejsca docelowego. Zliczanie działa zatem dla czytelników z wyłączoną obsługą JavaScript oraz wewnątrz przeglądów wewnątrz aplikacji, które blokują żądania w tle. Wyświetlenia stron są rejestrowane przez /api/track.
Warto znać dwa wykluczenia. Żądanie zawierające ważną sesję administratora jest pomijane, więc edytowanie własnej strony nie zawyża statystyk. Znane user agenty robotów indeksujących również są pomijane.
W kwestii zgody: na urządzeniu czytelnika nic nie jest przechowywane, a plik cookie przechowywany na urządzeniu czytelnika jest właśnie tym, o co prosi o zgodę baner plików cookie. Twoje obowiązki nadal zależą od miejsca zamieszkania czytelników, więc sprawdź je, ale tutaj nie ma pliku cookie śledzącego, który należałoby ujawnić, ani strony trzeciej otrzymującej dane.
Jedno zastrzeżenie, które zaskakuje użytkowników: obróć SECRET_KEY, a dzienna sól zmieni się wraz z nim, więc każdy powracający odwiedzający będzie od tego momentu liczony jako nowy.
Dlaczego kolumna kraju w analityce jest pusta?
Ponieważ żaden element stosu nie ustawia nagłówka kraju. LinkBreeze rozpoznaje kraj na podstawie nagłówków proxy, takich jak cf-ipcountry oraz x-vercel-ip-country. Na serwerze VPS działającym za własnym Caddy lub Nginx żaden z tych nagłówków nie istnieje, więc kraj jest zapisywany jako null, a zestawienie pozostaje puste. Wewnątrz kontenera nie ma bazy danych GeoIP.
Istnieją dwa sposoby na uzupełnienie tych danych. Umieszczenie Cloudflare przed domeną, co dodaje cf-ipcountry do każdego żądania przekazywanego przez proxy. Alternatywnie, ustawienie jednego z tych nagłówków we własnym reverse proxy na podstawie lokalnego wyszukiwania GeoIP.
Powiązany problem jest poważniejszy, więc należy go sprawdzić. Procedury obsługi kliknięć i wyświetleń odczytują adres klienta najpierw z X-Forwarded-For, następnie z X-Real-IP, a w przypadku braku obu nagłówków korzystają z 0.0.0.0. Upublicznienie portu 3000 bezpośrednio w Internecie bez proxy powoduje, że każdy odwiedzający jest przypisywany do tej samej wartości. Oznacza to, że liczba unikalnych użytkowników zawsze wynosi 1, a limit 60 zdarzeń na minutę na adres IP dotyczy wszystkich odbiorców jednocześnie. Po zastosowaniu dyrektywy reverse_proxy opisanej powyżej, Caddy automatycznie ustawia nagłówek i oba problemy zostają rozwiązane.
Import z Linktree oraz dane, które nie są przenoszone
Kreator migracji w panelu sterowania akceptuje publiczny adres URL profilu lub wyeksportowany plik. Rozpoznaje strony linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me oraz LittleLink, a także ogólne eksporty HTML i JSON. W przypadku adresów URL Linktree lub Bento odczytywany jest kod __NEXT_DATA__ JSON osadzony na tych stronach. W przypadku stron statycznych odczytywane są znaczniki kotwic.
Przenoszone są: tytuł, adres URL, opis i obraz każdego linku, informacja czy link jest profilem społecznościowym, a także nazwa wyświetlana, biografia oraz awatar. Użytkownik wybiera, które z odnalezionych linków zachować, zanim jakiekolwiek dane zostaną zapisane w bazie danych.
Nie są przenoszone: historia analityki, motyw i układ strony, subskrybenci poczty elektronicznej, zaplanowane daty publikacji oraz wszelkie dane, które stara platforma przechowuje za własnym panelem logowania. Należy zaplanować ręczne odtworzenie wyglądu i zaakceptować fakt, że stara historia kliknięć pozostaje w poprzednim serwisie.
Importer pobiera adres URL z poziomu serwera, a nie z przeglądarki, dlatego odrzuca adresy, które nie są publiczne. Komunikat Private/local URLs are not allowed oznacza podanie adresu wewnątrz własnej sieci; odmowa jest celowa: bez niej każda osoba z dostępem do panelu mogłaby użyć serwera do skanowania maszyn dostępnych tylko dla niego. Inne komunikaty, które mogą wystąpić, to Only http and https URLs are allowed, Request timed out oraz Response too large.
Scraping zależy od znaczników zewnętrznych serwisów. Jeśli kreator nie znajduje niczego na stronie, która ewidentnie zawiera linki, oznacza to, że platforma zmieniła strukturę HTML od czasu napisania parsera. Należy dodać linki ręcznie, zamiast czekać na poprawkę. Jeśli celem są mierzalne krótkie linki, a nie strona profilowa, samodzielnie hostowany skracacz adresów URL, taki jak Shlink wykonuje to zadanie i działa bez problemów na tym samym serwerze.
Aktualizacja przypiętego wdrożenia
# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreezeMigracje schematu bazy danych uruchamiają się automatycznie podczas startu kontenera. Brak jest udokumentowanej metody ich cofania, dlatego należy najpierw wykonać kopię wolumenu. Aktualizacja, której nie można wycofać, jest bezpieczna tylko wtedy, gdy istnieje możliwość przywrócenia stanu sprzed jej wykonania.
Panel sterowania wyświetla baner, gdy dostępna jest nowsza wersja oprogramowania. Sprawdzenie odbywa się poprzez pobranie niewielkiego pliku z informacją o wersji z repozytorium GitHub projektu raz na 24 godziny; proces ten nie przesyła żadnych danych na temat instancji. Przed zmianą tagu należy zapoznać się z informacjami o wydaniu (release notes), ponieważ na obecnym etapie rozwoju projektu wersja minor może zmieniać domyślne ustawienia, od których zależy działanie systemu.
Tryby awarii i komunikaty, które zobaczysz
manifest unknown podczas pobierania. Tag został zapisany jako :v1.2.7. Tagi w rejestrze nie posiadają v, więc użyj :1.2.7.
no matching manifest for linux/arm64/v8 in the manifest list entries. Opublikowany obraz jest dostępny wyłącznie dla architektury amd64. Zbuduj go na hoście ARM z otagowanego źródła.
Kontener zgłasza unhealthy, mimo że strona ładuje się poprawnie. Healthcheck w pliku compose wywołuje curl, którego obraz nie zawiera. Usuń go i pozwól działać wbudowanemu w obraz mechanizmowi wget.
Caddy zwraca błąd certyfikatu lub brak odpowiedzi. Sprawdź docker compose logs caddy. Typową przyczyną jest rekord A, który nie wskazuje jeszcze na ten VPS, lub zamknięty port 80 na firewallu, co blokuje wyzwanie HTTP (ACME - automatic certificate management environment), którego Caddy używa do weryfikacji kontroli nad domeną.
Liczba unikalnych użytkowników zatrzymała się na 1. Żadne proxy nie ustawia X-Forwarded-For, przez co każdy użytkownik jest haszowany w identyczny sposób.
Kontener wyłącza się zaraz po starcie, mimo że wczoraj działał. Jeśli nastąpiło przejście z wolumenu nazwanego na host bind mount, katalog danych jest własnością root, a aplikacja działa jako uid 1000, więc nie może otworzyć pliku bazy danych. sudo chown -R 1000:1000 katalog na hoście.
Żądania śledzenia odpowiadają kodem HTTP 429. Osiągnięto limit zapytań na adres IP dla /api/track oraz /go/<id>. Użytkownicy są nadal przekierowywani do celu, samo kliknięcie nie jest po prostu zliczane.
FAQ
Czy LinkBreeze jest gotowy do publicznego udostępnienia linku w bio?
Jest to młody projekt. Według stanu na sierpień 2026 r. repozytorium posiada 178 gwiazdek, 17 forków i jednego opiekuna, a pierwsze wydanie datowane jest na 1 lipca 2026 r. Wydania pojawiają się średnio częściej niż dwa razy w tygodniu, więc błędy są szybko naprawiane, a zachowanie aplikacji ulega częstym zmianom. Licencja MIT oraz lokalny plik SQLite oznaczają, że strona będzie działać nawet w przypadku wstrzymania prac rozwojowych, jednak publiczna aplikacja internetowa bez poprawek bezpieczeństwa staje się zagrożeniem. Należy traktować to oprogramowanie jako wymagające regularnych aktualizacji, a nie jako rozwiązanie typu „zainstaluj i zapomnij”.
Którego tagu obrazu LinkBreeze powinienem używać?
Należy używać tagu wersji, na przykład ghcr.io/manak-hash/linkbreeze:1.2.7, i zmieniać go w sposób świadomy. Proces wydawniczy wypycha jedynie latest oraz sam numer wersji, dlatego tag :v1.2.7 z dopiskiem v nie istnieje, a Docker zwraca błąd manifest unknown. Obraz jest budowany wyłącznie dla architektury linux/amd64, więc na serwerze VPS z architekturą arm64 konieczne jest sklonowanie repozytorium i zbudowanie obrazu lokalnie.
Dlaczego zestawienie krajów w analityce LinkBreeze pozostaje puste?
LinkBreeze odczytuje kraj odwiedzającego z nagłówków proxy, takich jak cf-ipcountry lub x-vercel-ip-country, i nie posiada własnej bazy danych GeoIP. Serwer VPS działający za własnym Caddy lub Nginx nie ustawia żadnego z tych nagłówków, dlatego kraj jest zapisywany jako null. Należy umieścić Cloudflare przed domeną lub skonfigurować reverse proxy tak, aby ustawiało jeden z tych nagłówków na podstawie lokalnego wyszukiwania GeoIP.
Co dokładnie należy archiwizować i jak przeprowadzić przywracanie?
Należy archiwizować cały wolumen linkbreeze-data, a nie tylko plik bazy danych. /app/data/linkbreeze.db przechowuje wszystkie linki, strony, ustawienia, subskrybentów oraz wiersze analityczne, a /app/data/uploads zawiera awatary i miniatury obrazów, do których odwołuje się strona. Należy zatrzymać kontener, wykonać docker compose cp linkbreeze:/app/data ./backup-$(date +%F), a następnie uruchomić go ponownie. Przywracanie polega na skopiowaniu katalogu z powrotem do zatrzymanego kontenera i jego uruchomieniu. Eksport JSON z panelu sterowania jest migawką konfiguracji profilu, linków, ustawień i motywów, która nie zawiera danych analitycznych ani obrazów.
Czy import z Linktree przenosi również analitykę i motyw?
Nie. Kreator migracji odczytuje tytuły linków, adresy URL, opisy i obrazy ze starego profilu publicznego, a także nazwę wyświetlaną, bio i awatar. Historia analityki, motyw, subskrybenci e-mail oraz zaplanowane daty publikacji pozostają na starej platformie. Po imporcie należy ponownie skonfigurować wygląd w edytorze motywów, a historia kliknięć pozostanie na poprzedniej platformie.