Unconditional panic() on storage-init failure at startup causes crash-loop instead of graceful degrade/retry
Describe the bug
main.initializeStorage() (main.go:105) calls panic(err) unconditionally when
store.Initialize() fails at startup, for any underlying error — including a transient
network/DNS failure to the configured storage.type: postgres backend. Combined with a
restart: unless-stopped/always-style supervisor (Docker, systemd, k8s), this turns a
temporary storage-connectivity blip into a tight crash-loop that never lets Gatus reach the
point where it would actually run and report on the endpoints it's configured to monitor.
This is the same crash signature as #1633 (panic: EOF at main.initializeStorage/
main.go:105, fixed for that specific case by bumping lib/pq in #1644) — but the underlying
issue is broader than any one driver bug: any transient failure to reach the storage backend at
startup is fatal, by design (confirmed in #1395: "Gatus is built with fail fast in mind"). This
report is about a different trigger that isn't fixed by a driver bump.
What do you see?
Setup: storage.type: postgres, storage.path pointing at a Postgres/TimescaleDB container by
Docker Compose service name. When that container is removed from the Docker network (not
merely stopped/restarted — a genuinely different condition than a normal "connection refused"),
Docker's embedded DNS resolver (127.0.0.11:53) returns a malformed/unusual response rather than
NXDOMAIN, which the Go resolver surfaces as server misbehaving:
panic: dial tcp: lookup timescaledb-v2 on 127.0.0.11:53: server misbehaving
goroutine 1 [running]:
main.initializeStorage(0x1c611bc80000)
/app/main.go:105 +0x1766
main.main()
/app/main.go:34 +0xc5lib/pq propagates this raw dial error out of store.Initialize(), and main.go panics on it
unconditionally. Under restart: unless-stopped, this repeated 206 times over ~19 hours
(224 occurrences of the panic line in the container logs) until the storage container was
restored — Gatus never once reached the point of running its configured checks (including, in
our case, a TCP check on that same host, which would have alerted on exactly this outage if
Gatus had ever gotten past initializeStorage).
Full write-up (self-inflicted circular dependency on our end, but the panic-vs-graceful-degrade question is the part we think is worth raising upstream): https://github.com/jagatsingh/trading_infrastructure/blob/main/docs/timescaledb-v2-removal-incident.md
What do you expect to see?
Ideally, store.Initialize() failing at startup should not be an unconditional, un-retried
panic(). A few options that would all help:
- Retry storage-connect with backoff for some bounded window before giving up, rather than panicking on the first attempt.
- Log the fatal error clearly and
os.Exit(1)instead ofpanic(), so it's visually obvious this is a startup-config problem rather than an internal Gatus bug (the current panic/stack trace reads like an application crash, not "your storage backend is unreachable"). - (Bigger ask, understand if out of scope) an option to fall back to an in-memory store with a loud warning if the configured persistent store can't be reached at startup, rather than refusing to start at all — since a monitoring tool arguably has more value running degraded than not running.
Any of the above would have meant Gatus kept running and alerting throughout the incident above instead of crash-looping silently for ~19 hours.
Steps to reproduce
docker composewith two services: a Postgres/TimescaleDB container (db) and Gatus configured withstorage.type: postgres,storage.path: postgres://...@db:5432/gatus....- Start the stack; confirm Gatus is running normally.
docker rm -f db(a full remove, notdocker stop) while Gatus keeps running.- Wait for Gatus's own
restart: unless-stoppedpolicy (or manually restart the Gatus container). - Observe
panic: dial tcp: lookup db on 127.0.0.11:53: server misbehavingatmain.initializeStorage/main.go:105, repeating on every restart untildbexists again.
Version
v5.36.0
Additional information
- Confirmed via source read at the
v5.36.0tag that this is not a bug in the per-check TCP code path (client.CanCreateNetworkConnectionalready returns(false, nil)gracefully on any dial error, DNS failures included) — the panic is specifically in the storage-initialization path at startup. - Our fix on our end is to stop pointing Gatus's own storage at a container one of its own checks monitors (removes the circular dependency entirely) — filing this because the unconditional-panic-on-storage-init-failure behavior itself seems like it could bite anyone whose storage backend has any transient startup-time connectivity hiccup, not just our specific circular-dependency case.
Source: TwiN/gatus