-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment
Three good options. Pick by ops preference. In every case, mount a volume for the dead-letter inbox so failures survive a redeploy.
Cheapest, predictable, full control.
docker build -t webhook-to-email .
docker run -d \
--name webhook \
--restart unless-stopped \
-p 3000:3000 \
--env-file .env \
-v webhook-data:/app/data \
webhook-to-emaildocker compose up -ddocker-compose.yml ships in the repo with a healthcheck on /health, restart: unless-stopped, and a named volume webhook-data mounted at /app/data for the dead-letter inbox.
| Provider | Spec | Monthly cost | Notes |
|---|---|---|---|
| Hetzner CX22 | 2 vCPU, 4GB | low | Best price to performance |
| Fly.io | shared 1x | free to low | Global edge, good for multi-region |
| Render | starter | low | Simple deploys |
| Railway | hobby | low | Easy to add a volume |
| DigitalOcean | basic droplet | low | Familiar |
your-domain.com {
reverse_proxy localhost:3000
}
Run Caddy via systemd. HTTPS is provisioned automatically by Let's Encrypt.
fly launch
fly volumes create webhook_data --size 1
fly secrets set RESEND_API_KEY=re_... NOTIFY_EMAIL=you@... WEBHOOK_SECRET=...
fly deployMount the volume at /app/data in fly.toml so the dead-letter inbox persists:
[mounts]
source = "webhook_data"
destination = "/app/data"Scale with fly scale count 2 for two instances. Note that each instance keeps its own in-memory queue and its own dead-letter file on its own volume.
- New, Web Service, connect the repo.
- Environment Node, build
npm install, startnpm start. - Add env vars in Settings.
- Add a disk mounted at
/app/datafor the dead-letter inbox.
The free tier sleeps after idle; upgrade for always-on.
All three accept custom domains:
CNAME hooks.your-domain.com -> your-app.fly.dev (or the Render URL, or an A record to the VPS IP)
Then point Stripe, GitHub, Cal.com and Linear webhooks at https://hooks.your-domain.com/hooks/<source>.
curl https://your-domain.com/health
# {"ok":true}
curl -X POST https://your-domain.com/hooks/test \
-H "Content-Type: application/json" \
-d '{"hello":"world"}'
# 202 {"ok":true,"queued":true,...}
curl https://your-domain.com/dead-letter
# {"ok":true,"count":0,"items":[]}The email arrives once the worker delivers it.
| Metric | Why |
|---|---|
Queue depth (GET /) |
A growing depth means Resend is slow or failing |
Dead-letter count (GET /dead-letter) |
Permanent failures that need attention |
| 401 rate | Wrong secret or an attack |
| 500 rate | Bugs in templates |
Pipe stdout to a log aggregator (BetterStack, Axiom, Datadog) and alert on a rising dead-letter count.