MAATRIX / Блог / Shlink в Docker Compose: готовый файл

Shlink в Docker Compose: готовый файл

MAATRIX

Сократить ссылку через bit.ly или clck.ru просто, пока не встаёт вопрос — а что если сервис завтра решит ограничить бесплатный тариф, удалить старые ссылки или просто закроется. Для рассылок, партнёрских ссылок и трекинга кампаний собственный shortener снимает эту зависимость: домен ваш, статистика ваша, ссылки не исчезнут по чужому решению. Shlink — один из немногих self-hosted shortener'ов, где из коробки есть REST API, полноценная аналитика по каждому переходу и готовый веб-интерфейс, а не только консоль. Ниже — рабочий docker-compose.yml, который поднимает сервис вместе с базой и панелью управления за один прогон.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое Shlink и чем он отличается от YOURLS

Shlink написан на PHP (Symfony) и изначально спроектирован как API-first сервис: короткая ссылка создаётся HTTP-запросом к REST API, а веб-панель (Shlink Web Client) — это отдельное SPA-приложение, которое просто дёргает тот же API. Это удобно, если сокращение ссылок нужно встроить в свой продукт или скрипт рассылки — не приходится парсить HTML формы, как у некоторых альтернатив.

Из коробки Shlink даёт:

  • REST API с авторизацией по API-ключу;
  • произвольные (custom) короткие коды вместо случайных;
  • теги для ссылок и фильтрацию по ним;
  • статистику по каждому переходу: referrer, User-Agent, страна и город (через базу GeoLite2);
  • генерацию QR-кодов для любой короткой ссылки без сторонних сервисов;
  • поддержку нескольких доменов на одном инстансе.

Если сравнивать с YOURLS — более старым и лёгким PHP-shortener'ом с плагинной архитектурой — разница именно в этом: YOURLS изначально задуман как веб-приложение с формой, API там появился позже и менее последователен, зато сам YOURLS легче по ресурсам и проще для однофайловой установки без Docker. Если вам нужен именно тонкий скрипт для личного использования — установка YOURLS на VPS может оказаться быстрее. Если же ссылки создаются программно (рассылки, интеграции, десятки ссылок в день) и важна честная аналитика переходов — Shlink забирает эту нишу увереннее.

Готовый docker-compose.yml

Официальный образ shlinkio/shlink поддерживает несколько СУБД (MySQL/MariaDB, PostgreSQL, SQLite, MS SQL). Ниже — конфигурация с PostgreSQL как более предсказуемым вариантом под нагрузку, плюс отдельный контейнер веб-панели.

Структура каталогов:

/opt/shlink/
├── docker-compose.yml
├── .env
├── db-data/        # том PostgreSQL
└── shlink-data/     # том с GeoLite2 базой и служебными файлами Shlink

Создаём каталог и файлы:

mkdir -p /opt/shlink/{db-data,shlink-data}
cd /opt/shlink
nano .env

Файл .env — все чувствительные значения вынесены сюда, чтобы не хранить пароли прямо в compose-файле:

POSTGRES_DB=shlink
POSTGRES_USER=shlink
POSTGRES_PASSWORD=замените-на-длинный-случайный-пароль

DEFAULT_DOMAIN=short.example.com
IS_HTTPS_ENABLED=true

INITIAL_API_KEY=замените-на-собственный-api-ключ

INITIAL_API_KEY — не обязательная переменная, но удобная: Shlink создаст этот ключ при первом запуске, и не придётся выуживать сгенерированный автоматически ключ из логов контейнера.

Сам docker-compose.yml:

services:
  shlink_db:
    image: postgres:16-alpine
    container_name: shlink_db
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  shlink:
    image: shlinkio/shlink:stable
    container_name: shlink
    restart: unless-stopped
    depends_on:
      shlink_db:
        condition: service_healthy
    env_file: .env
    environment:
      DB_DRIVER: postgres
      DB_NAME: ${POSTGRES_DB}
      DB_USER: ${POSTGRES_USER}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_HOST: shlink_db
      DB_PORT: 5432
      TIMEZONE: Europe/Moscow
    ports:
      - "8080:8080"
    volumes:
      - ./shlink-data:/etc/shlink/data

  shlink_web_client:
    image: shlinkio/shlink-web-client:stable
    container_name: shlink_web_client
    restart: unless-stopped
    depends_on:
      - shlink
    environment:
      SHLINK_SERVER_URL: https://short.example.com
      SHLINK_SERVER_API_KEY: ${INITIAL_API_KEY}
      SHLINK_SERVER_NAME: "Мой Shlink"
    ports:
      - "8081:80"

Запуск:

docker compose up -d

При первом старте контейнер shlink сам прогонит миграции базы — это может занять несколько секунд, следить за процессом удобно через docker compose logs -f shlink. После этого API доступен на порту 8080, веб-панель — на 8081; порт публиковать наружу без реверс-прокси не стоит, об этом — ниже.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Переменные окружения: что обязательно, а что можно донастроить позже

Минимально рабочий набор — это подключение к БД (DB_DRIVER, DB_HOST, DB_NAME, DB_USER, DB_PASSWORD) и DEFAULT_DOMAIN. Остальное можно донастроить в любой момент через переменные окружения контейнера shlink (после изменения нужен docker compose up -d для пересоздания):

ПеременнаяНазначение
DEFAULT_DOMAINдомен, который подставляется в короткие ссылки по умолчанию
IS_HTTPS_ENABLEDtrue, если сервис доступен по HTTPS (влияет на формируемые ссылки)
DEFAULT_SHORT_CODES_LENGTHдлина случайного короткого кода, по умолчанию 5 символов
VALIDATE_URLSпроверять ли доступность целевого URL при создании ссылки (true/false)
GEOLITE_LICENSE_KEYключ MaxMind для скачивания базы GeoLite2 (нужна для гео-статистики по визитам)
REDIS_SERVERSадреса Redis, если поднимаете несколько инстансов Shlink за балансировщиком

По GEOLITE_LICENSE_KEY есть нюанс: MaxMind с 2020 года требует бесплатную регистрацию на сайте, чтобы получить ключ для скачивания базы GeoLite2 — без ключа Shlink продолжит работать, но гео-данные (страна/город посетителя) в статистике будут пустыми. Актуальный порядок регистрации и точный формат переменной стоит свериться в официальной документации проекта на момент установки — эти детали у MaxMind менялись не раз.

Полный список переменных можно посмотреть прямо в контейнере, без похода на GitHub:

docker exec shlink shlink --help

Домен и HTTPS через реверс-прокси

Открывать порты 8080/8081 наружу напрямую — плохая идея: ни Shlink, ни shlink-web-client не занимаются TLS-терминацией сами, это осознанно оставлено реверс-прокси. Если на сервере ещё нет прокси с автоматическим SSL, самый быстрый путь — Caddy с автовыпуском сертификатов. Конфиг для связки Shlink + панель:

short.example.com {
    reverse_proxy localhost:8080
}

panel.example.com {
    reverse_proxy localhost:8081
}

Обратите внимание: короткие ссылки и панель управления лучше развести на разные поддомены (или домен + путь через прокси) — short.example.com должен быть чистым и коротким, это то, что вы будете рассылать, а панель с логином удобнее держать на отдельном имени, которое реже светится публично.

Если в инфраструктуре уже используется Traefik с автообнаружением контейнеров по label'ам — сравнение подходов и когда что выбирать разобрано в статье Traefik или Nginx Proxy Manager; для одного-двух сервисов на сервере Caddy обычно быстрее по времени настройки.

Работа с API: короткие ссылки, custom slug, теги

Все действия в Shlink идут через REST API — веб-панель лишь вызывает те же эндпоинты. Создание короткой ссылки:

curl -X POST https://short.example.com/rest/v3/short-urls \
  -H "X-Api-Key: ваш-api-ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "longUrl": "https://example.com/very/long/campaign/url?utm_source=telegram",
    "tags": ["telegram", "campaign-sept"]
  }'

Если нужен предсказуемый, а не случайный код — например, short.example.com/promo вместо short.example.com/aB3xZ:

curl -X POST https://short.example.com/rest/v3/short-urls \
  -H "X-Api-Key: ваш-api-ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "longUrl": "https://example.com/promo-page",
    "customSlug": "promo"
  }'

Запрос вернёт JSON с полем shortUrl — готовой ссылкой — и shortCode. Список всех ссылок с фильтром по тегу:

curl "https://short.example.com/rest/v3/short-urls?tags=telegram" \
  -H "X-Api-Key: ваш-api-ключ"

QR-код для существующей короткой ссылки отдаётся отдельным эндпоинтом без авторизации — им можно пользоваться прямо в шаблоне письма или на печатных материалах:

https://short.example.com/{shortCode}/qr-code

Для повседневной работы без ручных curl-запросов проще открыть веб-панель на panel.example.com, указать URL API-сервера и API-ключ при первом входе (или они уже подставлены переменными SHLINK_SERVER_URL/SHLINK_SERVER_API_KEY из compose-файла выше) — дальше создание ссылок, просмотр статистики и управление тегами доступны в интерфейсе.

Аналитика посещений и бэкап

Статистика переходов доступна по каждой ссылке отдельно — количество визитов, referrer, страна (если настроен GeoLite2), операционная система и браузер посетителя по User-Agent:

curl "https://short.example.com/rest/v3/short-urls/{shortCode}/visits" \
  -H "X-Api-Key: ваш-api-ключ"

Вся эта статистика и сами ссылки хранятся в PostgreSQL — том ./db-data из примера выше. Резервное копирование сводится к дампу базы:

docker exec shlink_db pg_dump -U shlink shlink > shlink-backup-$(date +%F).sql

Восстановление — обратная операция через psql, но перед этим убедитесь, что контейнер shlink временно остановлен, чтобы миграции не конфликтовали с восстанавливаемыми данными:

docker compose stop shlink
cat shlink-backup-2026-08-20.sql | docker exec -i shlink_db psql -U shlink shlink
docker compose start shlink

Если на сервере уже настроен общий пайплайн бэкапов, дамп PostgreSQL логично включить в него — например, через restic или borgbackup в Docker Compose, направив задание на каталог /opt/shlink/db-data или на регулярный pg_dump по cron. Отдельно от базы бэкапить нечего — shlink-data содержит только скачанную GeoLite2-базу, которую при необходимости можно перекачать заново.

Общие подходы к тому, как организовать production-стек в Docker Compose — сети, healthcheck'и, порядок запуска сервисов — подробнее разобраны в статье про Docker Compose для продакшена, если Shlink — не единственный сервис на сервере.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли использовать SQLite вместо PostgreSQL, чтобы не поднимать отдельный контейнер БД?

Да, для DB_DRIVER есть значение sqlite, и Shlink сам создаст файл базы в примонтированном томе ./shlink-data. Для личного использования и нескольких десятков ссылок в день этого достаточно; при заметном потоке переходов и параллельных запросах к API PostgreSQL или MySQL ведут себя предсказуемее.

Как перенести Shlink на другой сервер?

Переносится дамп базы (pg_dump/mysqldump или сам файл SQLite) и содержимое shlink-data. Домен в DEFAULT_DOMAIN и записи DNS нужно поменять отдельно — сами данные ссылок от домена не зависят.

Что делать, если после docker compose up -d контейнер shlink падает с ошибкой подключения к базе?

Чаще всего причина — shlink стартует раньше, чем PostgreSQL успевает подняться. В примере выше это решено через depends_on с condition: service_healthy; если healthcheck убрали, добавьте небольшую задержку рестарта — restart: unless-stopped в итоге сам поднимет контейнер после того, как база станет доступна.

Нужен ли GeoLite2-ключ, если гео-статистика не важна?

Нет, без GEOLITE_LICENSE_KEY Shlink работает полностью штатно, просто в статистике визитов не будет страны и города — остальные поля (referrer, browser, OS, дата) заполняются всегда.

Можно ли обслуживать несколько доменов одним инстансом Shlink?

Да, через API это настраивается при создании ссылки полем domain — база одна, но короткие ссылки могут выпускаться на разных доменах, если они все указывают на этот сервер.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →