Example: a single container
Uptime Kuma is a status page and uptime monitor. It runs as one container with SQLite inside, so it is the simplest kind of template: one image, one folder for its data, one port for the web address.
The whole manifest
apiVersion: obacht.dev/v2
kind: Template
metadata:
name: uptime-kuma
displayName: Uptime Kuma
description: |
Self-hosted status and uptime monitoring. Runs as a single container
with a persistent volume for its embedded SQLite database.
version: "1.0.3"
author: obacht
license: MIT
homepage: https://github.com/louislam/uptime-kuma
tags: [monitoring, status-page, self-hosted]
trustLevel: official
spec:
minSpecVersion: "v2.2"
compatibility:
architectures: [linux/arm64, linux/amd64]
resources:
minRamMb: 128
minDiskMb: 256
runtime:
type: container
container:
image: louislam/uptime-kuma:1
env:
UPTIME_KUMA_PORT: "3001"
TZ: "${cfg.timezone}"
volumes:
- source: ${cfg.dataPath}
target: /app/data
network: obacht-edge
labels:
obacht.template: uptime-kuma
services:
- name: web
targetType: container_port
targetPort: 3001
configSchema:
- key: dataPath
label: Data directory (on device)
type: text
advanced: true
immutable: true
default: "/var/lib/obacht/uptime-kuma/${instance.id}/data"
description: Where the SQLite database lives on the device.
- key: timezone
label: Timezone
type: timezone
default: "UTC"
description: For monitor schedules and the clock in the app.
This is the template as it ships in the registry, shortened only in the description. Yours sets trustLevel: community.
Line by line
minSpecVersion: "v2.2" because dataPath is immutable, which v2.2 added. Without that flag v2.1 would do.
architectures lists both, because the image louislam/uptime-kuma:1 is published for arm64 and amd64. Check this with docker manifest inspect <image> before you claim it.
resources is small on purpose: Uptime Kuma idles at well under 128 MB. The numbers decide on which devices the store offers the template, so measure, do not guess.
image uses the major tag :1, so devices get Uptime Kuma 1.x updates when the image is pulled again. If you want every device on exactly the image you tested, add imageDigest: sha256:… next to it. Bundles are different: there the registry pins every image for you, see the Compose example.
env sets the port the app listens on, and passes the time zone from the settings with ${cfg.timezone}.
volumes maps a folder on the device into the container. The source is a setting, so it can be changed in advanced mode, and its default contains ${instance.id}, so two Uptime Kumas on one device do not share a database. Keep data under /var/lib/obacht/<template>/.
network: obacht-edge is the shared network Caddy uses to reach your container. Every single-container template joins it.
services points the web address at port 3001. Caddy handles HTTPS in front of it. There is no ports: mapping: nothing is published on the device itself. There is no readiness block either: Uptime Kuma answers on / within seconds, so the default probe is enough.
configSchema asks for two things, and the easy install sheet shows only one of them. dataPath is advanced, so most people never see it and get the default. timezone is the one real question, with a searchable picker and a default that works.
What the user gets
The easy install sheet shows the web address, then one field: Timezone, preset to UTC. They click Install, the device pulls the pinned image, starts it, Caddy gets a certificate, and the Ready step shows the address with a QR code.
Make it yours
Most single-image apps follow this exact shape. Change the image, the port, the target path of the volume and the environment the app expects, then decide which one or two settings are worth asking. Leave everything else as it is here.
Next: Example: a Compose bundle.