Deploy to Railway
Watchdog runs on Railway as two services: Watchdog itself (built from the repo's Dockerfile) and PostgreSQL. The repo's railway.json sets the Dockerfile build, the health check at /api/health/live and the restart policy.
One click
Use the Watchdog template from the README. It creates:
| Service | Source | Notes |
|---|---|---|
Watchdog | this repository, docker/Dockerfile | Public domain, health check at /api/health/live |
Postgres | Railway PostgreSQL | Provides DATABASE_URL |
Everything is filled in for you, including the public domain and a generated WATCHDOG_SECRET_KEY. APP_URL is detected from the Railway domain. If you add a custom domain later, follow Moving to a custom domain below.
First run
- Open the generated domain.
- Enter the setup token. Set
SETUP_TOKENyourself, or read it from the service's deploy logs (First-run setup token). - Create the owner account and add two-step verification.
Back up WATCHDOG_SECRET_KEY. Stored alert credentials cannot be decrypted without it. Database migrations run automatically on every start.
Manual setup
- Create a project from the GitHub repository. Railway reads
railway.jsonand builds the Dockerfile. - Add a PostgreSQL database and reference its
DATABASE_URLfrom the service. - Set
WATCHDOG_SECRET_KEYto a random value of at least 32 characters (openssl rand -hex 32) andTRUST_PROXYto1. - Under the service's Networking tab, choose Generate domain.
Moving to a custom domain
APP_URL is detected from the generated Railway domain until you set it yourself, and Watchdog does not follow a domain change on its own. Passkeys, invite links, OAuth callbacks and every DSN depend on it, so change it in this order:
- Add the domain to the
Watchdogservice under Networking, Custom Domain, and create the DNS record Railway shows you. Wait until Railway reports the domain as active. - Set
APP_URLon theWatchdogservice to the new origin, for examplehttps://watchdog.example.com(no path, no trailing slash), and redeploy. Do this before you register any passkey on the new domain. - Check your recovery codes. Passkeys are bound to the host they were created on, so passkeys made on the old Railway domain stop working on the new one. Make sure you still have a recovery code, an authenticator app or email codes. Under Security you can make new recovery codes.
- Open the new domain and sign in with another method, then add a new passkey under Security. Remove the old ones.
- Update the DSN host in every app that reports to Watchdog. A DSN contains the host (
https://<key>@watchdog.example.com/<project-id>), so apps keep sending to the old address until their variable is changed. The variable name depends on your app, for exampleWATCHDOG_DSN,VITE_WATCHDOG_DSNorNEXT_PUBLIC_WATCHDOG_DSN. Redeploy or rebuild each app, because front-end apps bake the DSN into their bundle. - Remove the old Railway domain once nothing uses it anymore.
If you forget step 2, Watchdog tells you. The server logs a warning, the Security page and the passkey dialog show a banner naming both addresses, and adding a passkey fails with a message saying which APP_URL to set.
Blocked by the shield (temporary IP ban)
Watchdog uses nestjs-shield to rate limit requests. An address that keeps being rejected (default: 10 rejections within a minute) is blocked for 5 minutes, and the block doubles on every repeat, up to a day. That can include your own address, for example after a burst of failed requests from a broken script or a misconfigured proxy. While blocked, the app shows "Access temporarily blocked" with the time left.
Behind Railway, set TRUST_PROXY=1 (the template does). Without it every visitor shares the proxy address and one bad client blocks everyone.
To recover:
- Wait. The message says how long. The block ends by itself.
- Restart the service on Railway. Blocks are held in memory, so a restart clears them. If
REDIS_URLis set, blocks are stored there and survive restarts: restart or clear Redis instead. - Use the shield dashboard. Set
SHIELD_DASH=true,SHIELD_ADMIN_USERandSHIELD_ADMIN_PASSWORD, redeploy, and open/api/shield/admin. It lists blocked addresses and lets you lift them. The dashboard is exempt from the shield, so it stays reachable. Turn it off again when you are done.
To tune it, set SHIELD_AUTOBAN_THRESHOLD, SHIELD_AUTOBAN_WINDOW and SHIELD_AUTOBAN_DURATION (milliseconds), or list trusted addresses in SHIELD_WHITELIST (comma-separated IPs or CIDR ranges, for example 203.0.113.7,198.51.100.0/24). Whitelisted addresses skip every check, so only list addresses you control.
Keeping the database small
Replays and performance data grow fastest. Lower RETENTION_REPLAY_DAYS and RETENTION_PERFORMANCE_DAYS if storage becomes a concern.
Publishing the template (maintainers)
The template is built in Railway's dashboard. The GitHub repository must be public and bun run build and bun run test must pass on main. Railway's dashboard changes over time, so if a menu name differs, look for the closest match.
- In Railway, create a new template project.
- Add a service from the GitHub repo
Yassinos-coder/watchdog. Name itWatchdog. - Add a PostgreSQL service from Railway's database list. Keep its name as
Postgres. - On the
Watchdogservice open Variables, RAW Editor and paste:
DATABASE_URL=${{Postgres.DATABASE_URL}}
WATCHDOG_SECRET_KEY=${{secret(64, "abcdef0123456789")}}
TRUST_PROXY=1
SETUP_TOKEN=${{secret(32)}}
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=If the editor rejects the empty values, remove the SMTP_ lines. The app runs without them.
- Add descriptions, for example to
SMTP_HOST: "Optional: your mail server, used for invites and email codes", and toSETUP_TOKEN: "Needed once to create the owner account". - Enable a public domain for the
Watchdogservice. - Click Publish to Marketplace and fill in the listing.
Check it works
After publishing, deploy it into a throwaway project and verify:
- The deploy goes green and the health check at
/api/health/livepasses. - The generated domain opens the setup screen.
- Creating the owner with the
SETUP_TOKENvariable works. - Opening Projects, creating a project and sending a test error from an SDK shows an issue.
Then delete the throwaway project.