Как развернуть свой сервис сокращения ссылок Shlink
Установите Shlink на VPS через Docker Compose для управления короткими ссылками. В руководстве описана настройка DNS, базы данных Postgres, API ключей и веб-интерфейса.
Что вы создаете
Self-hosted сервис для сокращения ссылок — это небольшой сервер, который превращает длинную ссылку в короткую, принадлежащую вам, и подсчитывает количество переходов по ней. Shlink — оптимальный выбор: это open source решение, которое поставляется в виде Docker образа и выполняет всю работу в одном контейнере вместе с базой данных. В этом руководстве мы развернем его на VPS, привяжем к собственному короткому домену, настроим HTTPS, API key, QR-коды и статистику переходов.
Система состоит из двух компонентов, что делает её похожей на коммерческие сервисы сокращения ссылок. API server отвечает за редиректы и хранение данных. Web client — это отдельное статическое приложение, которое взаимодействует с API через ваш браузер. Вы можете запустить оба компонента или использовать только API, управляя им через командную строку.
Указанные здесь версии актуальны на июль 2026 года: Shlink 5.1 и shlink-web-client 4.8.
Сначала укажите короткий домен для сервера
Домен — это и есть продукт. s.example.com/abc123 — это ссылка, которую видят пользователи, поэтому выберите короткое имя до начала установки. Shlink сохраняет домен вместе с каждой короткой ссылкой, и изменение домена в будущем приведет к тому, что все ранее распространенные ссылки перестанут работать.
Создайте одну DNS-запись типа A для короткого домена, указав публичный IPv4-адрес вашего VPS. Добавьте также запись AAAA, если у сервера есть IPv6. Затем убедитесь, что домен разрешается, прежде чем продолжать.
dig +short s.example.com AВыводом должен быть IP-адрес вашего сервера. Если вывод пуст, значит, запись еще не распространилась, и каждый последующий шаг завершится ошибкой, которую будет сложно диагностировать, так как TLS-сертификат (transport layer security) невозможно выпустить для имени, которое не разрешается.
Файл compose
Для работы Shlink требуется база данных. SQLite подходит для тестирования, но для любых долгосрочных задач лучше выбрать Postgres: записи о посещениях быстро накапливаются, а Postgres эффективнее работает с индексами и параллельной записью. Разместите этот код в /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:Оба опубликованных порта привязаны к 127.0.0.1, поэтому сервисы недоступны из интернета, пока не настроен reverse proxy из следующего раздела. Docker создает собственные правила пересылки в обход межсетевого экрана хоста, поэтому обычная строка 8080:8080 открыла бы доступ к приложению даже на сервере с закрытым файрволом. Привязка к адресу loopback предотвращает это. Тот же подход применим к любому приложению, запускаемому таким образом; подробнее об этом рассказано в руководстве по Docker Compose на VPS.
Пароль от базы данных берется из файла .env, расположенного рядом с файлом compose, поэтому он никогда не попадает в 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Запустите контейнеры и дождитесь готовности API.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkПервый запуск включает миграции базы данных, поэтому он занимает больше времени, чем последующие. Когда процесс завершится, проверьте, отвечает ли сервис локально.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthКод 200 означает, что API работает, а соединение с базой данных установлено. Ошибка 500 в данном случае почти всегда указывает на проблемы с базой данных: значение DB_PASSWORD в .env не совпадает с тем, с которым был инициализирован Postgres. Образ Postgres считывает POSTGRES_PASSWORD только при создании пустой директории данных. Изменение пароля позже не даст эффекта, пока вы не удалите том и не запустите процесс заново.
Терминация HTTPS перед приложением
Shlink работает по протоколу HTTP на порту 8080. TLS должен быть настроен на стороне reverse proxy, при этом критически важно передавать исходное имя хоста. Shlink определяет домен для короткой ссылки, считывая заголовок Host. Если прокси перезаписывает этот заголовок, Shlink возвращает ошибку 404 для существующих ссылок, а статистика посещений привязывается к неверному домену.
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;
}
}Затем выпустите сертификат. Полное руководство, включая настройку таймера автоматического обновления, приведено в руководстве по Certbot для nginx в Ubuntu 24.04.
sudo certbot --nginx -d s.example.comПараметр IS_HTTPS_ENABLED: "true" в файле compose определяет, будет ли Shlink использовать https:// в генерируемых коротких ссылках. Сам по себе этот параметр не включает TLS. Если оставить его false при работе за HTTPS-прокси, каждая ссылка, возвращаемая API, будет иметь префикс http://. Это приведет к лишнему перенаправлению, что увеличивает время отклика и некорректно отображается в веб-клиенте.
Создание API-ключа
Ни один сервис не сможет взаимодействовать с API без ключа. Сгенерируйте его через CLI внутри контейнера.
sudo docker compose exec shlink shlink api-key:generate --name "web client"docker exec -it <container_name> ./bin/generate-key --name "my-key"
Команда выводит ключ только один раз. Скопируйте его сейчас, так как в системе он хранится в хешированном виде и повторный просмотр невозможен. shlink api-key:list отображает имена ключей и их статус (включен или выключен), но не сами ключи. Для отзыва ключа используйте shlink api-key:disable с указанием имени.
Каждый REST-запрос должен содержать ключ в заголовке X-Api-Key.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlscurl -H "X-API-Key: <your_key>" https://api.example.com/status
JSON-объект с ключом shortUrls означает, что ключ работает корректно. Код 401 с сообщением INVALID_API_KEY означает, что ключ неверный, отключен или срок его действия истек.
Создание коротких ссылок из командной строки
Интерфейс командной строки (CLI) — это самый быстрый способ создания ссылок, который лучше всего подходит для автоматизации с помощью скриптов.
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 позволяет получить читаемую ссылку вместо сгенерированного кода. Слаги уникальны для каждого домена, поэтому повторная попытка создать ссылку с уже занятым слагом завершится ошибкой, а не перезапишет существующую ссылку. Команду --tag можно использовать повторно, а теги позволяют группировать ссылки, для которых вы захотите получить общую статистику в будущем.
Выведите список существующих ссылок, а затем просмотрите статистику трафика для одной из них.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits выводит по одной строке на каждый клик с указанием даты, реферера и user agent. Столбцы с данными о стране и городе останутся пустыми, если вы не зададите переменную окружения GEOLITE_LICENSE_KEY. Это бесплатный ключ MaxMind, который Shlink использует для загрузки базы данных GeoLite2. Без него посещения всё равно будут фиксироваться, но без определения географического положения.
Веб-клиент и QR-коды
Веб-клиент теперь находится по адресу 127.0.0.1:8081 и требует отдельной записи в прокси-сервере или использования SSH-туннеля, если вы не хотите открывать его для публичного доступа. При первой загрузке он запрашивает URL сервера и API-ключ. Введите https://s.example.com и сгенерированный вами ключ. Клиент сохраняет оба значения в хранилище браузера и обращается к вашему API напрямую, поэтому данные не проходят через сторонние сервисы. Разделение интерфейса и API — это важный архитектурный шаблон; именно он позволяет Halcyon стилизовать библиотеку Jellyfin под видеопрокат 1990-х годов, не внося изменений в сам медиасервер.
QR-коды не требуют никакой настройки. Добавьте /qr-code к любому короткому URL, и API вернет изображение.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size — это ширина в пикселях, принимающая значения от 50 до 1000 (по умолчанию 300). format может принимать значения png или svg. margin — это размер свободного пространства вокруг кода в пикселях; итоговое изображение будет иметь размер кода плюс двойной отступ. Добавьте errorCorrection=Q для кода, который сохраняет читаемость при печати в малом размере или при частичном перекрытии.
Поддержание работоспособности
Сервис сокращения ссылок может завершить работу без вывода ошибок. Ссылки перестают перенаправлять пользователей, и вы не узнаете об этом, так как пользователь решит, что ссылка просто нерабочая. Настройте проверку доступности (uptime check) для реальной сокращенной ссылки, а не для главной страницы, и настройте оповещения на любой ответ, отличный от перенаправления. Экземпляр Uptime Kuma, развернутый на собственном сервере отлично справляется с этой задачей и позволяет отслеживать конкретный код состояния.
Выполняйте резервное копирование базы данных, а не контейнера. Один дамп создается одной командой.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzЭтот файл вместе с вашим compose-файлом позволяет полностью восстановить сервис на новом сервере. Каждому приложению на сервере требуется своя пара из этих файлов. Фотоархивы — особый случай, так как PhotoPrism и Immich хранят оригиналы файлов на диске, а не только записи в базе данных, поэтому одного дампа для восстановления недостаточно. Обновления выполняются через sudo docker compose pull с последующим sudo docker compose up -d, а Shlink автоматически применяет все новые миграции при запуске. Создавайте дамп перед обновлением образа, так как миграции невозможно откатить назад.
FAQ
Почему мои короткие ссылки возвращают 404 после добавления reverse proxy?
Shlink сопоставляет короткий код с доменом в заголовке Host. Если прокси передает свое имя или внутренний адрес, Shlink ищет код в домене, где нет ссылок, и возвращает 404. Установите proxy_set_header Host $host; в блоке location конфигурации nginx и перезагрузите прокси. Ссылки заработают сразу, перезапуск контейнера не требуется.
Нужен ли мне Postgres или достаточно SQLite?
SQLite подходит для тестирования Shlink и не требует отдельного контейнера. Переходите на Postgres до публикации важных ссылок, так как записи о посещениях растут с каждым кликом, а SQLite сериализует операции записи. Переход на другую БД позже потребует экспорта и импорта ссылок, поэтому выбор Postgres на старте избавит вас от этой миграции.
Можно ли восстановить забытый API key?
Нет. Shlink хранит только хеш ключа, поэтому api-key:list показывает имена и статусы, но не само значение. Создайте новый ключ с помощью shlink api-key:generate, вставьте его в веб-клиент, а затем отключите старый через shlink api-key:disable, чтобы он перестал работать.
Почему столбцы со странами пусты в статистике посещений?
Для геолокации требуется база данных GeoLite2, которую Shlink загружает только при наличии GEOLITE_LICENSE_KEY. Ключ можно бесплатно получить на сайте MaxMind. Добавьте его в секцию environment, пересоздайте контейнер, и новые посещения будут определяться. Посещения, записанные ранее, останутся без данных, пока вы не запустите shlink visit:locate.
Как перенести Shlink на другой сервер?
Сохраните домен и перенесите данные. Сделайте дамп базы данных с помощью pg_dump, скопируйте дамп и файл compose на новый сервер, запустите стек, а затем восстановите дамп в пустую базу данных до того, как пойдет реальный трафик. Запись DNS меняйте в последнюю очередь. Короткие коды и история посещений сохранятся, так как все данные находятся в базе.