Свой сокращатель URL на VPS с Shlink и Docker Compose
Настройте Shlink 5.1 на VPS: короткий домен с HTTPS, Postgres, API-ключи, веб-клиент 4.8, QR-коды и статистика переходов через Docker Compose.
Что вы создадите
Собственный сервис сокращения URL — это небольшой сервер, который преобразует длинную ссылку в короткую, принадлежащую вам, и подсчитывает каждый переход по ней. Рекомендуется Shlink: это ПО с открытым исходным кодом, оно выпускается в виде Docker-образа и выполняет всю работу в одном контейнере и базе данных. В этом руководстве сервис размещается на VPS за настоящим коротким доменом, с HTTPS, ключом API, QR-кодами и статистикой переходов.
Два компонента делают сервис похожим на коммерческий сокращатель ссылок. Сервер API обрабатывает перенаправления и хранит данные. Веб-клиент — это отдельное статическое приложение, которое обращается к этому API из браузера. Вы можете запустить оба компонента либо использовать только API и управлять им из командной строки.
Указанные здесь номера версий соответствуют актуальным по состоянию на июль 2026 года: Shlink 5.1 и shlink-web-client 4.8.
Сначала направьте короткий домен на сервер
Домен — это основа сервиса. s.example.com/abc123 — ссылка, которую видят пользователи, поэтому выберите короткое имя до установки каких-либо компонентов. Shlink сохраняет домен в каждом коротком URL. Если позже изменить домен, все уже распространенные ссылки перестанут работать.
Создайте одну DNS-запись A для короткого домена и укажите в ней общедоступный IPv4-адрес VPS. Если сервер поддерживает IPv6, также добавьте запись AAAA. Затем убедитесь, что домен разрешается, прежде чем продолжить.
dig +short s.example.com AВ выводе должен быть адрес вашего сервера. Если вывод пуст, запись еще не распространилась. В таком случае каждый следующий шаг завершится непонятной ошибкой, поскольку сертификат 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, поэтому из интернета к ним нельзя обратиться, пока не будет настроен обратный прокси из следующего раздела. 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 следует настраивать в обратном прокси. Важно передавать исходное имя хоста. Shlink определяет домен короткого кода по заголовку Host. Если прокси изменяет это значение, существующие ссылки возвращают ответ 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:// в возвращаемых им коротких URL. Сам по себе этот параметр TLS не включает. Оставьте его значением false за HTTPS-прокси. Тогда каждая ссылка, которую возвращает API, будет ссылкой http://, которая затем перенаправляет запрос. Это добавляет лишний сетевой обмен и выглядит неправильно в веб-клиенте.
Создайте API-ключ
Без ключа API не принимает запросы. Создайте ключ с помощью CLI внутри контейнера.
sudo docker compose exec shlink shlink api-key:generate --name "web client"Команда выводит ключ только один раз. Скопируйте его сейчас, поскольку ключ хранится в виде хеша и больше не может быть показан. 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-urlsОбъект 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 создает понятную ссылку вместо сгенерированного кода. Slug уникален в пределах домена. Поэтому повторная попытка использовать занятый 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. Поэтому данные не проходят через третьи стороны.
Для QR-кодов настройка не требуется. Добавьте /qr-code к любому короткому URL, и API вернет изображение.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20Параметр size задает ширину в пикселях. Допустимы значения от 50 до 1000. Значение по умолчанию — 300. Параметр format принимает значение png или svg. Параметр margin задает размер свободного пространства вокруг кода в пикселях. Размер готового изображения равен размеру кода плюс удвоенное значение поля. Добавьте errorCorrection=Q, чтобы код можно было распознать даже при печати в небольшом размере или частичном закрытии.
Поддерживайте работу сервиса
Сервис сокращения ссылок может незаметно прекратить работу. Ссылки перестают перенаправлять пользователей, и никто не сообщает об этом, потому что пользователь, открывший ссылку, считает ее нерабочей. Настройте проверку доступности для действующего короткого URL, а не для главной страницы, и отправляйте оповещение при любом ответе, который не является перенаправлением. Самостоятельно размещенный экземпляр Uptime Kuma хорошо подходит для этого и может проверять конкретный код состояния.
Создавайте резервную копию базы данных, а не контейнера. Для этого достаточно одной команды.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzЭтот файл вместе с файлом compose позволяет полностью восстановить сервис на новом сервере. Обновление выполняется с помощью sudo docker compose pull, после чего запускается sudo docker compose up -d, а Shlink при запуске применяет все новые миграции. Создайте дамп до выполнения pull, потому что миграцию нельзя откатить.
FAQ
Почему короткие ссылки возвращают 404 после добавления обратного прокси?
Shlink сопоставляет короткий код с доменом в заголовке Host. Если прокси передаёт собственное имя или внутренний адрес, Shlink ищет этот код в домене, для которого нет ссылок, и возвращает 404. Укажите proxy_set_header Host $host; в блоке location nginx и перезагрузите прокси. Ссылки сразу начнут работать. Перезапуск контейнера не требуется.
Нужен ли Postgres или достаточно SQLite?
SQLite подходит для тестирования Shlink и не требует второго контейнера. Перейдите на Postgres до публикации важных ссылок, поскольку число записей о посещениях увеличивается с каждым переходом, а SQLite последовательно обрабатывает записи. При последующем переходе потребуется экспортировать и повторно импортировать ссылки. Поэтому выбор Postgres на начальном этапе избавит вас от этой миграции.
Можно ли восстановить API-ключ, который я забыл скопировать?
Нет. Shlink хранит хеш ключа, поэтому api-key:list показывает имена и состояние, но не само значение. Создайте замену с помощью shlink api-key:generate, вставьте её в веб-клиент, затем отключите старый ключ с помощью shlink api-key:disable, чтобы он перестал работать.
Почему столбцы стран пусты в статистике посещений?
Для геолокации нужна база данных GeoLite2. Shlink скачивает её только после указания GEOLITE_LICENSE_KEY. Ключ можно бесплатно получить у MaxMind. Добавьте его в раздел переменных окружения и пересоздайте контейнер. После этого новые посещения будут определяться по местоположению. Посещения, записанные ранее, останутся без данных, пока вы не выполните shlink visit:locate.
Как перенести Shlink на другой сервер?
Сохраните домен и перенесите данные. Создайте дамп базы данных с помощью pg_dump, скопируйте дамп и compose-файл на новый сервер, запустите стек, затем восстановите дамп в пустую базу данных до поступления реального трафика. Измените запись DNS в последнюю очередь. Короткие коды и история их посещений сохранятся, поскольку всё хранится в базе данных.