Как развернуть LinkBreeze через Docker Compose
Пошаговое руководство по установке LinkBreeze на VPS с использованием Caddy. Вы узнаете, как настроить SQLite, обеспечить отслеживание кликов и зафиксировать версии образов.
Что такое LinkBreeze
LinkBreeze — это self-hosted альтернатива Linktree. Проект представляет собой один Docker-контейнер, который обслуживает публичную страницу со ссылками и панель администратора, при этом все данные хранятся в одном файле SQLite. Проект распространяется по лицензии MIT, написан на TypeScript с использованием Next.js и публикуется как ghcr.io/manak-hash/linkbreeze. Для запуска вам потребуется VPS, домен с A-записью, указывающей на этот VPS, открытые порты 80 и 443, а также Docker Engine с плагином Compose.
В этом руководстве рассматривается вариант развертывания, поддерживаемый репозиторием: Docker Compose за reverse proxy, который самостоятельно получает сертификаты. Также рассматриваются возможные причины сбоев, так как ссылка в профиле — это публичный URL, по которому переходят пользователи, и его неработоспособность приводит к потере трафика.
Прежде чем приступать, учтите, насколько новым является этот проект.
Достаточно ли LinkBreeze зрел для размещения публичной ссылки на профиль?
По состоянию на август 2026 года репозиторий имеет 178 звезд, 17 форков и одного мейнтейнера. Первый релиз с тегом, v1.0.0, датирован 1 июля 2026 года. Этому проекту всего несколько недель, а не лет.
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 .envSECRET_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 caddydocker compose ps должен показать, что контейнер LinkBreeze находится в состоянии healthy. Образ содержит собственную проверку работоспособности (healthcheck) wget --spider -q http://127.0.0.1:3000/api/health, поэтому добавлять ее вручную не нужно. Не копируйте healthcheck из примера Caddy в репозитории: он вызывает curl, а образ собран на базе node:22-alpine, в котором есть только busybox wget и отсутствует curl. Такой контейнер будет сообщать о статусе unhealthy, даже если страницы обслуживаются корректно.
Откройте https://links.example.com в браузере. При первом посещении откроется мастер настройки по адресу /setup, который создаст единственную учетную запись администратора. После этого панель управления будет доступна по адресу /dashboard, а форма входа — по адресу /login. Эта учетная запись является локальной для данного экземпляра, и в приложении нет встроенной поддержки единого входа (SSO). Если вы хотите, чтобы панель управления использовала те же учетные данные, что и другие ваши сервисы, необходимо настроить перед ней прокси с поддержкой forward auth, например самостоятельно развернутый Authentik.
Обратите внимание на то, чего не делает файл compose: он не публикует порт 3000. Только Caddy прослушивает публичный интерфейс. Если синтаксис Compose для вас в новинку, основы Docker Compose для VPS описывают аспекты, на которых базируется этот файл. Если вы уже используете другое решение в качестве фронтенда, сравнение Nginx, Caddy и Traefik объяснит необходимые изменения. В репозитории представлены рабочие примеры для Nginx с Certbot, Traefik и туннелем Cloudflare.
Где хранятся ваши данные и что должен содержать бэкап
DATABASE_PATH указывает на /app/data/linkbreeze.db. Загруженные аватары и миниатюры ссылок записываются рядом с ним в /app/data/uploads. Оба объекта находятся в именованном томе linkbreeze-data, поэтому единицей бэкапа является том, а не сам файл базы данных. Если восстановить файл без директории с загрузками, каждое изображение на странице будет выдавать ошибку 404.
Всё остальное действительно находится в этой единственной базе данных: страницы, ссылки, настройки, темы оформления, список email-подписчиков и строки аналитики.
Выполняйте копирование при остановленном контейнере:
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. Он содержит профиль, ссылки, настройки и сохраненные темы. Он не содержит историю аналитики, email-подписчиков или загруженные изображения, а восстановление из него удаляет текущие строки в этих четырех таблицах перед вставкой данных из файла. Рассматривайте это как снимок конфигурации для переноса на другой хост или отмены ошибки редактирования. Копия тома — это и есть бэкап.
Здесь действуют те же два правила хранения, что и везде, где вы используете SQLite в продакшене на VPS. Храните базу данных на локальном диске, так как блокировки SQLite ненадежны в сетевых файловых системах, и о проблеме вы узнаете только после повреждения страницы. Если вы заменяете именованный том на bind mount хоста, сначала выполните chown для директории на хосте: контейнер работает от имени пользователя node, не имеющего прав root, с uid 1000 в node:22-alpine. Директория, созданная пользователем root, будет недоступна для записи, поэтому приложение не сможет открыть базу данных, и контейнер завершит работу при запуске. В статье Bind mounts против именованных томов в Compose этот вопрос разобран полностью.
Аналитика и баннер согласия, которые вам не нужны
Это функция, оправдывающая самостоятельный хостинг страницы, которую можно бесплатно разместить в другом месте.
Аналитика работает без использования cookies. Для посетителя не устанавливается cookie, а на публичной странице не загружаются сторонние скрипты. Посетитель идентифицируется с помощью SHA-256 хеша IP-адреса, строки user agent и соли, обрезанного до 16 шестнадцатеричных символов. Соль сама по себе является хешем текущей даты UTC и вашего SECRET_KEY, поэтому она меняется в полночь по UTC, и хеши за вчерашний день невозможно сопоставить с сегодняшними. Исходный IP-адрес никогда не записывается в базу данных.
Клики подсчитываются на сервере. Каждая http-ссылка на публичной странице указывает на /go/<id> в вашем собственном домене, который фиксирует клик, а затем отвечает редиректом 302 на реальный адрес назначения. Таким образом, подсчет работает для читателей с отключенным JavaScript, а также внутри встроенных браузеров приложений, которые блокируют фоновые запросы. Просмотры страниц записываются через /api/track.
Следует знать о двух исключениях. Запрос, содержащий действительную сессию администратора, пропускается, поэтому редактирование собственной страницы не завышает показатели. Известные user agents поисковых роботов также пропускаются.
О согласии: на устройстве читателя ничего не сохраняется, а cookie, хранящийся на устройстве читателя, — это именно то, на что баннер cookie запрашивает разрешение. Ваши обязательства по-прежнему зависят от того, где живут ваши читатели, поэтому уточните их, но здесь нет отслеживающего cookie, о котором нужно уведомлять, и нет третьей стороны, получающей данные.
Один нюанс, который удивляет пользователей: при смене SECRET_KEY меняется и ежедневная соль, поэтому каждый вернувшийся посетитель с этого момента считается новым.
Почему столбец страны в аналитике пуст?
Это происходит потому, что в вашем стеке ни один компонент не устанавливает заголовок страны. 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, а ограничение частоты запросов (rate limit) в 60 событий в минуту будет применяться ко всей аудитории сразу. При использовании директивы 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 он считывает __NEXT_DATA__ JSON, который эти страницы содержат. Для статических страниц он считывает теги anchor.
Переносятся заголовок, URL, описание и изображение каждой ссылки, информация о том, является ли ссылка профилем в социальной сети, а также ваше отображаемое имя, биография и аватар. Вы выбираете, какие из найденных ссылок сохранить, прежде чем данные будут записаны в базу данных.
Не переносятся история аналитики, тема оформления и макет, список подписчиков электронной почты, запланированные даты публикации и всё то, что старая платформа хранит за собственным логином. Планируйте воссоздание внешнего вида вручную и примите тот факт, что старая история кликов останется на прежнем сервисе.
Импортер запрашивает URL с вашего сервера, а не из вашего браузера, поэтому он отклоняет адреса, которые не являются публичными. Private/local URLs are not allowed означает, что вы указали адрес внутри вашей собственной сети, и этот отказ является намеренным: без него любой пользователь с доступом к панели управления мог бы использовать ваш сервер для сканирования машин, доступных только вашему серверу. Другие сообщения, которые вы можете увидеть: Only http and https URLs are allowed, Request timed out и Response too large.
Парсинг зависит от разметки стороннего ресурса. Если мастер ничего не находит на странице, где явно есть ссылки, значит, эта платформа изменила свой HTML с момента написания парсера. Добавьте ссылки вручную, не дожидаясь исправления. Если вам нужны измеримые короткие ссылки, а не страница профиля, самохостируемый сокращатель ссылок, такой как 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 при выполнении pull. Тег был указан как :v1.2.7. Теги в реестре не имеют v, поэтому используйте :1.2.7.
no matching manifest for linux/arm64/v8 in the manifest list entries. Опубликованный образ доступен только для архитектуры amd64. Соберите его на ARM-хосте из исходного кода с соответствующим тегом.
Контейнер сообщает unhealthy, хотя страница загружается корректно. В вашем файле compose настроена проверка работоспособности (healthcheck), вызывающая curl, которого нет в образе. Удалите эту настройку и позвольте выполняться встроенной проверке wget самого образа.
Caddy выдает ошибку сертификата или не отвечает. Проверьте docker compose logs caddy. Обычно это происходит, если A-запись еще не указывает на этот VPS или порт 80 закрыт на межсетевом экране. Это блокирует 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 для публичного использования в качестве ссылки в профиле?
Это молодой проект. По состоянию на август 2026 года репозиторий имеет 178 звезд, 17 форков и одного мейнтейнера, а первый релиз датирован 1 июля 2026 года. Релизы выходят в среднем чаще двух раз в неделю, поэтому ошибки исправляются быстро, но и поведение системы меняется часто. Лицензия MIT и локальный файл SQLite означают, что страница продолжит работать, даже если разработка прекратится. Однако публичное веб-приложение без обновлений безопасности становится обузой, поэтому рассматривайте его как ПО, которое требует регулярного обновления, а не как решение «установил и забыл».
Какой тег образа LinkBreeze следует использовать?
Используйте тег версии, например ghcr.io/manak-hash/linkbreeze:1.2.7, и меняйте его осознанно. Процесс сборки релизов отправляет в реестр только latest и номер версии, поэтому тег :v1.2.7 с v не существует, и Docker вернет ошибку manifest unknown. Образ собран только для архитектуры linux/amd64, поэтому на VPS с arm64 вам придется клонировать репозиторий и выполнить сборку локально.
Почему статистика по странам в LinkBreeze остается пустой?
LinkBreeze считывает страну посетителя из заголовков прокси, таких как cf-ipcountry или x-vercel-ip-country, и не содержит собственной базы данных GeoIP. VPS, работающий за вашим собственным Caddy или Nginx, не передает эти заголовки, поэтому страна сохраняется как null. Подключите Cloudflare перед доменом или настройте ваш reverse proxy так, чтобы он добавлял один из этих заголовков на основе локального GeoIP-запроса.
Что именно нужно резервировать и как выполнять восстановление?
Создавайте резервную копию всего тома linkbreeze-data, а не только файла базы данных. В /app/data/linkbreeze.db хранятся все ссылки, страницы, настройки, данные подписчиков и записи аналитики, а в /app/data/uploads — аватары и миниатюры изображений, на которые ссылается страница. Остановите контейнер, выполните docker compose cp linkbreeze:/app/data ./backup-$(date +%F), затем запустите его снова. Восстановление выполняется путем копирования директории обратно в остановленный контейнер и его последующего запуска. Экспорт в формате JSON из панели управления — это лишь снимок конфигурации профиля, ссылок, настроек и тем; он не содержит аналитики и изображений.
Переносятся ли аналитика и тема при импорте из Linktree?
Нет. Мастер миграции считывает заголовки ссылок, URL-адреса, описания и изображения из вашего старого публичного профиля, а также отображаемое имя, биографию и аватар. История аналитики, тема оформления, список подписчиков и запланированные даты публикации не переносятся. После импорта настройте внешний вид в редакторе тем; история кликов останется на старой платформе.