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: 4 MB
User: morgankryze

Source details

Stars: 33
Language: Go
License: GPL-3.0
Updated: 5 hours 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 Coverage Release License: GPL-3.0 Go Image Self-hosted

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.

What your visitors get

  • A welcome note in your words: who hosts this, for whom, how to reach
you. Dismissable, remembered for a year (a plain cookie, no tracking).
  • Tools grouped by need, one plain sentence each, with optional "Learn
more" pages for the curious.
  • Live status pills fed by your Gatus,
each linking to its own endpoint page. Your server does the polling, never the visitor's browser.
  • Their language: the server picks it from the browser, a switcher pins
it. One config file, translations inline.
  • Search from anywhere: just start typing, or ⌘K. A name finds that one
service, not everything that mentions it; keyword search still reaches descriptions and hidden tags, accent-insensitive.
  • At home on a phone: the layout reflows to one hand, the header steps
aside as you scroll and returns the moment you head back up, and a burger keeps search within thumb's reach.
  • Built to stay readable: contrast measured against WCAG AA in both
themes, a skip link, named landmarks, search results announced through a live region, and right-to-left languages laid out the right way round. Tested with a browser, not yet with a real screen reader: if you use one, tell us what breaks.
  • Calm typography, light and dark, keyboard-friendly, and every feature
degrades gracefully without JavaScript.

What you get as the operator

  • One static binary, under 10 MB, zero runtime dependencies, no database.
  • YAML mounted read-only; edits apply live within seconds, and a config
error names the file, the line and the expected shape instead of taking the site down.
  • Legal pages served by cairn itself (legal notice, privacy…): the pages
self-hosters never have anywhere to put.
  • Wherever your domain has room: a domain, a subdomain, or a sub-path of
one you already use (example.org/cairn/, one flag). cairn handles the prefix itself, so the proxy needs no rewriting rule.

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. What little runs is kept deliberately tight, from the image down to the wire.
  • 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 all work out of the box. See the hardened compose.
  • A strict Content-Security-Policy (default-src 'none', inline fragments
pinned by hash) and the hardening headers, with no third-party script or font to trust.
  • No outbound requests of its own: air-gap friendly, its only companions
optional and yours, a self-hosted Gatus for status and icons you can serve yourself. The demo runs on a network with no route out and serves its own icons, so the page it shows you makes no third-party request at all.
  • A watched supply chain: every push and pull request runs govulncheck,
a Trivy image scan and CodeQL, Dependabot follows the dependencies, and every CI action is pinned to a commit rather than a movable tag.
  • Artifacts you can check: the image is signed with cosign and ships SLSA
provenance and an SBOM; the release binaries carry their own attestation. The verification commands are two lines.

Quickstart

Nothing to write, nothing to mount, just look at it:
docker run --rm -p 8080:8080 ghcr.io/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: ghcr.io/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/, and it is written to be read, not just grepped: each page teaches the why before the how, in plain prose a developer can enjoy rather than a reference dump to endure. If you like understanding how a thing works, start anywhere below.
StartGetting started, the five-minute path
ConfigureServices · Site · Text · Theming · Languages
DeployDocker Compose · Podman · Bare binary · Kubernetes · Helm · Reverse proxies
RecipesStatus page with Gatus · Icons · Multiple files · Migration
Look upReference · FAQ · Comparison

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.

Ideas, contributions, support

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

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 4 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.