# cairn **The directory page for the people you host services _for_.** [![Build](https://github.com/MorganKryze/cairn/actions/workflows/build.yml/badge.svg)](https://github.com/MorganKryze/cairn/actions/workflows/build.yml) [![Security](https://github.com/MorganKryze/cairn/actions/workflows/security.yml/badge.svg)](https://github.com/MorganKryze/cairn/actions/workflows/security.yml) [![Tests](https://raw.githubusercontent.com/MorganKryze/cairn/badges/tests.svg)](https://github.com/MorganKryze/cairn/actions/workflows/build.yml) [![Coverage](https://raw.githubusercontent.com/MorganKryze/cairn/badges/coverage.svg)](https://github.com/MorganKryze/cairn/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/MorganKryze/cairn?label=release&color=247b7b)](https://github.com/MorganKryze/cairn/releases) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE) [![Image](https://img.shields.io/badge/image-5%20MB-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/r/morgankryze/cairn/tags) [![Docker pulls](https://img.shields.io/docker/pulls/morgankryze/cairn?label=docker%20pulls&color=2496ED&logo=docker&logoColor=white&cacheSeconds=3600)](https://hub.docker.com/r/morgankryze/cairn) [![Go](https://img.shields.io/badge/Go-single%20static%20binary-00ADD8?logo=go&logoColor=white)](https://go.dev) [![Helm](https://img.shields.io/badge/Helm-chart-0F1689?logo=helm&logoColor=white)](docs/deployment/helm.md) [![Docker Compose](https://img.shields.io/badge/Compose-ready-2496ED?logo=docker&logoColor=white)](docs/deployment/docker-compose.md) [![Kubernetes](https://img.shields.io/badge/Kubernetes-ready-326CE5?logo=kubernetes&logoColor=white)](docs/deployment/kubernetes.md) [![Context7](https://img.shields.io/badge/Context7-ask%20your%20assistant-247b7b?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTEyIDJhMTAgMTAgMCAxIDAgMCAyMCAxMCAxMCAwIDAgMCAwLTIwem0wIDNhNyA3IDAgMSAxIDAgMTQgNyA3IDAgMCAxIDAtMTR6Ii8+PC9zdmc+)](https://context7.com/morgankryze/cairn)
> A cairn is a small stack of stones left by hikers who walked the trail > before you, so you find your way without digging.
cairn home page: a welcome note, service cards grouped by category with live status pills, and a category trail in the margin
--- Your family has no use for a dashboard, and neither do your clients or your friends. They want to know what this place is, what each tool does and whether it works right now. cairn is the page that tells them, written in their language and readable without an account or a manual, and it is boring for you to operate. ## Features ### What your visitors get - 👋 **A welcome note in your words**: who hosts this, for whom, how to reach you. Dismissable, remembered for a year. - 🗂️ **Tools grouped by need**, one plain sentence each, with a "Learn more" page for the curious. - 🚦 **Live status pills** fed by the monitor you already run. Your server does the polling, never the visitor's browser. - 🏷️ **A word for where a service stands**: coming soon, beta, new, deprecated, no longer available. The last two stop being links. - 🌍 **Their language**: the server reads it from the browser, a switcher pins it. cairn ships [eight](docs/configuration/i18n.md), your own text goes inline. - 🔒 **A lock on what needs an account**, right after the name, and a note on its page saying whom to ask for one. - 🔍 **Search from anywhere**: start typing, or ⌘K, and Tab through what it finds. A name finds that one service, not everything that mentions it. - 📱 **At home on a phone**: one-handed layout, and a header that steps aside as you scroll and returns the moment you head back up. - ♿ **Built to stay readable**: WCAG AA contrast in both themes, a skip link, named landmarks, announced results, right-to-left the right way round. - 🌗 **Calm typography, light and dark**, and every feature still works with JavaScript off. > CI measures the contrast in a real browser on every pull request. No audit > has covered cairn as a whole, and no screen reader user has tried it yet. If > you use one, [tell us what breaks](https://github.com/MorganKryze/cairn/issues).
Left: a service detail page, with the name beside its icon and a button opening the tool at the far end of the same row, then a live status pill, a paragraph explaining what the tool is for, and a screenshot with its caption. Right: the same directory on a phone, a row of category chips then cards each showing an icon, a sentence, a self-hosted flag and a status pill Behind a card when a visitor wants more than one sentence, and the same page in a hand. Seventeen seconds: a visitor lands, searches, opens a service page, switches language. Click it yourself if the player stays blank.
### What you get as the operator - 📦 **One static binary, no database**, in an image of about 5 MB to pull. - 📝 **YAML mounted read-only**, and cairn picks up an edit within seconds. A bad one names the file, the line and the shape it wanted, instead of taking the site down. - 📜 **Legal pages served by cairn itself**, in ordinary [markdown](docs/configuration/text.md): the notice and privacy pages self-hosters never have anywhere to put. - 🌐 **A domain, a subdomain, or a sub-path** of one you already use. cairn handles the prefix, so your [proxy](docs/deployment/reverse-proxies.md) needs no rewriting rule. - 🗃️ **Or no server at all**: `cairn -export site.zip` writes the site as [static files](docs/deployment/static.md) for Cloudflare Pages, Netlify or an Apache host. The status pills are the one thing that stays behind. ## Status monitoring **Whatever already tells you it is up.** Keep the monitor you run: cairn reads it from your server, never from the visitor's browser.
Self-hosted Gatus · Uptime Kuma · Cachet · Statping-ng · Upptime
Hosted Atlassian Statuspage · Instatus · UptimeRobot · Better Stack · StatusCake
Every one was read from a live instance, not from a manual. Anything else publishing a list of names and states takes six lines of config, and [which monitors cairn reads](docs/recipes/status.md#which-monitors-cairn-reads) also says what cannot be read, and why.
Gatus **cairn knows [Gatus](https://github.com/TwiN/gatus) best.** It is the only one cairn integrates with **both ways**, since `cairn -emit-gatus` writes its endpoint config out of your services, and the only one whose pills link to a page per service rather than to one page for everything. If you have no monitor yet, start there.
## Security **Secure by subtraction.** cairn stores nothing and signs no one in, and it takes no input it has to trust. - 🪨 **`FROM scratch`, non-root**: no shell, no package manager, no libc in the image, so a compromised process has nothing to pivot into. - 🛡️ **Runs locked down**: `read_only`, `cap_drop: ALL` and a self-probing healthcheck work out of the box. [Hardened compose](docs/deployment/docker-compose.md). - 🧱 **A strict Content-Security-Policy** (`default-src 'none'`, inline fragments pinned by hash), with no third-party script or font to trust. - 🔌 **No outbound requests of its own**, so it is air-gap friendly. The [demo](demo/README.md) runs on a network with no route out and still makes no third-party request. - 🔬 **A watched supply chain**: `govulncheck`, a Trivy image scan and CodeQL on every pull request and weekly on a schedule, and every action pinned to a commit rather than a movable tag. - 🧪 **More test than product**, and every check has to fail before it earns trust: patch the fix out, read the red. The badges above carry the live count. - ✍️ **Artifacts you can check**: cosign signature, SLSA provenance and an SBOM on the image, an attestation on the binaries. [Two lines to verify](SECURITY.md#verifying-what-you-pulled). ## Quickstart To look at it first, with nothing to write or mount: ```sh docker run --rm -p 8080:8080 morgankryze/cairn:stable ``` That is a running cairn on , telling you what to feed it. Feeding it takes two files and one command: ```yaml # config/services.yaml - id: pdf url: https://pdf.example.org icon: stirling-pdf name: PDF toolbox desc: Merge, split, compress your PDFs. ``` (Want two languages? `name: { fr: Boîte à outils PDF, en: PDF toolbox }`; every text key works both ways. See [Languages](docs/configuration/i18n.md).) ```yaml # compose.yaml services: cairn: image: morgankryze/cairn:latest ports: - 8080:8080 volumes: - ./config:/config:ro ``` ```sh docker compose up -d ``` now shows a finished page. If you would rather not keep a server running, add `url: https://tools.example.org` to a `config/site.yaml` and the same config exports as static files for Cloudflare Pages, Netlify or any Apache host, OVH shared hosting included: ```sh docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" \ morgankryze/cairn:stable -config /work/config -export /work/site.zip ``` The status pills stay behind, since nothing polls. The rest, and a pipeline that exports on every push, is in [Static hosting](docs/deployment/static.md). Everything else (title, languages, categories, status, theming) is one optional key at a time, at your pace: follow [Getting started](docs/getting-started.md). A public instance runs at **** if you want to see it live first, and the [demo stack](demo/README.md) spins up your own copy, with a real Gatus and a handful of sample services, one intentionally dead, in one command: ```sh git clone https://github.com/MorganKryze/cairn.git && cd cairn/demo docker compose up -d --build ``` ## Documentation Everything lives in [docs/](docs/README.md). Each page explains why a setting exists before it shows how to set it. Start anywhere.
Start Getting started, the five-minute path · Upgrading, what a new version can refuse and how to fix it
Configure Services · Site · Text · Theming · Languages
Deploy Docker Compose · Podman · Bare binary · Kubernetes · Helm · Air-gapped · Static hosting · Reverse proxies
Recipes Status page · Icons · Multiple files · Migration
Look up Reference · FAQ · Comparison
### For your AI assistant Those pages are indexed on **[Context7](https://context7.com/morgankryze/cairn)**, so an assistant with the Context7 MCP server can pull cairn's real documentation instead of inventing config keys that never existed. Ask it for `morgankryze/cairn` by name. ## Scope **Not a dashboard, on purpose.** cairn is a directory, with no auth, no widgets, no Docker socket and no admin UI. If the audience is _you_, the admin, [Homepage](https://github.com/gethomepage/homepage) or [Homer](https://github.com/bastienwirtz/homer) will make you happier; the [comparison](docs/comparison.md) is honest about it. ## Contributing cairn is young and opinionated, and it needs other people's eyes. Ideas and bugs in the [issues](https://github.com/MorganKryze/cairn/issues), code and docs through [contributing](CONTRIBUTING.md), within the scope above. And if cairn serves your people well, a [coffee](https://ko-fi.com/morgankryze) keeps its maintainer walking the trail. ## Contributors Following the [all-contributors](https://allcontributors.org) convention, which counts every kind of contribution rather than only the commits: an issue that names a real problem, a bug report with the screenshot that cracks it, and a translation are all work.

MorganKryze

💻 📖 🎨 🤔 🚧

AntonPalmqvist

🐛 🤔 🌍

rbourgeat

💻 🤔

justinjakull

🤔
💻 code · 📖 documentation · 🌍 translation · 🎨 design · 🤔 ideas · 🐛 bug reports · 🚧 maintenance The avatars come from GitHub itself rather than from a third-party image service, for the reason the coverage badge is self-hosted: nothing about this repository should depend on somebody else's uptime to render. Adding yourself here belongs in the pull request itself. ## Colophon A colophon tells how the book was made, so here is mine: Go, plain YAML, [Fraunces](https://github.com/undercasetype/Fraunces) for the headings, and [Claude Code](https://claude.com/claude-code) drafting at my side, never on autopilot. The taste, the reviews and the final word stay mine; the tests, the CI and the public history keep me honest. ## License Free software under [GPL-3.0](LICENSE): use it, modify it, share it. What you redistribute stays under the same license, source included. Hosting your own instance is not distribution and asks nothing of you.