Repository navigation
Deployment
Shortr's own HTTP server speaks plain HTTP only — terminate TLS in front of it (Nginx Proxy Manager, Traefik, Caddy, Cloudflare Tunnel, or a bare nginx). Whatever you use, make sure it:
- Forwards the real client IP (
X-Forwarded-Forby default), and - Its own address (or the whole proxy network) is listed in
SHORTR_TRUSTED_PROXIES.
Skipping step 2 means Shortr ignores the forwarded header entirely — every
click and every rate-limit bucket gets attributed to the proxy's IP, which
silently breaks analytics and can let one abusive client exhaust the rate
limit for everyone. See
deploy/NGINX_PROXY_MANAGER.md for a
worked example, and deploy/nginx-advanced.conf for a hand-rolled nginx
config.
| File | Use case |
|---|---|
docker-compose.yml |
SQLite, no proxy — good for a quick trial or a single-user instance behind your own reverse proxy on the host. |
deploy/docker-compose.postgres.yml |
PostgreSQL backend, for multi-GB click volumes or when you already run Postgres. |
deploy/docker-compose.npm.yml |
Wired for Nginx Proxy Manager — Shortr isn't published on a host port, only reachable via the NPM network. |
All variants read secrets from environment variables with no committed
defaults; SHORTR_SECRET_KEY and POSTGRES_PASSWORD (postgres variant) use
${VAR:?required} syntax so docker compose up fails loudly instead of
starting with an empty secret.
If you want the redirect hot path reachable from the internet (e.g. via a
Cloudflare Tunnel) while keeping /app, /api, /auth and /mcp
reachable only from your own VPN (Tailscale, Netbird, WireGuard, ...), see
deploy/NGINX_PROXY_MANAGER.md
section 6 — two NPM proxy hosts against one shortr backend, one path-locked
by an nginx rule, the other IP-locked to your VPN's subnet.
deploy/shortr.service runs the binary directly with:
-
DynamicUser=yes— no shared system account -
NoNewPrivileges=yes,ProtectSystem=strict,ProtectHome=yes,PrivateTmp=yes -
Restart=always,RestartSec=2 - A
StateDirectory=scopedReadWritePaths
See Installation for the install steps.
Run a single Shortr instance. Rate limits and the link cache are kept in memory per process, so several instances behind a load balancer would each enforce their own limits. If you need more throughput, give the one instance more CPU first.
-
GET /healthz— liveness (process is up). -
GET /readyz— readiness (database is reachable). -
GET /metrics— Prometheus text exposition; gate it withSHORTR_METRICS_TOKENif it's reachable from outside your monitoring network. - Structured JSON logs to stdout by default (
SHORTR_LOG_FORMAT=textfor human-readable);SHORTR_LOG_LEVELcontrols verbosity.
docker compose pull && docker compose up -d
# or, bare metal:
sudo systemctl stop shortr
sudo cp shortr-new /opt/shortr/shortr
sudo systemctl start shortrDatabase migrations run automatically at startup (shortr migrate also
exists to apply them without starting the server, e.g. in a CI/CD step
before a rolling restart). Migrations are tracked in schema_migrations and
safe to run repeatedly — an already-applied migration is skipped.