Skip to content

Deployment

github-actions[bot] edited this page Sep 23, 2026 · 3 revisions

Deployment

Reverse proxy / TLS

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:

  1. Forwards the real client IP (X-Forwarded-For by default), and
  2. 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.

Docker Compose variants

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.

Public redirects, private admin console

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.

systemd

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= scoped ReadWritePaths

See Installation for the install steps.

Horizontal scaling

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.

Observability

  • GET /healthz — liveness (process is up).
  • GET /readyz — readiness (database is reachable).
  • GET /metrics — Prometheus text exposition; gate it with SHORTR_METRICS_TOKEN if it's reachable from outside your monitoring network.
  • Structured JSON logs to stdout by default (SHORTR_LOG_FORMAT=text for human-readable); SHORTR_LOG_LEVEL controls verbosity.

Updating

docker compose pull && docker compose up -d
# or, bare metal:
sudo systemctl stop shortr
sudo cp shortr-new /opt/shortr/shortr
sudo systemctl start shortr

Database 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.

Clone this wiki locally