Monitor and maintain
Check health, read logs, scrape metrics, run a storage check, and upgrade a Sluby server.
This page covers running a Sluby server day to day: the health endpoint, the status view, logs, Prometheus metrics, the storage check that compares your database with Sia, and upgrades.
The commands below are for the base stack. On a production server, add -f docker-compose.yml -f docker-compose.prod.yml after docker compose.
Service health
The postgres, redis, backend and web services have Docker health checks. See their state with:
docker compose psThe web proxy starts only after the backend reports healthy, and its own health check goes through to the backend, so web turns unhealthy when it can no longer reach the backend.
Health endpoint
GET /health answers while the backend process is up:
curl http://localhost:8080/health{ "status": "ok", "timestamp": "2026-10-20T09:00:00.000Z", "version": "0.2.0" }It reports liveness only, so a slow indexer never makes Docker restart the backend. The state of the database, Redis and the Sia indexer is in the status view below. The version field is the backend's version.
Status view
Admins see the full status in the Studio under Deployment > Health: database, Redis, the Sia indexer with account usage and hosts, the last storage check and the delivery cache. See Overview, health and metrics.
The same data comes from GET /api/v1/system/status. Admins get the full status, members a health summary: system status reference.
Logs
docker compose logs -f backendLOG_LEVELsets how much the backend logs.- With
NODE_ENVset toproductioneach line is a JSON object. Otherwise the lines are colored text. - Requests to
/healthlog atdebug, so the health checks do not fill the log atinfo. - Docker keeps at most five files of 10 MB of logs per service, then drops the oldest.
Prometheus metrics
The backend serves Prometheus metrics at GET /metrics on its port 3000. The Compose files publish no backend port, so a scraper reaches it in one of two ways:
| Scraper | URL | Without METRICS_TOKEN | With METRICS_TOKEN |
|---|---|---|---|
| On the Compose network | http://backend:3000/metrics | Served | Served with Authorization: Bearer <token> |
| Anywhere else, through the web proxy | https://<your domain>/metrics (your PUBLIC_URL) | 404, as for a path that does not exist | Served with Authorization: Bearer <token> |
So metrics are never public: through the web proxy they need METRICS_TOKEN, and without it the proxy answers 404 and does not reveal that the endpoint exists. The backend tells the two apart by the X-Forwarded-For header, which the web proxy always sets on /metrics, replacing any value the client sent. A request with X-Forwarded-For or Forwarded counts as proxied, so a proxy of your own in front of port 3000 that adds either header is treated the same way.
To scrape from outside the server, set a token:
METRICS_TOKEN=<generated value>and give your scraper the same value as a bearer token, for example in Prometheus:
scrape_configs:
- job_name: sluby
scheme: https
authorization:
credentials: <generated value>
static_configs:
- targets: ['video.example.com']The web proxy limits /metrics per client address like sign-in (WEB_AUTH_RATE and the settings next to it), which a scrape every few seconds stays well under.
To look at the metrics from the server:
docker compose exec backend node -e "fetch('http://127.0.0.1:3000/metrics').then(r => r.text()).then(console.log)"Add { headers: { Authorization: 'Bearer <token>' } } as the second argument to fetch when a token is set.
Besides the Node.js process metrics, the backend exports:
| Metric | Type | Meaning |
|---|---|---|
sluby_upload_success_total | counter | successful uploads |
sluby_upload_failure_total | counter | failed uploads |
sluby_upload_bytes_total | counter | bytes uploaded |
sluby_processing_duration_seconds | histogram | time from the start to the end of a video's processing job, counted when the video becomes ready |
sluby_processing_queue_depth | gauge | jobs waiting or running across the transcode, upload and finalize queues; NaN while Redis does not answer |
sluby_redis_up | gauge | 1 when Redis answered the queue depth read of this scrape within 2 seconds, 0 otherwise |
sluby_playback_initiation_total | counter, label mode | playback URLs handed out |
sluby_segment_requests_total | counter, label kind | objects served through the server (manifest, media, object) |
sluby_bandwidth_bytes_total | counter | bytes served through the server |
sluby_cache_hits_total | counter | whole-object cache lookups served from memory |
sluby_cache_misses_total | counter | whole-object cache lookups that fetched the object from Sia |
sluby_sia_upload_duration_seconds | histogram | time of each upload to Sia (one upload carries several objects packed together) |
Storage check
The storage check (reconciliation) compares the objects your database expects with what the indexer stores, and reports the differences. It runs on a schedule, and a separate sweep on its own schedule finishes deletions that stopped partway. What the report means: Deletion and reconciliation.
- In the Studio: an admin sees the result on the Storage check card of the Health page and starts a run with Check now.
- With the API: read the latest report with
GET /api/v1/reconciliationand start a run withPOST /api/v1/reconciliation/run, which needs an admin's key with themanagescope.
Turn the schedules on or off and change their intervals with RECONCILE_ENABLED, RECONCILE_INTERVAL_MS, DELETION_GC_ENABLED and DELETION_GC_INTERVAL_MS. The configuration reference lists each default.
Housekeeping the backend does
At startup and then every hour, the backend cleans up on its own. Nothing needs to be scheduled for this. It removes:
- uploads that expired unfinished (when an upload expires);
- source files it no longer needs;
- ended Studio sessions past their retention (sessions);
- webhook delivery records past their retention (delivery log).
Upgrade
Get the new code
git pullRebuild and restart
docker compose up -d --buildCheck the migrations
The backend applies database migrations every time its container starts, before it takes requests, and skips the ones it already applied. Check the log:
docker compose logs backend | grep -i migrationA successful start logs Running migrations... and Migrations complete. A failed one logs Migration failed: with the error, and the backend does not start.
When several backend containers start at once, they apply migrations one at a time; the later ones find nothing left to do.
On stop or restart, the backend stops taking requests and gives running work 25 seconds to finish. Docker waits 30 seconds before it kills the container.