Portainer Templates logo

Portainer Templates

cairn cairn

Container

DashboardWeb

The directory page for the people you host services for: no account, multilingual, live status, one tiny container. Source: https://github.com/MorganKryze/cairn

Image details

Architecture: amd64, arm64
Image size: 5 MB
User: morgankryze

Source details

Stars: 79
Forks: 3
Language: Go
License: GPL-3.0
Updated: 2 days ago

Configuration

Type
Container
Platform
linux
Image
ghcr.io/morgankryze/cairn:latest
Ports
8080:8080/tcp
Volumes
/config
Restart
unless-stopped

Notes

Starts with no configuration at all and serves a getting-started page. Drop a services.yaml into the mounted /config directory and it reloads within seconds, no restart needed. It writes nothing at runtime, so it also runs read-only with every capability dropped.

Standalone Install

Select an install method, to see config/commands for deploying cairn

Installation method

Install on Portainer

Import all app templates into your Portainer instance, for easy 1-click deploys

  1. Ensure both Docker and Portainer are installed, and up-to-date
  2. Log into your Portainer web UI
  3. Under Settings → App Templates, paste the below URL
  4. Head to Home → App Templates, and the list of apps will show up
  5. Select cairn, fill in any config options, and hit Deploy

Template Import URL

https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json
Show Me demo

More install options in our documentation, or see MorganKryze/cairn for app-specific guidance.

cairn

The directory page for the people you host services for.
Build Security Tests Coverage Release License: GPL-3.0
Image Docker pulls Go Helm Docker Compose Kubernetes Context7

A cairn is a small stack of stones left by hikers who walked the trail before you, so you find your way without digging.


Your family, your clients, your friends: they don't want a dashboard, they want to know what this place is, what each tool does, and whether it works right now. cairn is that page. Written in their language, readable without an account or a manual, and 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 ten, your own text goes inline.
  • 🔍 Search from anywhere: start typing, or ⌘K. 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.


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.

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: 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 needs no rewriting rule.

Status monitoring

Whatever already tells you it is up. cairn does not ask you to change monitors. It reads the one you run, and the pills come 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 also says what cannot be read, and why.
Gatus
Gatus gets the warmest handshake, and has earned it. 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: the safest surface is the one that isn't there. cairn stores nothing, signs no one in, and 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
.
  • 🧱 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 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.

Quickstart

Nothing to write, nothing to mount, just look at it:
docker run --rm -p 8080:8080 morgankryze/cairn:stable

That is a running cairn on , telling you what to feed it. When you are ready to feed it, two files and one command:
# 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.)
# compose.yaml
services:
  cairn:
    image: morgankryze/cairn:latest
    ports:
      - 8080:8080
    volumes:
      - ./config:/config:ro

docker compose up -d

Open : that is a finished page. Everything else (title, languages, categories, status, theming) is one optional key at a time, at your pace: follow Getting started.
Prefer to see it live first? A public instance runs at , and the demo stack spins up your own copy, with a real Gatus and a handful of sample services, one intentionally dead, in one command:
git clone https://github.com/MorganKryze/cairn.git && cd cairn/demo
docker compose up -d --build

Documentation

Everything lives in docs/. Each page teaches the why before the how. 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 · Reverse proxies
Recipes Status page · Icons · Multiple files · Migration
Look up Reference · FAQ · Comparison

For your AI assistant

Those pages are indexed on Context7, 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, not a control panel: no auth, no widgets, no Docker socket, no admin UI. If the audience is you, the admin, Homepage or Homer will make you happier; the comparison is honest about it.

Contributing

cairn is young and opinionated, and other people's eyes make it better. Ideas and bugs in the issues, code and docs through contributing, within the scope above. And if cairn serves your people well, a coffee keeps its maintainer walking the trail.

Contributors

Following the all-contributors 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

💻 🤔

💻 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 is part of a pull request, not an afterthought.

Colophon

A colophon tells how the book was made, so here is mine: Go, plain YAML, Fraunces for the headings, and 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: 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.

Serve cairn on your own domain behind Caddy, Nginx or Traefik. Fill in your domain and copy the result. It's a starting point, some apps need their own base URL or extra headers set too.

Proxying cairn.example.com to http://cairn:8080

Add this to your Caddyfile

cairn.example.com {
	reverse_proxy http://cairn:8080
}

Check the logs first

Nine times out of ten the logs tell you exactly what went wrong.

  • In Portainer, go to Containers, click the container, then Logs. Or run docker logs cairn
  • Exit codes help too: 137 means killed, usually out of memory. 126 or 127 means the command inside the image is broken.

Port already in use

If deployment fails with "Bind for 0.0.0.0:8080 failed: port is already allocated", something else on your server is using that port.

  • Find what's using it: sudo ss -tlnp | grep :8080
  • Stop the other service, or pick a different host port. In 8080:8080 only the left number is yours to change, the right one belongs to the app.

Running but the page won't load

The container is up but nothing appears in your browser.

  • Use your server's real IP: http://your-server-ip:8080. The 0.0.0.0 link Portainer shows isn't a real address.
  • Give it a minute after first deploy, cairn can take a while to initialise.
  • Make sure your firewall allows the port, e.g. sudo ufw allow 8080

Image won't pull

Test the pull directly on the host: docker pull ghcr.io/morgankryze/cairn:latest

  • "manifest unknown" means the tag no longer exists. This template uses latest, so try pinning a specific version instead.
  • "toomanyrequests" is the Docker Hub rate limit. Log in with docker login to raise it.
  • "no space left on device" means a full disk. Reclaim space with docker system prune

"exec format error"

This means the image was built for a different CPU architecture than your server.

  • This image supports: amd64, arm64
  • Check yours with uname -m: x86_64 is amd64, aarch64 is arm64. Raspberry Pi and other ARM boards are the usual culprits.

Container keeps restarting

The unless-stopped restart policy relaunches the app after every crash, so the real error can scroll past.

  • Check the logs right after a restart, the last few lines before it died are the useful ones.
  • Get the exit code with docker inspect cairn --format '{{.State.ExitCode}}'
  • Still stuck? Redeploy once with the restart policy set to no so the failure stays visible.

Raise an issue

Found something which isn't working as it should? Here's how to report it.

A single container

cairn runs as one container, the simplest kind of app here. Just the one image to pull and nothing else wired up alongside it.

The app image

An image is the app packed up ready to go, everything cairn needs bundled into one download. This template pulls ghcr.io/morgankryze/cairn:latest, which Docker fetches once (about 5 MB) and then starts your own copy from.

Where the image comes from

Docker pulls its images from registries, public libraries of ready-built apps. cairn's comes from the GitHub Container Registry, published by morgankryze.

Version tags

The bit after the colon in the image name is the version tag. Here it's latest, which always points at the newest build, so a redeploy can bump you to a newer release without you asking. Pin a specific tag if you would rather stay on one version.

Which machines it runs on

Every image is built for particular CPU types. This one ships for amd64, arm64, so it runs on both regular x86 servers and ARM boards like a Raspberry Pi.

Ports

A port is the door the app answers on. A mapping like 8080:8080 means it's reachable on port 8080 of your server, where the left number is yours to change and the right one belongs to the app. Once it's running, open http://your-server-ip:8080 in a browser. It opens:

  • 8080:8080, likely the web interface

Volumes

A volume is where cairn keeps its files so they survive an update or a restart. Without one, anything it saves would sit inside the container and vanish the moment it's recreated. This template mounts:

  • /config as a volume Docker manages for you

Restart policy

The restart policy here is unless-stopped, so Docker restarts cairn after a crash or reboot, but leaves it off when you stop it on purpose. You can change this on the deploy screen. The choices are no (never restart), on-failure (only after a crash), unless-stopped (restart unless you stop it), and always (bring it back no matter what).

Networking

Nothing custom is set, so cairn sits on Docker's default bridge network: its own private space that reaches the outside world only through the ports it publishes.

Container name

Once it's deployed, Portainer names the container cairn. That's what you'll spot in the containers list and use in commands like docker logs cairn.

Platform

The platform is linux, the kind of system the container is built to run on. Docker and Portainer handle this on a normal Linux server.

Open source license

cairn is open source, released under the GPL-3.0 license. In plain terms the code is out in the open, so you're free to run it and change it to fit what you need.

Portainer app templates

Zooming out, this whole page comes from a Portainer app template: a short recipe telling Portainer how to set cairn up. Add the template list to Portainer once, then deploying cairn is a click rather than a wall of config.