English · Italiano
Self-hosted, containerized monitoring for other people's status pages. It polls the public status pages of the providers you depend on — GitHub, Cloudflare, Anthropic, npm, anything running Atlassian Statuspage — and messages you when one of them changes state.
It exists so a single developer or a small team gets early warning about upstream trouble without keeping five status dashboards open. It notifies on transitions, never on every poll, so a quiet week is a silent week.
Two editions from one codebase: Light (polling and notifications only, no server) and UI (the same engine plus a local dashboard, configured at runtime).
On this page: 1. What it does · 2. Quick start · 10. Roadmap · 11. Branch layout and merge policy
The manual, one file per section:
- 3. Configuration —
config.yml, runtime settings, secrets, providers, channels, routing, delivery policy - 4. Docker — images, Compose profiles, volumes and probes, Kubernetes
- 5. Verifying a deployment — smoke checks, the dashboard, an end-to-end notification, troubleshooting
- 6. HTTP API — every route, the metrics, live updates, badges
- 7. How it works — data flow, components, when a notification fires, resilience
- 8. Theming and localisation — themes,
en/it, accessibility - 9. Development — layout, tech stack, live development, tests, releases
Each page has an Italian twin beside it (docs/<name>.it.md).
Every few minutes IsItDown fetches each provider's status page, normalises the answer, compares it with what it saw last time, and sends a message only if something actually changed.
GitHub operational ████████████████████████████ 99.98%
Cloudflare degraded ███████████████▁▁▁▁▁████████ 99.61% ← you get a message
Anthropic operational ████████████████████████████ 99.93%
- No external dependencies at runtime. No database server, no message broker, no cloud account. A JSON file or an embedded SQLite file is enough at this scale.
- Config-driven. Adding a provider never means touching code — an entry in
config.yml(Light) or a dialog in the dashboard (UI). - Idempotent notifications. Only state transitions notify: operational → degraded, degraded → outage, outage → resolved. A restart notifies nothing.
- Provider-agnostic. Most providers run Atlassian Statuspage and need no code at all; anything else gets a small adapter.
- Secrets from the environment only. A token is read from an environment
variable, never written to a config file, a database, an API response or a log
line. The UI edition can save one to a
0600file beside its database, still as a variable, so the dashboard can set a credential without a restart.
| Light | UI | |
|---|---|---|
| Image | ghcr.io/devmanfre/isitdown:light-latest |
…:ui-latest (built FROM light) |
| Poller · Adapters · Diff Engine · Notifiers | shared | shared |
| Configuration | config.yml, re-read every cycle |
SQLite, edited in the dashboard |
| State store | JSON file, atomic writes | SQLite (also carries history) |
| HTTP server | none | Express on :3000 |
| Uptime history and charts | — | 7/30/90-day views, plus a year heat calendar per provider |
| Theme | — | light / dark / system |
| Localisation | notification text | notification text and the whole dashboard |
| Footprint | 264MB image, no listening socket | 267MB — the Light image plus one layer |
Both editions run the same core engine. They differ only in what gets injected into it: where configuration comes from, and where state is kept.
UI edition, without a clone — one file, two commands. The images are
published to GHCR for linux/amd64 and linux/arm64, so this is also the
Raspberry Pi, Unraid, Portainer and Synology path:
curl -O https://raw.githubusercontent.com/DevManfre/isitdown/main/docker-compose.yml
docker compose --profile ui up -d
# then visit http://localhost:3000Everything the UI edition needs is configured in the dashboard, credentials
included: Settings → a channel → Value → Salva applies at once. Put them in a
.env next to the compose file instead if you would rather the container own
them — it is optional, and read if present:
printf 'TELEGRAM_BOT_TOKEN=...\nTELEGRAM_CHAT_ID=...\n' > .env
docker compose --profile ui up -d # recreates the container with the tokensLight edition — polling and notifications, nothing listening. This one needs
a config.yml to mount, so start from a clone:
git clone https://github.com/DevManfre/isitdown.git && cd isitdown
cp .env.example .env # only fill in the channels you will enable
cp config.example.yml config.yml # edit: providers, interval, channels
docker compose --profile light up -d
docker logs -f isitdown-lightBoth can run at once; they use separate data volumes.
Contributors building from the source tree add --build, which overrides the
pull and builds the image locally instead:
docker compose --profile ui up -d --buildRequires Node 24 for the build as well as the runtime — .nvmrc pins it and
npm install refuses anything older, because the runtime's own SQLite driver and
native TypeScript support are load-bearing at build time too. build:ui
additionally runs Vite to bundle the dashboard; build:light skips that step,
since Light ships no dashboard.
nvm use # or: nvm install 24
npm install
cp config.example.yml config.yml
cp .env.example .env
npm run build:light && node dist/light/index.js # Light
npm run build:ui && node dist/ui/server.js # UI, then open :3000Useful overrides when running locally: CONFIG_PATH, DATA_PATH, DB_PATH,
PORT, LOG_LEVEL (see 3.3).
Delivered:
- v1 — Light edition: polling, the diff engine, Telegram and generic-webhook
notifications,
config.ymlwith environment-referenced secrets, a JSON state store with atomic writes,ghcr.io/devmanfre/isitdown:light-latest. - v1.1 — UI prototyping: the dashboard explored in Claude Design and kept in
design/claude-design-prototypes/. Option3a, the navigable console, is the implementation reference; the dark palette and the longer Italian label lengths were validated there rather than discovered later. - v1.2 — UI edition: that design as an Express + vanilla-ES-module dashboard over
SQLite, with configuration managed at runtime and applied on the next cycle without
a restart.
ghcr.io/devmanfre/isitdown:ui-latest, builtFROMthe Light image. - v1.3 — history: per-provider uptime and incident history with status-page daily
bars and 7/30/90-day views, aggregated server-side and served from
/history. - v1.4 — dark mode and i18n: token-based light/dark/system theming with a
persisted preference, and a localised dashboard (
en,it) on top of the localised notification messages both editions already share.
Since v1.4 (on dev): adapters for AWS, Google Cloud and Azure, conditional
requests so most cycles are a 304, a per-provider poll cadence, the delivery log
view, a provider removal that can be undone inside a restore window, ntfy and
Gotify alongside the other channels, a year heat calendar per provider,
readiness split from liveness, an integrity check and vacuum on demand, and a
committed Grafana dashboard.
Still open:
- A native review of the Italian strings.
The roadmap that carried the project this far is finished — every row of it
shipped — and is archived at docs/roadmap/ROADMAP.md.
What comes next is being weighed in ROADMAP_2.md and ROADMAP_3.md, neither of
which commits to anything.
What IsItDown is. Both an aggregator of status pages and a monitor of
things that publish none — and every provider says which of the two answers for
it. A status page provider is declared: its own page is the record, and a
probe that disagrees is reported as a disagreement rather than as an overrule. A
probe is observed: our reading is the record, and a page that contradicts it is
an opinion. The default comes from the adapter and needs no configuration; the
authority field is only there for the cases where the operator disagrees with
that default. This was left open for two roadmaps, and leaving it open is why
"what does 93% mean here" had two answers on one screen.
Explicit non-goals: multi-user auth (this is a local, single-operator dashboard), status pages behind a login, and a packaged mobile app.
The Claude Code tooling (.claude/, CLAUDE.md) and the merge filter itself
(.mergeexclude, .githooks/, scripts/) are tracked on dev only. On
main none of those paths exist — neither in the commit nor in the working tree.
Everything else (source, docs, config) flows normally from dev to main.
Because the filter is not readable from main, it is installed into this clone's
.git directory, which every branch shares.
git switch dev
scripts/setup-hooks.shIt copies:
From (dev) |
To (shared by every branch) |
|---|---|
scripts/git-merge-clean |
$GIT_DIR/merge-clean |
.githooks/* |
$GIT_DIR/hooks/* |
.mergeexclude |
$GIT_DIR/merge-exclude |
and installs the git mergeclean alias. Re-run it after changing
.mergeexclude or scripts/git-merge-clean.
git switch main
git mergeclean dev # not `git merge dev`git mergeclean merges the branch, drops the paths listed in the exclude list,
commits with the repo's 🔀 subject format, and removes those paths from the
working tree. It refuses to run on a dirty tree. Genuine conflicts outside the
excluded paths stop the run so you can resolve them and git commit as usual.
| Piece | Role |
|---|---|
$GIT_DIR/merge-exclude |
the path list |
$GIT_DIR/merge-clean |
the merge wrapper (--sync purges, --guard checks) |
$GIT_DIR/hooks/post-checkout |
purges the excluded paths after a branch switch |
$GIT_DIR/hooks/pre-merge-commit, pre-commit |
abort any commit that would add an excluded path to a branch that does not track it |
A plain git merge dev on main is refused by the guard hooks — run
git merge --abort and use git mergeclean instead. git commit --no-verify
bypasses the guard if you ever genuinely need to.
- The setup is per clone and cannot be automatic. Git never runs hooks taken
from a clone, and a clone that only ever checks out
mainhas nothing to install from. On a new machine, check outdevand run the setup before merging anything intomain. - Until the setup is run, nothing is enforced. A plain
git merge devon a fresh clone will pull.claude/andCLAUDE.mdintomainas soon as you resolve the conflicts it raises.
- Edit
.claude/,CLAUDE.md,.mergeexclude,.githooks/andscripts/only while ondev— onmainthey do not exist. - The purge only deletes files that
devalso has and that are byte-identical to it, so machine-local files (.claude/settings.local.json) and local edits are never touched.
