Shortli — самостоятельно размещаемый сервис сокращения URL-адресов. Он поддерживает пользовательские псевдонимы, ограничение срока действия ссылок, QR-коды, аналитику переходов, управление учётными записями, инструменты модерации и публичный API.
Порты среды разработки намеренно не используют распространённые диапазоны
5173, 3000 и 8080:
- frontend:
http://127.0.0.1:43177; - backend:
http://127.0.0.1:43178; - порт PostgreSQL на хосте:
5001.
Сначала запустите PostgreSQL, а затем приложения в отдельных терминалах:
docker-compose up -d postgres
cd backend
go run ./cmd/shortliServicecd frontend
npm ci
npm run devВ режиме разработки backend читает backend/.env. Если этого файла нет,
создайте его на основе backend/.env.example.
Полный стек состоит из PostgreSQL, API на Go и веб-шлюза Nginx. Шлюз раздаёт
SPA, проксирует запросы /api/*, а остальные одноуровневые пути передаёт
обработчику перенаправлений.
- Скопируйте
.env.exampleв.env. - Замените все тестовые секреты независимыми случайными значениями.
POSTGRES_USERиспользуется только для инициализации и миграций, аDATABASE_USER— постоянным процессом API; эти имена должны различаться. ЕслиDATABASE_PASSWORDоставить пустым, Compose создаст отдельный случайный пароль приложения в закрытом Docker volume. - Укажите итоговый публичный адрес в
PUBLIC_BASE_URLиFRONTEND_ORIGIN. - Для публичного домена используйте только HTTPS и задайте
COOKIE_SECURE=true. Backend отклонит небезопасную production-конфигурацию. - Соберите и запустите стек:
docker-compose up -d --build
docker-compose psПо умолчанию шлюз привязан к 127.0.0.1:43176. Благодаря этому Shortli может
работать рядом с другими проектами и находиться за Caddy, Nginx, Traefik или
управляемым туннелем. Внешний прокси должен завершать TLS-соединения и
перенаправлять трафик на этот адрес. Не открывайте PostgreSQL в публичную сеть.
Перед развёртыванием обновления создайте резервную копию:
.\ops\backup.ps1
docker-compose up -d --build
docker-compose psЛоги контейнеров ротируются: для каждого сервиса хранится до пяти файлов
размером не более 10 МБ. Исходные IP-адреса в журнал запросов не попадают:
вместо них записывается стабильный псевдоним client_id, вычисленный с помощью
ANALYTICS_SALT.
SQL-миграции находятся в backend/internal/database/migrations и встроены в
исполняемый файл backend. Compose сначала идемпотентно создаёт ограниченную роль
приложения, затем запускает одноразовый контейнер migrate с ролью владельца
базы и только после его успешного завершения запускает API. Постоянный backend
не получает права суперпользователя, создания ролей, баз данных или схемы.
Применённые версии записываются в таблицу schema_migrations. Консультативная
блокировка PostgreSQL не позволяет нескольким экземплярам одновременно
выполнять миграции, поэтому параллельный запуск контейнеров безопасен. Миграции
работают только вперёд: перед установкой новой версии создайте резервную копию,
а для отката схемы или данных восстановите эту копию.
Никогда не изменяйте уже применённую миграцию. Создавайте новый .sql-файл с
лексикографически более поздним именем.
Создание сжатой резервной копии PostgreSQL:
.\ops\backup.ps1Копии записываются в backups/ и исключены из Git. Переносите их на другой
компьютер или в объектное хранилище: копия на том же диске не защищает от
аварии сервера.
Для восстановления необходимо явно подтвердить разрушительную операцию:
.\ops\restore.ps1 -Backup .\backups\shortli-YYYYMMDD-HHMMSS.dump -ConfirmDatabaseResetНа Linux используйте ops/backup.sh и следующую команду восстановления:
CONFIRM_DATABASE_RESET=yes ./ops/restore.sh backups/shortli-file.dumpРекомендуемое расписание:
- создавать копию базы данных каждые 24 часа;
- сразу переносить новую копию за пределы сервера;
- хранить 7 ежедневных, 4 еженедельные и 6 ежемесячных копий;
- не реже раза в месяц выполнять тестовое восстановление.
GET /api/health/liveподтверждает, что процесс работает.GET /api/health/readyпроверяет PostgreSQL и показывает состояние надёжной очереди переходов.GET /api/metricsотдаёт метрики процессов, HTTP-запросов и очереди в формате Prometheus. Передавайте заголовокAuthorization: Bearer <METRICS_TOKEN>.
Пример:
Invoke-WebRequest http://127.0.0.1:43176/api/metrics `
-Headers @{ Authorization = "Bearer $env:METRICS_TOKEN" }Рекомендуемые первоначальные оповещения:
- endpoint готовности недоступен дольше двух минут;
- значение
shortli_click_spool_pendingостаётся выше нуля пять минут; shortli_click_spool_bytesпревышает 80% отshortli_click_spool_max_bytes;shortli_click_buffer_depthпревышает 80% отshortli_click_buffer_capacity;shortli_click_events_dropped_totalувеличился хотя бы на единицу;- доля ответов 5xx превышает 2% за десять минут;
- свободное место на диске опустилось ниже 15%;
- успешная резервная копия не появлялась более 30 часов.
Подробные события переходов хранятся в течение
ANALYTICS_RETENTION_DAYS дней, а обработанные обращения модерации — в течение
REPORT_RETENTION_DAYS дней. Общий счётчик переходов сохраняется у ссылки и
после удаления подробных событий.
Путь перенаправления не выполняет файловых операций: событие помещается в
ограниченный буфер размером CLICK_BUFFER_CAPACITY, после чего отдельный writer
записывает его в shortli-click-spool с fsync. Размер spool вычисляется один
раз при старте и затем поддерживается O(1)-счётчиками, поэтому рост очереди при
аварии PostgreSQL не замедляет редиректы. Сохранённые файлы переживают перезапуск
контейнера и автоматически повторяются. Каждое событие имеет уникальный ключ в
базе данных, поэтому сбой между фиксацией транзакции и удалением файла не
удваивает переход.
При штатном завершении API прекращает принимать запросы, writer переносит буфер на диск, а обработчики пытаются записать оставшиеся события в PostgreSQL. При аварийном завершении процесса могут потеряться только события, которые ещё оставались в памяти; это осознанный компромисс в пользу доступности редиректов.
Размер очереди жёстко ограничен параметром CLICK_SPOOL_MAX_BYTES (по
умолчанию 256 МиБ), поэтому длительная недоступность PostgreSQL не заполнит весь
диск. После достижения лимита перенаправления продолжают работать, но новые
события аналитики отбрасываются и отражаются в метрике
shortli_click_events_dropped_total.
Публичная страница /report принимает сообщения о фишинге, вредоносном ПО,
спаме, выдаче себя за другое лицо и других опасных сценариях. Обращения:
- ограничиваются по сетевому адресу;
- не дублируются, пока находятся на рассмотрении;
- защищены honeypot-полем от автоматической отправки;
- хранят односторонний хеш IP-адреса вместо исходного адреса.
Владелец, администраторы и техподдержка рассматривают обращения на странице /stats. Обращение можно
отклонить, отметить рассмотренным, приостановить ссылку либо одновременно
приостановить её и заблокировать домен назначения. Заблокированный домен и все
его поддомены нельзя использовать для новых ссылок, пока владелец или администратор не
снимет блокировку.
На этой же странице действует ролевая модель доступа:
- владелец имеет полный доступ и управляет всеми ролями;
- администратор управляет сервисом и назначает администраторов, техподдержку и пользователей, но не владельцев;
- техподдержка работает со списком ссылок и очередью жалоб без доступа к управлению командой;
- пользователь управляет только своими ссылками и не имеет доступа к администрированию.
Для создания ссылок, аутентификации и отправки обращений действуют отдельные
ограничения частоты запросов. Ответ 429 содержит Retry-After и заголовки
текущего лимита. Валидатор URL также отклоняет учётные данные в
адресе, схемы кроме HTTP/HTTPS, localhost, частные адреса и зарезервированные
псевдонимы.
Текущая Docker-конфигурация рассчитана на один экземпляр backend, поэтому лимиты хранятся в памяти процесса. Перед горизонтальным масштабированием перенесите их в общий Redis-совместимый стор; иначе каждый экземпляр будет считать лимит независимо.
Перед каждым выпуском выполните локальные проверки:
cd backend
go test -race ./...
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...
cd ..\frontend
npm ci
npm audit --audit-level=high
npm run lint
npm run buildЭти же проверки выполняются в GitHub Actions. CI также запускает интеграционный сценарий с PostgreSQL, ищет секреты через Gitleaks, проверяет Compose и сканирует готовые контейнеры через Trivy. Внешние действия закреплены по полным commit SHA, а Dependabot еженедельно предлагает контролируемые обновления Go, npm, Docker и GitHub Actions.
На чистой базе задайте в .env случайный ADMIN_BOOTSTRAP_TOKEN длиной не менее
32 символов и запустите стек. Затем один раз выполните запрос:
$body = @{
email = "admin@example.com"
password = "replace-with-a-strong-password-42"
token = $env:ADMIN_BOOTSTRAP_TOKEN
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:43176/api/admin/bootstrap `
-ContentType "application/json" -Body $bodyПосле успешного ответа удалите ADMIN_BOOTSTRAP_TOKEN из .env и перезапустите
backend. Повторный bootstrap невозможен, пока в базе существует хотя бы один
владелец. Последнего владельца нельзя удалить или лишить этой роли.
TRUSTED_PROXY_CIDRS содержит только подсети Nginx и внешних прокси, которым
разрешено передавать клиентский IP. Backend разбирает X-Forwarded-For справа
налево и игнорирует заголовок, если непосредственный отправитель не входит в этот
список. Для Docker подходит сеть 172.16.0.0/12; при отдельном reverse proxy
добавьте его точную подсеть. Никогда не указывайте 0.0.0.0/0 или ::/0.
Удаление учётной записи удаляет и все принадлежащие ей короткие ссылки. Смена
пароля завершает все сеансы и отзывает активные API-ключи, поэтому после неё нужно
войти снова и при необходимости создать новые ключи. Миграция 0004 один раз
завершит существующие сеансы, поскольку старые значения были сохранены без хеша.