SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-15

Як розгорнути LinkBreeze на VPS через Docker Compose

Розгорніть LinkBreeze на VPS через Docker Compose і Caddy: закріпіть теги образів, увімкніть cookieless-відстеження кліків і збережіть сайт в одному volume.

Що таке LinkBreeze

LinkBreeze — це self-hosted альтернатива Linktree: один Docker-контейнер, який обслуговує публічну сторінку посилань у профілі та адміністративну панель. Увесь стан зберігається в одному файлі SQLite. Проєкт поширюється за ліцензією MIT, написаний мовою TypeScript на базі Next.js і опублікований як ghcr.io/manak-hash/linkbreeze. Для запуску потрібні VPS, домен із A-записом, що вказує на цей VPS, відкриті порти 80 і 443, а також Docker Engine із Compose plugin.

У цьому посібнику описано розгортання, яке фактично підтримує репозиторій: Docker Compose за reverse proxy, який сам отримує сертифікати. Також розглянуто типові проблеми. Посилання в профілі — це публічний URL, за яким переходять інші люди. Якщо воно не працює, ви втрачаєте переходи.

Передусім потрібно врахувати, наскільки новим є цей проєкт.

Чи достатньо зрілий LinkBreeze для публічного профільного посилання?

Станом на серпень 2026 року репозиторій має 178 зірок, 17 форків і одного мейнтейнера. Перший позначений реліз v1.0.0 датовано 1 липня 2026 року. Це проєкт, якому кілька тижнів, а не кілька років.

ChartLinkBreeze tagged releases per week, v1.0.0 to v1.2.7
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
  }
]

Після v1.0.0 проєкт випустив 17 позначених релізів протягом 7 календарних тижнів. Останній тиждень на цій діаграмі ще тривав, коли цей посібник готували, і в ньому вже було 3 таких релізів.

Розглядайте це як два окремі факти. Мейнтейнер активно працює, а помилки виправляють протягом кількох днів. Водночас схема та значення за замовчуванням усе ще змінюються, тому екземпляр, який ви розгорнули й залишили без нагляду, з часом може суттєво відхилитися від коду, що розробляється.

Ліцензія захищає вас від найгіршого сценарію. MIT, образ контейнера та файл SQLite на вашому диску означають, що в разі припинення розробки наявний у вас екземпляр продовжить працювати. Але ліцензія не захищає від ситуації, коли загальнодоступний вебзастосунок перестає отримувати виправлення безпеки та з часом стає відповідальністю. Розгортайте його як сервіс, який ви регулярно оновлюватимете, і від першого дня підтримуйте працездатність наведеної нижче процедури резервного копіювання.

Зафіксуйте тег образу й не використовуйте latest

Процес випуску публікує рівно два теги для кожної версії: latest і номер версії без початкового v. Тому зафіксований тег для випуску v1.2.7 — ghcr.io/manak-hash/linkbreeze:1.2.7. Запис :v1.2.7 нічого не завантажує, а Docker виводить manifest unknown, оскільки цей тег не було опубліковано.

Зафіксуйте тег, оскільки latest змінюється. За темпом, показаним на наведеній вище діаграмі, docker compose pull для latest є неперевіреним оновленням сторінки, якою користується ваша аудиторія. Із зафіксованим тегом оновлення відбувається лише після редагування файлу.

Є ще один важливий момент щодо образу. Процес випуску виконує збірку без параметра platforms:, тому опублікований образ підтримує лише linux/amd64. На хості arm64 завантаження завершується помилкою no matching manifest for linux/arm64/v8 in the manifest list entries. Якщо ви використовуєте ARM VPS замість x86, зберіть образ безпосередньо на цьому сервері:

git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .

Потім використайте linkbreeze:1.2.7 як ім’я образу у наведеному нижче compose-файлі.

Розгорніть LinkBreeze за Caddy з автоматичним TLS

Caddy самостійно запитує та поновлює сертифікати в Let's Encrypt, тому для TLS (transport layer security) не потрібен окремий крок налаштування сертифіката. Усе розгортання складається з трьох файлів в одному каталозі.

Спочатку згенеруйте секрет:

mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env

SECRET_KEY підписує cookie сесії адміністратора та додає сіль до хешу відвідувача в аналітиці. У compose-файлі, опублікованому в репозиторії, за замовчуванням вказано ${SECRET_KEY:-changeme-in-production}, тому екземпляр, у якому цей крок пропущено, використовує ключ підпису сесій, опублікований у відкритому доступі на GitHub. Встановіть його до першого запуску, оскільки подальша зміна призведе до виходу з облікового запису та скине сіль аналітики.

Створіть 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 необов’язковий, але його варто вказати: він повідомляє застосунку фактичну публічну адресу, тому запит із підробленим заголовком Host не зможе змусити застосунок генерувати посилання на чужий домен.

Створіть поруч Caddyfile, указавши власний домен:

links.example.com {
    encode zstd gzip
    reverse_proxy linkbreeze:3000
}

Caddy автоматично встановлює X-Forwarded-For і X-Forwarded-Proto для проксійованих запитів. Вони потрібні аналітиці. Запустіть стек:

docker compose up -d
docker compose ps
docker compose logs -f caddy

docker compose ps має показати контейнер LinkBreeze зі станом healthy. Образ містить власну перевірку стану — wget --spider -q http://127.0.0.1:3000/api/health, тому додавати її не потрібно. Не копіюйте перевірку стану з прикладу Caddy у самому репозиторії: вона викликає curl, а образ побудовано на node:22-alpine, який містить busybox wget і не містить curl. Через це контейнер повідомляє unhealthy, хоча сторінки працюють без проблем.

Відкрийте https://links.example.com у браузері. Під час першого відвідування відкривається майстер початкового налаштування за адресою /setup. Він створює єдиний обліковий запис адміністратора. Після цього панель керування доступна за адресою /dashboard, а форма входу — за адресою /login. Цей обліковий запис локальний для цього екземпляра, а в застосунку немає механізму єдиного входу. Тому, якщо панель керування має використовувати той самий обліковий запис, що й інші розміщені вами сервіси, це потрібно реалізувати через проксі forward auth перед застосунком, наприклад self-hosted Authentik.

Зверніть увагу, чого compose-файл не робить: він не публікує порт 3000. На публічному інтерфейсі слухає лише Caddy. Якщо синтаксис Compose-файлів для вас новий, у матеріалі Основи Docker Compose для VPS описано компоненти, які передбачає цей файл. Якщо перед LinkBreeze уже працює інший проксі, матеріал Порівняння Nginx, Caddy і Traefik пояснює необхідні зміни. У репозиторії є робочі приклади для Nginx із Certbot, Traefik і тунелю Cloudflare.

Де зберігаються дані і що має містити резервна копія

DATABASE_PATH вказує на /app/data/linkbreeze.db. Завантажені аватари та мініатюри посилань записуються поруч із ним у /app/data/uploads. Обидва каталоги розташовані в іменованому томі linkbreeze-data, тому одиницею резервного копіювання є том, а не лише файл бази даних. Якщо відновити файл без каталогу uploads, кожне зображення на сторінці повертатиме помилку 404.

Усі інші дані справді зберігаються в цій одній базі даних: сторінки, посилання, налаштування, тема, підписники електронної пошти та рядки аналітики.

Створюйте копію, коли контейнер зупинений:

docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreeze

Спочатку зупиніть контейнер, оскільки копіювання бази даних SQLite під час запису процесом може захопити незавершену транзакцію. У такому разі копія відкриється як пошкоджений файл. Поки виконується копіювання, сторінка недоступна. Відновлення виконується у зворотному порядку:

docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreeze

Панель керування також підтримує експорт у JSON, доступний у /api/backup за адресою linkbreeze-backup-YYYY-MM-DD.json. Він містить профіль, посилання, налаштування та збережені теми. Він не містить історію аналітики, підписників електронної пошти або завантажених зображень. Під час відновлення поточні рядки в цих чотирьох таблицях видаляються перед вставленням даних із файлу. Розглядайте цей файл як знімок конфігурації для перенесення на інший хост або скасування помилки редагування. Резервною копією є копія тому.

Тут діють ті самі два правила зберігання, що й у будь-якому іншому випадку використання SQLite у production на VPS. Зберігайте базу даних на локальному диску, оскільки блокування SQLite ненадійно працює на мережевій файловій системі. Пошкоджена сторінка бази даних зазвичай виявляє цю проблему вже після її виникнення. Якщо замінюєте іменований том на bind mount хоста, спочатку виконайте chown для каталогу на хості: контейнер працює від імені користувача node без прав root, uid 1000 у node:22-alpine. Каталог, створений root, недоступний для запису цьому користувачу, тому застосунок не може відкрити базу даних, а контейнер завершує роботу під час запуску. У розділі Bind mounts замість іменованих томів у Compose цей компроміс розглянуто повністю.

Аналітика та банер згоди, який не потрібен

Це функція, яка виправдовує self-hosting сторінки, яку в іншому разі можна було б безкоштовно отримати в іншому місці.

Аналітика працює без cookie. Для відвідувача не встановлюється cookie, і на публічній сторінці не завантажується сторонній скрипт. Відвідувач ідентифікується за SHA-256-хешем IP-адреси, рядка user agent і salt, скороченим до 16 шістнадцяткових символів. Сам salt є хешем поточної дати UTC і вашого SECRET_KEY, тому він змінюється опівночі за UTC, а вчорашні хеші не можна зіставити із сьогоднішніми. Необроблена IP-адреса ніколи не записується до бази даних.

Кліки підраховуються на сервері. Кожне http-посилання на публічній сторінці веде на /go/<id> у вашому домені. Цей endpoint записує клік, а потім відповідає перенаправленням 302 на фактичне призначення. Тому підрахунок працює для читачів із вимкненим JavaScript і у вбудованих браузерах застосунків, які блокують фонові запити. Перегляди сторінок записуються через /api/track.

Варто знати про два винятки. Запит із дійсною admin-сесією пропускається, тому редагування власної сторінки не збільшує показники. Відомі user agent сканерів також пропускаються.

Щодо згоди: на пристрої читача нічого не зберігається, а саме збереження cookie на пристрої читача є причиною, через яку банер cookie запитує дозвіл. Ваші обов’язки все одно залежать від місця проживання читачів, тому перевірте відповідні вимоги. Однак тут немає tracking cookie, про який потрібно повідомляти, і немає третьої сторони, яка отримує ці дані.

Є один нюанс, який дивує багатьох: якщо змінити SECRET_KEY, щоденний salt також зміниться, тому від цього моменту кожен постійний відвідувач рахуватиметься як новий.

Чому стовпець країни в аналітиці порожній?

Тому що у вашому стеку ніщо не встановлює заголовок країни. LinkBreeze визначає країну із заголовків проксі, наприклад cf-ipcountry і x-vercel-ip-country. Якщо VPS працює за вашим Caddy або Nginx, жодного з цих заголовків немає, тому країна записується як null, а розподіл залишається порожнім. Усередині контейнера немає бази даних GeoIP.

Є два способи заповнити це поле. Розмістіть Cloudflare перед доменом. Він додає cf-ipcountry до кожного проксійованого запиту. Або встановіть один із цих заголовків у власному reverse proxy на основі локального пошуку GeoIP.

Пов’язана проблема серйозніша, тому перевірте її. Обробники кліків і переглядів спочатку читають адресу клієнта з X-Forwarded-For, потім із X-Real-IP, а якщо жодного з цих заголовків немає, використовують 0.0.0.0. Якщо опублікувати порт 3000 безпосередньо в інтернет без проксі, усі відвідувачі хешуються в одне й те саме значення. У результаті кількість унікальних відвідувачів назавжди дорівнює 1, а обмеження 60 подій на хвилину для однієї IP-адреси застосовується одночасно до всієї вашої аудиторії. За директивою reverse_proxy, наведеною вище, Caddy встановлює цей заголовок автоматично, і обидві проблеми зникають.

Імпорт із Linktree і дані, які не переносяться

Майстер міграції в панелі керування приймає загальнодоступну URL-адресу профілю або експортований файл. Він розпізнає сторінки linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me і LittleLink, а також загальні експорти HTML та JSON. Для URL-адрес Linktree або Bento він читає JSON __NEXT_DATA__, вбудований у ці сторінки. Для статичної сторінки він читає теги прив’язки.

Переносяться заголовок, URL-адреса, опис та зображення кожного посилання, ознака профілю в соціальній мережі, а також ваше ім’я, біографія й аватар. Перед записом даних у базу даних ви вибираєте, які зі знайдених посилань залишити.

Не переносяться історія аналітики, тема й макет, підписники електронної пошти, заплановані дати публікації та будь-які дані, які стара платформа зберігає за власною формою входу. Плануйте відтворити вигляд вручну. Історія старих переходів залишиться в старому сервісі.

Імпортер отримує URL-адресу із сервера, а не з браузера, тому відхиляє адреси, які не є загальнодоступними. Private/local URLs are not allowed означає, що ви вказали адресу у власній мережі. Відмова є навмисною: без цього будь-хто з доступом до панелі керування міг би використовувати ваш сервер для перевірки машин, доступних лише з вашого сервера. Інші повідомлення, які можуть з’явитися, — Only http and https URLs are allowed, Request timed out і Response too large.

Скрейпінг залежить від HTML-розмітки сторонньої платформи. Якщо майстер не знаходить нічого на сторінці, де явно є посилання, ця платформа змінила HTML після написання парсера. Додайте посилання вручну, а не очікуйте виправлення. Якщо вам насправді потрібні вимірювані короткі посилання, а не сторінка профілю, self-hosted скорочувач URL-адрес, наприклад Shlink виконує це завдання й без проблем працює на тому самому сервері.

Оновлення розгортання із зафіксованим тегом

# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreeze

Міграції схеми запускаються автоматично під час старту контейнера. Документованого способу виконати їх у зворотному напрямку немає, тому спочатку створіть копію тому. Оновлення, яке неможливо скасувати, безпечне лише за наявності можливості відновити попередній стан.

Панель керування показує банер, коли доступний новіший реліз. Вона перевіряє це, завантажуючи невеликий файл із версією з репозиторію проєкту на GitHub раз на 24 години. Жодних даних про ваш інстанс вона не надсилає. Перед зміною тега прочитайте примітки до релізу, оскільки на цьому етапі розвитку проєкту мінорна версія може змінити параметри за замовчуванням, від яких залежить ваша конфігурація.

Типові причини збоїв і повідомлення, які ви побачите

manifest unknown під час отримання образу. Тег записано як :v1.2.7. Теги реєстру не містять v, тому використовуйте :1.2.7.

no matching manifest for linux/arm64/v8 in the manifest list entries. Опублікований образ доступний лише для amd64. Зберіть його на ARM-хості з джерела з указаним тегом.

Контейнер повідомляє unhealthy, хоча сторінка завантажується без проблем. Перевірка стану у вашому compose-файлі викликає curl, якого немає в образі. Видаліть цю перевірку й дозвольте виконуватися власній перевірці стану образу wget.

Caddy повертає помилку сертифіката або взагалі нічого не обслуговує. Перевірте docker compose logs caddy. Найчастіше A-запис ще не вказує на цей VPS або порт 80 закритий у firewall. Через це блокується HTTP-перевірка ACME (середовища автоматичного керування сертифікатами), яку Caddy використовує, щоб підтвердити контроль над доменом.

Кількість унікальних відвідувачів не змінюється й дорівнює 1. Жоден проксі не встановлює X-Forwarded-For, тому хеш для кожного відвідувача однаковий.

Контейнер завершує роботу одразу після запуску, хоча вчора все працювало. Якщо ви замінили іменований том на bind mount хоста, каталог із даними належить root, а застосунок працює з uid 1000 і не може відкрити файл бази даних. sudo chown -R 1000:1000 каталог на хості.

Запити відстеження отримують відповідь HTTP 429. Спрацювало обмеження кількості запитів на IP-адресу для /api/track і /go/<id>. Відвідувачів і далі перенаправляє до місця призначення, але перехід не враховується.

FAQ

Чи готовий LinkBreeze для публічного розміщення в bio?

Це молодий проєкт. Станом на August 2026 репозиторій має 178 зірок, 17 форків і одного мейнтейнера, а перший реліз датовано 1 July 2026. У середньому нові релізи виходять понад двічі на тиждень, тому помилки виправляють швидко, але поведінка також швидко змінюється. Ліцензія MIT і локальний файл SQLite дають змогу зберегти робочу сторінку, навіть якщо розробка припиниться. Проте публічний вебзастосунок без security fixes стає відповідальністю, тому ставтеся до LinkBreeze як до програмного забезпечення, яке потрібно регулярно оновлювати, а не встановити один раз.

Який tag образу LinkBreeze слід використовувати?

Використовуйте tag версії, наприклад ghcr.io/manak-hash/linkbreeze:1.2.7, і змінюйте його свідомо. Workflow релізу публікує лише latest і базовий номер версії, тому :v1.2.7 із v не існує, а Docker повертає manifest unknown. Образ зібрано лише для linux/amd64, тому на VPS з arm64 потрібно клонувати tag і зібрати образ локально.

Чому розподіл за країнами в аналітиці LinkBreeze залишається порожнім?

LinkBreeze отримує країну відвідувача із proxy headers, наприклад cf-ipcountry або x-vercel-ip-country, і не має власної GeoIP database. VPS за вашим Caddy або Nginx не встановлює жодного з цих заголовків, тому країна зберігається як null. Розмістіть Cloudflare перед доменом або налаштуйте reverse proxy на встановлення одного з цих заголовків на основі локального GeoIP lookup.

Що саме потрібно резервувати і як це відновити?

Резервуйте весь volume linkbreeze-data, а не лише файл бази даних. /app/data/linkbreeze.db містить усі посилання, сторінки, налаштування, підписників і рядки аналітики, а /app/data/uploads містить зображення аватарів і мініатюр, на які посилається сторінка. Зупиніть контейнер, виконайте docker compose cp linkbreeze:/app/data ./backup-$(date +%F), а потім запустіть його знову. Для відновлення скопіюйте каталог назад у зупинений контейнер і запустіть його. JSON-експорт із dashboard є snapshot конфігурації профілю, посилань, налаштувань і тем. Він не містить аналітики та зображень.

Чи переносить імпорт із Linktree мою аналітику й тему?

Ні. Майстер міграції зчитує назви посилань, URL, описи та зображення зі старого публічного профілю, а також ваше display name, bio й аватар. Історія аналітики, тема, підписники email-розсилки та заплановані дати публікації не переносяться. Після імпорту відновіть оформлення в редакторі тем і врахуйте, що історія кліків залишиться на старій платформі.

#linkbreeze#linktree-alternative#docker-compose#sqlite#self-hosting#analytics