Reference
Every callable in the library, generated from the same catalog the
Assembler
reads — so it never drifts from the code. The catalog
itself is published at /catalog.json
for programmatic use.
Workloads
Deployable starting points. Import a stage by its canonical path, compose
+ features, and render with kurly.list.
2fauth
A 2FAuth server (a self-hosted web app to manage TOTP/HOTP two-factor accounts and generate one-time codes) on the official image; with the default SQLite backend its database lives on a PersistentVolume. kurly authors no Secret; APP_KEY (encrypts stored 2FA secrets) comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The 2FAuth server. appUrl is the public URL; secretName holds APP_KEY (envFrom). Data at /2fauth. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/2fauth/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "twofauth" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
secretName | string | — | "twofauth" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
aastro
An Aastro API gateway (it matches an incoming request against a flow, fans it out to the upstreams that flow names and aggregates their answers into one response) on the official image; its config.yaml is its only state, rendered as a ConfigMap. Flows are passed through VERBATIM — kurly does not model the flow schema — and default to none, so an unconfigured gateway answers its probes and routes nothing. Two listeners: the data port :7805 and an ADMIN port :7806 carrying health, readiness and Prometheus metrics, bound to 0.0.0.0 explicitly because Aastro binds it to loopback by default, where a kubelet probe never arrives and the pod never turns ready. Probes read the admin port, since a request to the data port is answered by an upstream. kurly authors no Secret; nothing in the configuration is a credential. Stateless, so any replica count is safe.
server http
The Aastro gateway. flows is passed through verbatim into gateway.routing.flows and gateway merges over the rest of the generated configuration (observability, rate limiter, trusted proxies, TLS). adminBindAddr keeps the admin listener reachable by the kubelet; keep the admin port off any exposure unless the diagnostics are meant to be public. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/aastro/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "aastro" |
image | string | — | — |
replicas | int | — | 2 |
flows | array | — | — |
timeout | string | — | "10s" |
headerTimeout | string | — | "5s" |
adminBindAddr | string | — | "0.0.0.0" |
gateway | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
accent
An Accent server (a translation and localisation tool for developers: it reads the translation files out of a repository, gives translators a web app to work in, and writes the files back). A plain composable http workload on the Elixir release image, backed by an external PostgreSQL — all state is in the database, so it claims no volume and can run several replicas. The release migrates the database as it starts, so the first boot against a fresh database is slower than the ones after it. Sign-in goes through an external provider (GitHub, GitLab, Google, Slack, Discord) or the password-less DUMMY_LOGIN_ENABLED; with neither configured nobody can sign in at all. Probed by connection, because the web app is a JavaScript bundle behind an authenticating router. Serves on :4000.
server http
The Accent server. All state is in PostgreSQL and the release migrates it on start. secretName holds DATABASE_URL and SECRET_KEY_BASE — the database password is embedded in the URL, and SECRET_KEY_BASE signs the session cookie, so a value that changes on every restart signs everybody out. canonicalUrl is the origin a browser reaches the instance at, which Accent builds the links in its web app and its emails from; unset, those links point somewhere else. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/accent/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "accent" |
image | string | — | — |
secretName | string | — | "accent" |
canonicalUrl | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
activepieces
An Activepieces server (a self-hosted, open-source no-code automation / workflow builder à la Zapier) on the official all-in-one image, backed by an external PostgreSQL and Redis. Pairs with a cnpg-cluster named activepieces-db and a Redis. Connection settings are env from the dbHost/database/dbUser/redisHost parameters; a provided Secret holds AP_POSTGRES_PASSWORD, AP_ENCRYPTION_KEY and AP_JWT_SECRET via envFrom (kurly authors none). Stateless: a plain rolling Deployment. Serves on :80.
server http
The Activepieces server. frontendUrl is the public URL (webhook URLs derive from it). Connection settings are env from dbHost/database/dbUser/redisHost; secretName holds AP_POSTGRES_PASSWORD, AP_ENCRYPTION_KEY and AP_JWT_SECRET (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/activepieces/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "activepieces" |
image | string | — | — |
replicas | int | — | 2 |
frontendUrl | string | — | "http://localhost:80" |
dbHost | string | — | "activepieces-db-rw" |
database | string | — | "activepieces" |
dbUser | string | — | "activepieces" |
redisHost | string | — | "activepieces-cache-headless" |
secretName | string | — | "activepieces" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
actualbudget
An Actual Budget server (a local-first personal finance and budgeting app). A plain composable http workload that keeps its budgets and sync state in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app and sync API on :5006.
server http
The Actual Budget server. Keeps everything in SQLite at /data on the volume (ACTUAL_DATA_DIR), so it needs nothing external. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/actualbudget/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "actualbudget" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
adguardhome
An AdGuard Home server (a self-hosted, network-wide DNS ad- and tracker-blocker with a friendly web UI, an alternative to Pi-hole) on the official image; its configuration and runtime data live on a PersistentVolume. It answers DNS on :53 (TCP/UDP), separate ports to add a Service for. The config and work directory both live under /opt/adguardhome, so a single volume persists everything; it binds the privileged DNS port so it runs as root with a writable root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. The admin UI (setup wizard) serves on :3000.
server http
The AdGuard Home server. Config and data under /opt/adguardhome; DNS (:53) needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/adguardhome/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "adguardhome" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
admidio
An Admidio server (member management for clubs and organisations: people, the roles they hold, events and the mailings that go with them) on the project's own image, backed by an external MySQL/MariaDB or PostgreSQL — dbType selects which dialect the generated config.php speaks and must match the server it points at. Configuration and uploaded files live on a PersistentVolume. The entrypoint provisions the application tree, chowns it to www-data, rewrites the Apache port configuration and starts cron and postfix before Apache drops privileges, so root, capabilities and a writable image tree are all relaxed deliberately. Every variable it reads is ADMIDIO_*, so service links are disabled. kurly authors no Secret; ADMIDIO_DB_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Admidio server. dbType is mysql (also MariaDB) or pgsql; dbHost/dbPort/database/dbUser point at it and secretName holds ADMIDIO_DB_PASSWORD (envFrom). rootPath is the public URL Admidio builds links against — left unset it keeps the placeholder from the shipped example configuration and every generated link points elsewhere. mailRelayHost gives the bundled postfix somewhere to deliver. Config and uploads at /opt/app-root/src/adm_my_files. The first request lands on the installation wizard, so probes are by connection. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/admidio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "admidio" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbType | string | — | "mysql" |
dbHost | string | — | "admidio-db" |
dbPort | int | — | 3306 |
database | string | — | "admidio" |
dbUser | string | — | "admidio" |
rootPath | string | — | — |
organisation | string | — | — |
mailRelayHost | string | — | — |
secretName | string | — | "admidio" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
adminer
An Adminer server (a full-featured database management tool in a single PHP file: MySQL, PostgreSQL, SQLite and more) on the official image. Stateless: a plain rolling Deployment. Serves on :8080.
server http
The Adminer server. Stateless; connects to whatever database you point it at. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/adminer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "adminer" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
adventurelog
An AdventureLog backend (a travel log: the places you have been, the trips you are planning, and the photos, dates and map positions that go with them). A composable http workload running the BACKEND image only — the SvelteKit web front end is a separate image and is not carried here — with uploaded photos on a PersistentVolume and everything else in an external PostgreSQL. That database must have PostGIS: the application is a GeoDjango project whose first migration creates the extension, so a plain PostgreSQL leaves the pod migrating, failing and restarting forever. publicUrl is the origin a browser reaches it at, baked into the media URLs the API returns and validated against for CSRF, so a wrong value shows an application that loads and cannot save. supervisord runs nginx, memcached and gunicorn together and drops privileges from root, so this workload is deliberately less hardened. The first start migrates, creates the extension and downloads the world region dataset before anything answers, which is minutes. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The AdventureLog backend: nginx, memcached and the Django application in one pod. Uploaded photos live at /code/media on the volume; everything else is in PostgreSQL, which must have PostGIS. publicUrl and frontendUrl are the origins Django trusts for CSRF and hands out media URLs against; csrfTrustedOrigins adds more. adminUsername/adminEmail create the first administrator on start. secretName holds POSTGRES_PASSWORD, SECRET_KEY and DJANGO_ADMIN_PASSWORD — all three ship with published defaults upstream, and SECRET_KEY signs sessions. Probed by connection, because every path redirects or answers 403 unauthenticated. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/adventurelog/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "adventurelog" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "adventurelog-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "adventurelog" |
dbUser | string | — | "adventurelog" |
publicUrl | string | — | "https://adventurelog.example.com" |
frontendUrl | string | — | "https://adventurelog.example.com" |
csrfTrustedOrigins | array | — | — |
adminUsername | string | — | "admin" |
adminEmail | string | — | "admin@example.com" |
disableRegistration | bool | — | false |
secretName | string | — | "adventurelog" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
agentgateway
An agentgateway proxy (it puts several MCP servers, A2A servers and HTTP APIs behind one endpoint, applying CORS, authentication and rate-limiting policies on the way through). A plain composable http workload on the project's own image: the whole gateway is its configuration file, rendered as a ConfigMap, so it keeps nothing and replicas scale freely. STDIO TARGETS RUN A COMMAND IN THIS CONTAINER — a target declared with `stdio` makes the gateway fork a process and speak MCP over its pipes, so it can do whatever this pod can do and needs a binary this image does not ship; in a cluster the arrangement that works is an HTTP target pointing at an MCP server running as its own workload. Serves proxied traffic on :3000.
gateway http
The agentgateway proxy. targets are the MCP servers exposed through it, passed through verbatim; config merges over the rendered configuration for policies, listeners and telemetry; port is both the listener and the Service port. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/agentgateway/gateway.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "agentgateway" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 3000 |
targets | array | — | — |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
airsonic-advanced
An Airsonic-Advanced server — a free, self-hosted music streaming server, an actively-maintained fork of Airsonic. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4040.
server http
The An Airsonic-Advanced server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/airsonic-advanced/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "airsonic-advanced" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
airtrail
An AirTrail server (a personal flight log: routes on a world map, distances and time in the air, importable from the trackers people already use). A composable http workload backed by an external PostgreSQL, with uploaded files on a PersistentVolume. Set `origin` to the URL people visit — this is a SvelteKit application, and it refuses every sign-in and every saved flight whose Origin header does not match, which reads as broken rather than misconfigured. The image runs as its own unprivileged account and drops no privileges itself, so the hardened posture stands. The entrypoint migrates the database before the server listens, which is what the startup probe budgets for. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The AirTrail server. Uploaded files live at /app/uploads on the volume; everything else is in PostgreSQL. secretName holds DB_URL, the whole PostgreSQL connection string, because it carries the password. origin is the URL a browser reaches this instance at and is checked on every state-changing request. bodySizeLimit is well above a form's usual size, since detailed flight tracks are submitted inside the flight form. Probed on /api/ping, the endpoint the image's own health check uses. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/airtrail/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "airtrail" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
origin | string | — | — |
secretName | string | — | "airtrail" |
bodySizeLimit | string | — | "20M" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
alertmanager
An Alertmanager as a prometheus-operator `Alertmanager` custom resource: it receives alerts from a Prometheus, groups and deduplicates them, and routes them to receivers. Authors the CR (like prometheus) for the operator to reconcile into a StatefulSet and the `alertmanager-operated` Service. Requires the prometheus-operator installed. Routing comes from the AlertmanagerConfig objects it selects.
server alertmanager
The Alertmanager. alertmanagerConfigSelector (verbatim operator schema) decides which AlertmanagerConfig objects supply routing/receivers; {} selects everything, none runs the operator default. Wire a Prometheus to it through that workload's spec escape (alerting.alertmanagers -> alertmanager-operated:web). Reach it at alertmanager-operated.<namespace>.svc:9093.
Import: import 'github.com/metio/kurly/workloads/alertmanager/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "alertmanager" |
image | string | — | — |
replicas | int | — | 1 |
retention | string | — | "120h" |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
alertmanagerConfigSelector | object | — | — |
namespaceSelector | object | — | — |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
algernon
An Algernon server (a self-contained web server for static files, Markdown and Lua scripts, with templates and a built-in database). A composable http workload with the served directory and the Bolt database on one PersistentVolume, so it needs nothing external. The image's own entrypoint serves HTTPS from a certificate it does not ship, so this stage replaces it with the same server in plain HTTP mode and lets the exposure terminate TLS. It carries no site: a fresh volume is empty and it answers with a directory listing until content is put under www/ on it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Algernon server. Serves contentPath over plain HTTP on the given port and keeps its Bolt database (users, permissions, key-value data) beside it on the volume mounted at /data. The container command is built here rather than inherited, because the published entrypoint wants a TLS keypair in a second volume; extraArgs is spliced in before the served directory and the address. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/algernon/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "algernon" |
image | string | — | — |
port | int | — | 3000 |
contentPath | path | — | "/data/www" |
databaseFile | path | — | "/data/algernon.db" |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
alist
An AList server (a self-hosted file list / WebDAV program fronting many storage backends — local disk, S3, WebDAV, cloud drives — behind one web UI) on the official image. A plain composable http workload that keeps its SQLite database and configuration on a PersistentVolume under /opt/alist/data. Single writer over a ReadWriteOnce volume: one replica, recreated. On first start it logs a randomly generated admin password. Serves the UI and WebDAV on :5244.
server http
The AList server. Database and config at /opt/alist/data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/alist/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "alist" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
alloy
A Grafana Alloy agent — an OpenTelemetry collector distribution that scrapes metrics, tails logs and receives traces, then forwards them on. A composable daemon workload, because collecting a node's logs means being on that node. ALLOY STORES NOTHING: it is the collector half of a stack, so a deployment needs a backend for what it forwards, and one with no destination configured runs happily and drops everything. THE CONFIGURATION IS A PROGRAM, NOT A DOCUMENT — Alloy's own language wires components to each other by reference, so config takes that language verbatim rather than pretending a YAML shape underneath. It asks the apiserver what to scrape, which is a cluster-wide read grant, and reads the node's /var/log READ-ONLY. The storage path is a write-ahead buffer, not a database: on the pod it survives a container restart and not a reschedule, which for a metrics agent is a gap rather than a lost record. Its HTTP server binds every interface so the kubelet can probe it, and a daemon publishes no Service — adding one publishes the component graph and the values flowing through it.
agent daemon
The Alloy agent, one pod per node. config is Alloy's own configuration language, verbatim; the default logs and watches nothing else. namespace is where the ServiceAccount lives and is required, because a ClusterRoleBinding naming no namespace grants nothing.
Import: import 'github.com/metio/kurly/workloads/alloy/agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "alloy" |
image | string | — | — |
namespace | string | — | "alloy" |
config | string | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
answer
An Apache Answer server (a self-hosted Q&A platform for a community knowledge base, à la Stack Overflow) on the official image. A plain composable http workload — with the SQLite backend its data and uploads live on a PersistentVolume, no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80. Configure external PostgreSQL/MySQL via the installer to scale out.
server http
The Apache Answer server. Data and uploads at /data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/answer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "answer" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
anubis
An Anubis server (it sits in FRONT of another service and makes every unrecognised client solve a proof-of-work challenge before the request is forwarded) on the official image. One instance per protected service — it proxies to exactly one upstream, and the exposure belongs on Anubis, not on the service behind it, since a route still pointing at the backend goes straight past the challenge. Probes read /healthz on the metrics port (:9090): the proxy port answers with the backend or with a challenge page, neither of which says Anubis is healthy. kurly authors no Secret; ED25519_PRIVATE_KEY_HEX signs the challenge passes, and without it every restart invalidates every pass and each replica rejects its siblings' — which is what makes it a prerequisite for more than one replica rather than hardening. Stateless (challenges live in memory), so no volume and any replica count is safe. Serves on :8923.
server http
The Anubis server. target is the service a passing request is forwarded to; cookieDomain is the registrable domain the challenge cookie is set for, never the host. difficulty doubles with every increment and is paid by human visitors too. secretName holds ED25519_PRIVATE_KEY_HEX. Compose an exposure onto THIS workload, not onto the service it protects.
Import: import 'github.com/metio/kurly/workloads/anubis/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "anubis" |
image | string | — | — |
replicas | int | — | 2 |
target | string | — | "http://localhost:3923" |
difficulty | int | — | 4 |
cookieDomain | string | — | — |
cookieSecure | bool | — | true |
serveRobotsTxt | bool | — | true |
secretName | string | — | "anubis" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
apache-airflow
An Apache Airflow instance running the official image in standalone mode: one container holding the API server (UI and REST API on :8080), the scheduler, the DAG processor and the triggerer, with the metadata database external. Pairs with the cnpg-cluster workload; the connection string, the Fernet key and the API JWT secret all come from a consumer-provided Secret read with envFrom, since each carries a credential. LocalExecutor, so tasks run as subprocesses in this pod — Celery and Kubernetes executors need components this stage does not render. AIRFLOW_HOME (config, DAG files, task logs) lives on a ReadWriteOnce volume, which is also how DAGs get in: one replica, recreated.
server http
The Airflow instance, serving the UI and REST API on :8080 and running the scheduler, DAG processor and triggerer in the same container (`airflow standalone`). secretName is read with envFrom, so its keys ARE Airflow settings: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN (the metadata database), AIRFLOW__CORE__FERNET_KEY (encrypts stored connections and variables — rotating it makes every existing one unreadable) and AIRFLOW__API_AUTH__JWT_SECRET (signs UI and task tokens; unset, every restart invalidates them). loadExamples turns the tutorial DAGs on. DAGs go in /opt/airflow/dags on the volume. First start applies the whole migration set before anything listens. kurly authors no Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/apache-airflow/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "apache-airflow" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "apache-airflow" |
loadExamples | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
apache-http-server
An Apache HTTP Server (httpd: static files, rewriting, proxying and authentication through its module system) on the official image. A plain composable http workload: config is httpd's own configuration language, mounted verbatim at /etc/httpd/httpd.conf and named with -f, beside the image's own conf directory rather than over it so mime.types and the extra/ snippets still resolve. The shipped configuration cannot be used as it stands — it listens on :80, which an unprivileged uid cannot bind, and writes the pid file, the scoreboard and both logs under a read-only root filesystem — so the default is a complete minimal configuration with the runtime directory on a scratch at /tmp and both logs on the container's own file descriptors. The served directory is the image's htdocs, so it keeps nothing: stateless, scale freely. Serves on :8080.
server http
The Apache HTTP Server. config is httpd's own configuration, mounted verbatim at /etc/httpd/httpd.conf; the default serves the image's htdocs on the declared port with the runtime directory and the pid file on the scratch at /tmp. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/apache-http-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "apache-http-server" |
image | string | — | — |
port | int | — | 8080 |
documentRoot | string | — | "/usr/local/apache2/htdocs" |
config | string | — | — |
env | object | — | — |
replicas | int | — | 2 |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
apache-solr
An Apache Solr server (the search platform built on Lucene: full-text indexing, faceting, filtering and highlighting over an HTTP/JSON API). A plain composable http workload on the official image whose cores and indexes live on a PersistentVolume, needing nothing external. Solr ships with no authentication until a security.json is installed, so an exposure publishes an admin UI that can create and drop collections — keep it in-cluster or authenticate in front of it. This is the standalone server, not SolrCloud. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the API and admin UI on :8983.
server http
The Apache Solr server. Cores, indexes and logs live under /var/solr on the volume. Runs as the image's own uid 8983, which is what makes the entrypoint skip its root chown path, with fsGroup making the volume writable. Service links are disabled because an injected SOLR_PORT is read by bin/solr as the port to listen on. Compose an exposure onto the HTTP port only if something in front of it authenticates.
Import: import 'github.com/metio/kurly/workloads/apache-solr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "apache-solr" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
apisix
An Apache APISIX gateway (routing, authentication, rate limiting and observability in front of upstream services) in APISIX's STANDALONE mode, where routes come from a YAML file rather than from etcd — no second stateful system to run and back up, at the cost of the admin API, so routes change by rendering again. THE FILE MUST END WITH `#END`: APISIX treats that marker as the end of the configuration and ignores a file without it, leaving a gateway that answers 404 for everything and says nothing useful; the marker is appended by the stage rather than left to a caller. Logs and the generated nginx.conf go to scratch volumes, so the root filesystem stays read-only and replicas scale freely. Serves proxied traffic on :9080.
gateway http
The APISIX data plane in standalone mode. routes are APISIX route definitions, passed through verbatim; objects carries anything else apisix.yaml holds (upstreams, services, plugin_configs, consumers, global_rules); config merges over the rendered config.yaml for APISIX's own runtime settings. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/apisix/gateway.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "apisix" |
image | string | — | — |
replicas | int | — | 2 |
routes | array | — | — |
objects | object | — | — |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
apprise
An Apprise API server (a self-hosted push-notification relay that fans one request out to 100+ services: email, Slack, Telegram, ntfy, webhooks) on the official image. A plain composable http workload that keeps persistent named notification configs on a PersistentVolume under /config; it can also run stateless (POST with inline URLs). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The Apprise server. Named configs at /config on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/apprise/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "apprise" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | {"APPRISE_STATEFUL_MODE":"simple"} |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
archivebox
An ArchiveBox server (self-hosted web archiving: give it URLs, RSS feeds or bookmark exports and it keeps copies as HTML, PDF, screenshots and WARC). A plain composable http workload whose archive, SQLite index and search index share one PersistentVolume — no external database. Its entrypoint requires root to drop to an unprivileged account with gosu, and the image needs a writable root filesystem, so this workload is deliberately less hardened than most here. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI on :8000.
server http
The ArchiveBox server. The archive, its SQLite index and the search index live at /data on the volume. puid/pgid are the account the entrypoint chowns that volume to and runs as, so changing them later means chowning the whole archive. Headless Chrome drives the PDF and screenshot extractors and gets a 256Mi shared-memory scratch. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/archivebox/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "archivebox" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
puid | int | — | 911 |
pgid | int | — | 911 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
artalk
An Artalk server (a self-hosted comment system: a small script on a page posts to this backend, which keeps the threads, moderates them and sends the notifications). A plain composable http workload keeping its SQLite database, its generated configuration, its log and its uploaded images on a PersistentVolume. The first administrator is NOT created for you — Artalk mints it through an interactive CLI, so a fresh instance needs one kubectl exec before anybody can log in. siteUrl is the address readers reach rather than the Service, because the notification mails link back to it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :23366.
server http
The Artalk server. SQLite, the configuration its entrypoint generates on first start, the log and uploaded images all live at /data on the volume; ATK_DB_FILE and ATK_LOG_FILENAME are set absolutely so they do not follow the working directory. secretName holds ATK_APP_KEY, which signs the JWTs users and administrators hold — Artalk writes one into its configuration when unset, so it survives a restart but not a fresh volume. Run `artalk admin` in the pod once to create the first administrator. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/artalk/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "artalk" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
siteUrl | string | — | — |
siteDefault | string | — | "Default Site" |
locale | string | — | "en" |
timezone | string | — | "UTC" |
secretName | string | — | "artalk" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
atheos
An Atheos server (a web-based IDE continued from Codiad) on the maintainer's Apache/PHP image. It keeps no database: users, projects and sessions live in data/, edited files in workspace/, and the installer writes config.php next to the code, so the whole install tree at /var/www/html is the state and lives on a PersistentVolume — seeded from the image by an init container, because a PersistentVolume arrives empty and would hide the application. A fresh instance answers with its install page, so finish the installer before anyone else can reach it. Apache starts as root to bind :80 and drops its workers to www-data, relaxing non-root and read-only rootfs while granting back only the identity and file-access capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the IDE on :80.
server http
The Atheos server. The install tree — application, config.php, data/ and workspace/ — lives at /var/www/html on the volume, seeded from the image on first boot. Finish the web installer once before exposing it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/atheos/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "atheos" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
atomic-server
An AtomicServer instance (a graph database with documents, collections, full-text search and a browser UI, all in one static binary). A plain composable http workload with the store, the uploads and the search index on one PersistentVolume, so it needs nothing external. The port is moved off the image's ATOMIC_PORT=80 to upstream's own 9883, which an ordinary user may bind. serverUrl must be set before anybody writes data: every resource is identified by an absolute URL derived from it, and changing it later leaves the existing resources addressed under a hostname that no longer serves them. The first visitor to /setup becomes the root agent. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9883.
server http
The AtomicServer instance. The store, uploads and search index live at /atomic-storage on the volume. Set serverUrl to the absolute URL the instance is reached at before anybody writes data, because every stored resource is identified by a URL derived from it. Claim /setup right after the first deploy — the first visitor there becomes the root agent. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/atomic-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "atomic-server" |
image | string | — | — |
port | int | — | 9883 |
serverUrl | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
atsumeru
An Atsumeru server (a media server for manga, comics, and light novels) on the official image. A plain composable http workload that keeps its configuration, database, cover cache and logs on a PersistentVolume and reads its library from /library on the same volume — no external database. The JVM writes temp files to the rootfs, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. Probes are connection probes: the server answers /api/server/ping with a 4xx while healthy. The admin password is printed to the log on first start. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :31337.
server http
The Atsumeru server. Configuration at /app/config, database, cache and logs beside it, library at /library, all on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/atsumeru/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "atsumeru" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
audiobookshelf
An Audiobookshelf server (a self-hosted audiobook and podcast server) on the official image. A plain composable http workload that keeps its config, metadata, and library on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Audiobookshelf server. Config at /config, metadata at /metadata, library at /audiobooks, all on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/audiobookshelf/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "audiobookshelf" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
authelia
An Authelia server (a self-hosted authentication and authorization gateway adding SSO and 2FA in front of your apps via a reverse proxy forward-auth) on the official image. Its behaviour is its configuration.yml, mounted as a ConfigMap and passed verbatim; with the default SQLite storage its database lives on a PersistentVolume. The default config is a minimal skeleton that MUST be completed. kurly authors no Secret; the session/storage/JWT secrets come from a provided Secret via envFrom (as AUTHELIA_* env). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9091.
server http
The Authelia server. config is Authelia's own configuration.yml (complete the skeleton); secretName holds the AUTHELIA_* secrets (envFrom). Data at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/authelia/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "authelia" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
config | object | — | — |
users | object | — | — |
secretName | string | — | "authelia" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
authentik
An authentik server (a self-hosted identity provider and SSO: OAuth2, SAML, LDAP, forward-auth) on the official image, backed by an external PostgreSQL and Redis. Two stages: a web/API server (HTTP :9000) and a background worker (migrations, scheduled tasks, outposts). kurly authors no Secret; the PostgreSQL/Redis connection and AUTHENTIK_SECRET_KEY come from a provided Secret via envFrom, shared by both stages. Pairs with a cnpg-cluster named authentik-db and a Redis. Stateless.
server http
The authentik web/API server. secretName holds the PostgreSQL/Redis connection and AUTHENTIK_SECRET_KEY (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/authentik/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "authentik" |
image | string | — | — |
replicas | int | — | 2 |
dbHost | string | — | "authentik-db-rw" |
database | string | — | "authentik" |
dbUser | string | — | "authentik" |
redisHost | string | — | "authentik-cache-headless" |
secretName | string | — | "authentik" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The authentik background worker (migrations, scheduled tasks, outposts). Shares the server's Secret via envFrom. No Service.
Import: import 'github.com/metio/kurly/workloads/authentik/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "authentik-worker" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "authentik-db-rw" |
database | string | — | "authentik" |
dbUser | string | — | "authentik" |
redisHost | string | — | "authentik-cache-headless" |
secretName | string | — | "authentik" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
authorizer
An Authorizer server (authentication: sign-up and sign-in, social logins, multi-factor, and the OAuth2/OIDC endpoints applications point at). A plain composable http workload on the project's own image; every account and session lives in the external database, so it claims no volume. SECRETS REACH IT THROUGH A SHELL, DELIBERATELY: Authorizer v2 takes all configuration as command-line flags and reads no environment variables at all, so writing the admin and JWT secrets into args would put both in the Deployment spec, readable by anything that can read Deployments — the container runs `sh -c` and expands them from the Secret at startup, the pattern upstream documents for the same reason. They remain visible in the process's own argv inside the container; what this buys is keeping them out of the manifest. Replicas scale, but every one must carry the same jwt-secret or a token minted by one is rejected by the next. Serves on :8080.
server http
The Authorizer server. databaseType selects the backend (postgres, mysql, sqlite, mongodb and the rest) and jwtType the signing algorithm; appUrl is the URL a browser reaches this at, which Authorizer builds its redirects from. secretName holds DATABASE_URL, ADMIN_SECRET, JWT_SECRET, CLIENT_ID and CLIENT_SECRET, expanded into the command line at startup. extraArgs is appended verbatim for the flags this stage does not model — not a place for a credential. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/authorizer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "authorizer" |
image | string | — | — |
replicas | int | — | 1 |
databaseType | string | — | "postgres" |
appUrl | string | — | — |
secretName | string | — | "authorizer" |
jwtType | string | — | "HS256" |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
automatisch
An Automatisch server (a self-hosted, open-source business-automation / workflow tool, an open alternative to Zapier) on the official image, backed by an external PostgreSQL and Redis. Two stages: a web server (HTTP :3000) and a background worker that runs the flow executions the server enqueues onto Redis. Pairs with a cnpg-cluster named automatisch-db and a Redis. kurly authors no Secret; the PostgreSQL/Redis connection and the ENCRYPTION_KEY / WEBHOOK_SECRET_KEY / APP_SECRET_KEY come from a provided Secret via envFrom, shared by both stages. Stateless.
server http
The Automatisch web server. secretName holds the PostgreSQL/Redis connection and the ENCRYPTION_KEY / WEBHOOK_SECRET_KEY / APP_SECRET_KEY (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/automatisch/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "automatisch" |
image | string | — | — |
replicas | int | — | 2 |
dbHost | string | — | "automatisch-db-rw" |
database | string | — | "automatisch" |
dbUser | string | — | "automatisch" |
redisHost | string | — | "automatisch-cache-headless" |
secretName | string | — | "automatisch" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The Automatisch background worker (runs the flow executions the server enqueues onto Redis). Shares the server's Secret via envFrom. No Service.
Import: import 'github.com/metio/kurly/workloads/automatisch/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "automatisch-worker" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "automatisch-db-rw" |
database | string | — | "automatisch" |
dbUser | string | — | "automatisch" |
redisHost | string | — | "automatisch-cache-headless" |
secretName | string | — | "automatisch" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
azuracast
An AzuraCast server (a web radio management suite: media library, playlists and schedules, live DJ sessions, and the Icecast/Liquidsoap stack that broadcasts them) on the official all-in-one image, backed by an external MariaDB and an external Redis, with the station media library on a PersistentVolume. The web app and API are on :80, but each station broadcasts on its own raw TCP port from 8000 up, which an HTTP exposure cannot carry — stationPorts decides how many of those the Service publishes. supervisord starts nginx, php-fpm, Liquidsoap and Icecast together and drops privileges from root, so this workload is deliberately less hardened. kurly authors no Secret; MYSQL_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The AzuraCast server. dbHost/dbPort/database/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster); redisHost/redisPort/redisDatabase at a Redis. secretName holds MYSQL_PASSWORD (envFrom). The station media library lives at /var/azuracast/stations on the volume — only that subtree is mounted, since /var/azuracast also holds the application itself. stationPorts publishes broadcast ports from 8000 up on the Service; those are raw TCP streams and need a TCP route, not the HTTP exposure the web app takes. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/azuracast/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "azuracast" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
dbHost | string | — | "azuracast-db" |
dbPort | int | — | 3306 |
database | string | — | "azuracast" |
dbUser | string | — | "azuracast" |
redisHost | string | — | "azuracast-cache" |
redisPort | int | — | 6379 |
redisDatabase | int | — | 1 |
stationPorts | int | — | 2 |
secretName | string | — | "azuracast" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
baikal
A Baikal server (a lightweight CalDAV + CardDAV server on sabre/dav) on the maintained ckulka image. A plain composable http workload that keeps its configuration and SQLite database on a PersistentVolume — no external database by default. The nginx + PHP-FPM image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Baikal server. Keeps its SQLite database at /var/www/baikal/Specific and its generated config at /var/www/baikal/config (both on the volume). Point it at external MySQL/PostgreSQL through the setup wizard to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/baikal/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "baikal" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
barcodebuddy
A Barcode Buddy server (scan a barcode and have the item added to, or consumed from, a Grocy inventory). A composable http workload keeping its settings and learned barcodes in a SQLite database on a PersistentVolume, with everything else living in Grocy. IT IS A FRONT END FOR GROCY AND DOES NOTHING WITHOUT ONE — grocyUrl points at a Grocy API and secretName carries the key it authenticates with; kurly carries grocy, so the pair deploys together, and pointed at nothing every scan fails. THE SCANNER IS SOMEWHERE ELSE: a USB reader attaches to a machine rather than a pod, so in a cluster the working arrangements are the web interface, a phone camera, or the project's own script running beside the scanner and posting to this API. The image starts nginx, php-fpm and a websocket server under supervisor as root, so four defaults are relaxed deliberately. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Barcode Buddy server. grocyUrl is the Grocy API it drives; secretName holds BBUDDY_API_KEY through envFrom. Its database and settings live at /config on the volume.
Import: import 'github.com/metio/kurly/workloads/barcodebuddy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "barcodebuddy" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
grocyUrl | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bazarr
A Bazarr server — a companion to Sonarr and Radarr that manages and downloads subtitles. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6767.
server http
The A Bazarr server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bazarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bazarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
beelzebub
A Beelzebub honeypot (it pretends to be services an attacker wants to find — SSH, HTTP, databases — records what they try, and answers convincingly enough to keep them going). A plain composable http workload on the project's own image: every impersonated service is a YAML file in one ConfigMap, and events go to stdout or an external sink, so it claims no volume. A honeypot is MEANT to be attacked, which makes it the one workload in a namespace to assume is compromised: it runs with the hardened default and belongs in a namespace of its own with a NetworkPolicy that lets it reach nothing inside the cluster. Ports follow the services — each service names its own address and the published ports are derived from them, so there is no list to keep in step by hand. A port below 1024 needs addCapabilities(['NET_BIND_SERVICE']), or an exposure that maps it.
honeypot http
The Beelzebub honeypot. services is one entry per impersonated service, keyed by file name, each a Beelzebub service definition passed through verbatim; the first one's port is the probed port and the rest are published beside it. Beelzebub's global settings come from the BEELZEBUB_* environment overrides through env, because a missing core configuration file is not an error and kurly gives a workload one ConfigMap. Compose an exposure onto whichever ports should be reachable.
Import: import 'github.com/metio/kurly/workloads/beelzebub/honeypot.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "beelzebub" |
image | string | — | — |
replicas | int | — | 1 |
services | object | — | {"http-8080":{"address":":8080","apiVersion":"v1","protocol":"http"}} |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
beszel
A Beszel hub (a lightweight server-monitoring dashboard) on the official image. A plain composable http workload that keeps its data in SQLite on a PersistentVolume — no external database. Beszel agents run on the monitored machines and report to this hub. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8090.
server http
The Beszel hub. Keeps its SQLite data at /beszel_data on the volume. Compose an exposure onto the HTTP port; agents on monitored hosts report to it.
Import: import 'github.com/metio/kurly/workloads/beszel/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "beszel" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bifrost
A Bifrost gateway (one OpenAI-compatible endpoint in front of many model providers, with failover, load balancing and per-key budgets, so an application holds one URL and one key instead of a provider's) on the project's own image. The configuration and the request logs live in a file database on the volume at /app/data. secretName carries the provider API keys the configuration refers to, through envFrom; kurly authors none of them. The gateway requires no client key by default, so reaching it is equivalent to holding every provider key it carries until governance is configured through its interface. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
gateway http
The Bifrost gateway, serving its API and web interface on :8080. secretName holds the provider API keys (envFrom). storageSize sizes the volume at /app/data, which carries the configuration and the request logs. Probes are by connection, because every HTTP route is a gateway route rather than a health endpoint. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bifrost/gateway.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bifrost" |
image | string | — | — |
secretName | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
logLevel | string | — | "info" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bitcart
A Bitcart merchants API server (the backend of the self-hosted cryptocurrency payment processor: stores, invoices, wallets and the admin API) on the official backend image, backed by an external PostgreSQL and Redis. Pairs with a cnpg-cluster named bitcart-db and a valkey named bitcart-cache. Connection settings are env from dbHost/dbName/dbUser/redisUrl; a provided Secret holds DB_PASSWORD via envFrom (kurly authors none). The container upgrades the schema before gunicorn, so the first boot is slow and startup-probed. The entrypoint chowns the datadir as root before dropping to the electrum account with gosu, so root, privilege escalation, capabilities and a writable root filesystem are relaxed. Uploads, plugin trees and backups move onto one ReadWriteOnce volume: one replica, recreated. Serves on :8000. The background worker and the per-coin daemons a full deployment also needs are not carried here.
server http
The Bitcart merchants API, serving the admin/store API on :8000. apiHost/adminHost are the public hosts absolute links are built from; cryptos is BITCART_CRYPTOS and each coin still needs its own daemon. dbHost/dbName/dbUser default to a cnpg-cluster named bitcart-db, redisUrl to a valkey named bitcart-cache; secretName holds DB_PASSWORD (envFrom). The datadir holds uploads, plugin trees and backups on a ReadWriteOnce volume. The API fetches its plugin schema over HTTPS at startup and does not survive that failing. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bitcart/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bitcart" |
image | string | — | — |
apiHost | string | — | "localhost:8000" |
adminHost | string | — | "localhost:3000" |
cryptos | string | — | "btc" |
workers | int | — | 4 |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "bitcart-db-rw" |
dbName | string | — | "bitcart" |
dbUser | string | — | "bitcart" |
redisHost | string | — | "bitcart-cache-headless" |
secretName | string | — | "bitcart" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bitmagnet
A bitmagnet server (a BitTorrent indexer that crawls the DHT, classifies what it finds and serves it as a searchable catalogue with a web UI, a GraphQL API and Torznab endpoints for the Servarr stack). A composable http workload backed by an external PostgreSQL and claiming no volume of its own: everything it collects is in the database, which GROWS for as long as the crawler runs. The DHT crawler speaks to the swarm on :3334, TCP and UDP both, and needs unrestricted egress to arbitrary internet hosts on arbitrary UDP ports — a NetworkPolicy allowing only PostgreSQL leaves the index permanently empty while the pod stays healthy. The image declares no user and needs none, so it runs as an unprivileged uid with a read-only root filesystem. Serves on :3333.
server http
The bitmagnet server, running the http, queue and DHT crawler services in one process. State lives entirely in PostgreSQL. secretName holds POSTGRES_PASSWORD, which upstream publishes as `postgres` in its own compose file. keys selects the services the worker runs — drop dht_crawler to serve an index without adding to it. Set TMDB_API_KEY through env to enrich classification. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bitmagnet/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bitmagnet" |
image | string | — | — |
keys | array | — | ["http_server","queue_server","dht_crawler"] |
dbHost | string | — | "bitmagnet-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "bitmagnet" |
dbUser | string | — | "bitmagnet" |
secretName | string | — | "bitmagnet" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
blackbox-exporter
The Prometheus blackbox_exporter: it probes endpoints from the outside (HTTP, TCP, DNS, ICMP) and turns each probe into metrics. A plain composable http workload, deployed once as the prober kurly.expose.probe points a workload Probe at. Serves /probe on :9115.
server http
The exporter. modules is rendered as its config.yml; the default covers http_2xx (dual-stack) plus IPv4/IPv6-pinned variants and tcp_connect. Reach it at blackbox-exporter:9115 (the kurly.expose.probe default prober). An ICMP module needs CAP_NET_RAW, so relax the dropped capabilities for it.
Import: import 'github.com/metio/kurly/workloads/blackbox-exporter/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "blackbox-exporter" |
image | string | — | — |
replicas | int | — | 1 |
modules | object | — | — |
resources | object | — | {"limits":{"memory":"64Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
blinko
A Blinko server (a self-hosted, AI-powered note-taking app for quickly capturing ideas) on the official image, backed by an external PostgreSQL, with uploads on a PersistentVolume. Pairs with a cnpg-cluster named blinko-db. kurly authors no Secret; DATABASE_URL and NEXTAUTH_SECRET come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :1111.
server http
The Blinko server. nextauthUrl is the public URL. secretName holds DATABASE_URL (with the DB password) and NEXTAUTH_SECRET (envFrom). Uploads at /app/.blinko. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/blinko/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "blinko" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
nextauthUrl | string | — | "http://localhost:1111" |
secretName | string | — | "blinko" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
blocky
A fast, lightweight DNS proxy and ad-blocker for a local network (a Pi-hole alternative with no database and no web console) on the official image; its config.yml is its only state, rendered as a ConfigMap. It answers DNS on :53 (TCP/UDP), separate ports to add a Service for. kurly authors no Secret; nothing in its config is a credential. Stateless, so any replica count is safe. Serves its REST API and metrics on :4000.
server http
The blocky server. upstreams sets the default resolver group blocky needs to start. DNS (:53) needs an extra Service. Compose an exposure onto the API port.
Import: import 'github.com/metio/kurly/workloads/blocky/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "blocky" |
image | string | — | — |
replicas | int | — | 1 |
upstreams | object | — | {"default":["1.1.1.1","8.8.8.8"]} |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bookstack
A BookStack server (a simple, self-hosted platform for organising and storing documentation) on the maintained LinuxServer image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with config and uploads on a PersistentVolume. The LinuxServer s6 image runs as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; DB_PASS and APP_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The BookStack server. dbHost/dbName/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). appUrl is the public URL. secretName holds DB_PASS and APP_KEY (envFrom). Config/uploads at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bookstack/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bookstack" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "bookstack-db" |
dbName | string | — | "bookstack" |
dbUser | string | — | "bookstack" |
appUrl | string | — | — |
secretName | string | — | "bookstack" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bracket
A Bracket API server (a tournament system: clubs, teams, players and courts, and the single-elimination, round-robin and swiss stages that decide them). This is the BACKEND image — it answers the API and serves uploaded logos under /static, and the web frontend is a separate image not carried here. A composable http workload backed by an external PostgreSQL, with the uploaded club and team logos on a PersistentVolume. secretName carries the whole PG_DSN connection string, the JWT_SECRET signing users' tokens, and ADMIN_PASSWORD — the last of which reads optional and is not: an empty database is only ever populated by the branch that creates the administrator account, and with no admin credentials the Alembic run that follows tries to drop an index no table has and the process exits, so a fresh install without them never starts. The first boot migrates the schema and is guarded by a startup probe. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8400.
server http
The Bracket API server. Uploaded tournament and team logos live at /app/static on the volume; everything else is in PostgreSQL. secretName holds PG_DSN, JWT_SECRET and ADMIN_PASSWORD, which together with adminEmail creates the administrator on the first start — a fresh database is only ever populated by that branch, so leaving them out means the instance never starts. baseUrl is the public URL the API is reached at (unset by default, since the right value here is wrong everywhere else) and corsOrigins the browser origins allowed to call it — the frontend's URL, since the application's own default is '*'. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/bracket/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bracket" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
corsOrigins | string | — | — |
adminEmail | string | — | "admin@example.com" |
secretName | string | — | "bracket" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
budget-board
A Budget Board deployment (personal budgeting: monthly spending against a budget, net worth over time, and progress towards savings goals, with optional automatic account synchronisation). Two composable stages, because upstream publishes two images — server (the ASP.NET API on :8080) and client (the web bundle served by nginx on :6253, which proxies /api/ to the server). The browser talks to the CLIENT, so the exposure goes there, and the server takes that same public origin as clientAddress: it is the sole CORS origin, the API refuses to start without one, and a wrong value leaves a web app that loads and cannot call anything. All state is in an external PostgreSQL, which the server migrates as it starts, so neither stage claims a volume and both can run several replicas. The API is probed by connection: it serves no health endpoint and every route is either the identity API or behind authentication. Its data-protection keys live in an emptyDir, so a restart signs out whoever is logged in.
client http
The Budget Board web interface: a static bundle served by nginx on :6253, which also proxies /api/ to the server stage. serverHost is the in-cluster host of that stage, reached on :8080 as the image's own template hard-codes. The OIDC and authentication switches mirror the server's so the login screen matches what the API accepts. nginx substitutes the environment into its site config and into the bundle's settings file inside the image tree, so this stage runs with a writable root filesystem. Compose the exposure onto this stage, and give the server the same address as clientAddress.
Import: import 'github.com/metio/kurly/workloads/budget-board/client.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "budget-board-client" |
image | string | — | — |
replicas | int | — | 1 |
serverHost | string | — | "budget-board-server" |
port | int | — | 6253 |
oidcEnabled | bool | — | false |
oidcIssuer | string | — | "" |
oidcClientId | string | — | "" |
disableLocalAuth | bool | — | false |
disableNewUsers | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
server http
The Budget Board API, serving on :8080. All state is in PostgreSQL and the application applies its own migrations on start. dbHost/database/dbUser default to a cnpg-cluster named budget-board-db; secretName holds POSTGRES_PASSWORD, which ships with a published default in the project's own compose file. clientAddress is the browser-visible origin of the client stage and is sent verbatim as the allowed CORS origin, so it is a URL with a scheme. Expose the client rather than this.
Import: import 'github.com/metio/kurly/workloads/budget-board/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "budget-board-server" |
image | string | — | — |
replicas | int | — | 1 |
clientAddress | string | — | "http://budget-board-client:6253" |
dbHost | string | — | "budget-board-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "budgetboard" |
dbUser | string | — | "budgetboard" |
secretName | string | — | "budget-board" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
bunkerm
A BunkerM server (Eclipse Mosquitto and a web dashboard for its clients, ACLs and dynamic-security roles in one image, so the broker is administered without editing configuration files by hand) on the project's own image. TWO PORTS, ONLY ONE OF THEM HTTP: :2000 serves the dashboard and its API and is the Service's http port, while :1900 is the MQTT listener, a raw TCP protocol needing a TCP route rather than an HTTP exposure — publishing the dashboard does not publish the broker. The dashboard authenticates against an API key kept at /nextjs/data/.api_key: supplied through a Secret it is used and persisted, left unset the entrypoint generates a random one on first boot, which the deployment then does not know. The entrypoint chowns its password file and log directories and supervisord runs mosquitto, nginx and the API under their own users, so root, privilege escalation, capabilities and a writable root filesystem are relaxed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :2000.
server http
The BunkerM broker and dashboard. secretName carries API_KEY (envFrom); without it the first start generates one onto the volume. storageSize sizes the volume holding the broker persistence, the password file and that key. The MQTT listener is published as the extra port mqtt and needs a TCP route of its own. Compose an exposure onto the HTTP port for the dashboard.
Import: import 'github.com/metio/kurly/workloads/bunkerm/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "bunkerm" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
caddy
A Caddy server (a web server, static file server and reverse proxy with a config API) on the official image. Its whole behaviour is its Caddyfile, mounted as a ConfigMap and passed verbatim; the default serves the static site the image ships. The site address is a bare port, so it serves plain HTTP and asks for no certificate — TLS belongs to the exposure composed onto it, which is why /data and /config are scratch volumes. A Caddyfile naming a public hostname wants a store at /data instead, so issued certificates survive a restart. Stateless: scale freely. Serves on :8080.
server http
The Caddy server. caddyfile is Caddy's own configuration, mounted verbatim at /etc/caddy/Caddyfile; the default serves the image's static site on the declared port. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/caddy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "caddy" |
image | string | — | — |
replicas | int | — | 2 |
caddyfile | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cadvisor
A cAdvisor agent (per-container resource usage and performance metrics, read off the node's cgroups and exported for Prometheus). A composable daemon workload. THE KUBELET ALREADY EMBEDS IT — every Kubernetes node exposes cAdvisor's metrics at /metrics/cadvisor, and a cluster scraping those needs nothing here; this stage is for a node whose kubelet metrics are off, a machine outside the cluster, or a deployment that wants cAdvisor's own UI and the detail the kubelet's endpoint trims. What it reads is the node's own state — the root filesystem, /sys, /var/run and, with diskNames, /dev/disk — all read-only, and no capability beyond that is asked for: the paths are the privilege. A container runtime's data directory is NOT mounted by default, because its path differs per runtime and distribution, so runtimeDataDir names it rather than guessing wrong and reporting nothing. diskNames is off by default because /dev/disk is populated by udev, and a node without it cannot start the pod at all. Serves the UI and /metrics on :8080.
agent daemon
The cAdvisor agent, one pod per node. runtimeDataDir mounts the container runtime data directory for image and layer sizes (/var/lib/docker, /var/lib/containerd, or wherever the distribution puts it). extraArgs is appended to cAdvisor's own flags. Compose kurly.serviceMonitor() to have Prometheus scrape it.
Import: import 'github.com/metio/kurly/workloads/cadvisor/agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cadvisor" |
image | string | — | — |
runtimeDataDir | string | — | — |
diskNames | bool | — | false |
extraArgs | array | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"150m","memory":"200Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
calagopus
A Calagopus panel (a game server management panel written in Rust: it creates game servers on wings nodes, hands their owners a console, files and backups, and keeps the accounts and permissions around them). A plain composable http workload on the official single-binary image, backed by an external PostgreSQL and an external Redis/valkey, with the panel's data directory on a PersistentVolume. It is only half a deployment: the servers themselves run on wings daemons, which want a host with Docker and are therefore not carried here. secretName holds DATABASE_URL, REDIS_URL and APP_ENCRYPTION_KEY — the encryption key protects the node and server credentials stored in the database and ships upstream as the literal CHANGEME, and changing it later does not re-encrypt what is already stored. The panel reaches the wings nodes it manages over the network, so a NetworkPolicy allowing only the database and the cache leaves every server unreachable. Runs as an unprivileged uid on a read-only root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The Calagopus panel, serving the web interface and its API on :8000. The data directory lives at /var/lib/calagopus on the volume and the logs on a scratch volume; everything else is in PostgreSQL and the cache. primary sets APP_PRIMARY, which decides whether this instance runs the background cleanups and schedules — exactly one instance may have it. migrate sets DATABASE_MIGRATE, so the first boot against a fresh database is slower than the ones after it and is start-up probed separately. secretName holds DATABASE_URL, REDIS_URL and APP_ENCRYPTION_KEY. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/calagopus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "calagopus" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
primary | bool | — | true |
migrate | bool | — | true |
secretName | string | — | "calagopus" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
calibre
A Calibre server (the full Calibre e-book management desktop app, served in the browser over a remote-desktop session) on the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Calibre server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/calibre/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "calibre" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
calibre-web
A Calibre-Web server (a clean web interface for browsing, reading and downloading books from an existing Calibre library) on the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8083.
server http
The Calibre-Web server. puid/pgid own the mounted files; timezone sets TZ. Config at /config; point it at an existing Calibre library on first run. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/calibre-web/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "calibre-web" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
calibre-web-automated
A Calibre-Web Automated server (a self-hosted web reader and library manager for a Calibre ebook library, adding automatic ingest and format conversion on top of Calibre-Web) on the LinuxServer.io-based image. A plain composable http workload that keeps its application config on one PersistentVolume under /config and the Calibre library on another under /calibre-library. The s6-overlay init runs as root and drops to PUID/PGID, so this runs as root with a writable root filesystem while keeping the rest of the hardening. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :8083.
server http
The Calibre-Web Automated server. Config at /config, library at /calibre-library. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/calibre-web-automated/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "calibre-web-automated" |
image | string | — | — |
configSize | quantity | — | "1Gi" |
librarySize | quantity | — | "20Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cannery
A Cannery server (an inventory for firearms, ammunition and range use) on the official image. A plain composable http workload on the Phoenix release, backed by an external PostgreSQL — all state is in the database, so it claims no volume. The release migrates on start, so a first boot against a fresh database is slower than the ones after it, which is what the startup budget covers. Mail is not optional: the production configuration refuses to start without SMTP_HOST, SMTP_USERNAME and SMTP_PASSWORD, because users are invited and addresses confirmed by email. host is the public domain links and invite mails are built from and defaults to localhost, which boots and is wrong for every real deployment. Registration is invite-only by default. kurly authors no Secret; SECRET_KEY_BASE, DATABASE_URL and the SMTP credentials come from a provided Secret via envFrom. Probed by connection, because every page redirects an unauthenticated visitor to the sign-in form. One replica: without libcluster the Phoenix PubSub is per node. Serves on :4000.
server http
The Cannery server. All state is in PostgreSQL and the release migrates it on start. secretName holds SECRET_KEY_BASE, which signs the session cookie — a value that changes on every restart signs everybody out — plus DATABASE_URL and the SMTP credentials. host is the public domain the app builds its links and invite mails from; smtpHost/smtpPort/smtpSsl name the relay, whose credentials stay in the Secret. registration chooses between invite and public sign-up. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cannery/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cannery" |
image | string | — | — |
host | hostname | — | "localhost" |
secretName | string | — | "cannery" |
smtpHost | hostname | — | "localhost" |
smtpPort | string | — | "587" |
smtpSsl | bool | — | false |
emailFrom | string | — | — |
emailName | string | — | "Cannery" |
registration | string | — | "invite" |
locale | string | — | "en_US" |
poolSize | string | — | "10" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cassandra-cluster
A highly-available Apache Cassandra cluster as a cass-operator CassandraDatacenter custom resource. Cassandra is Apache-2.0 — a clean default for a platform that monetizes hosting. Authors the CR directly like cnpg-cluster; composed by parameter, not by + feature. Requires cass-operator; the operator mints the superuser Secret.
cluster cassandra
The CassandraDatacenter CR. name is the datacenter name; clusterName defaults to it. size is the node count. config is extra cassandra.yaml/JVM tuning (cass-operator schema, verbatim). Render with kurly.list.
Import: import 'github.com/metio/kurly/workloads/cassandra-cluster/cluster.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cassandra" |
clusterName | string | — | — |
size | int | — | 3 |
serverVersion | string | — | "5.0.8" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"1","memory":"2Gi"}} |
config | object | — | — |
labels | object | — | — |
annotations | object | — | — |
centrifugo
A Centrifugo server (a real-time messaging server: clients subscribe over WebSocket, SSE or HTTP-streaming and a backend publishes to them over an HTTP/GRPC API). A plain composable http workload and a STATELESS one: channel history and presence live in memory or in Redis, never on disk, so it claims no PersistentVolume. One replica by default, which is a correctness bound rather than caution — without a broker each instance knows only its own subscribers. Point it at Redis to scale horizontally. Serves clients and the API on :8000.
server http
The Centrifugo server. Stateless, so it claims no volume. secretName holds the token key, the HTTP API key and any admin credentials — everything here authenticates somebody and kurly mints none of it. admin turns on the administrative web UI, which is off by default because it answers on the same port as the client traffic an exposure publishes. Compose an exposure that does not cut long-lived connections; WebSocket and SSE are the point.
Import: import 'github.com/metio/kurly/workloads/centrifugo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "centrifugo" |
image | string | — | — |
secretName | string | — | "centrifugo" |
admin | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
changedetection
A changedetection.io server (self-hosted website change detection: watch pages and get notified when they change) on the official image; its datastore (SQLite plus page snapshots) lives on a PersistentVolume. Fetching JavaScript pages needs a companion Playwright/Chrome service (PLAYWRIGHT_DRIVER_URL); the plain HTTP fetcher works without it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5000.
server http
The changedetection.io server. baseUrl is the public URL (notification links derive from it). Datastore at /datastore. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/changedetection/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "changedetection" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
chatpad
A Chatpad AI server (a self-hosted, clean web UI for OpenAI chat models) on the official image. A plain composable http workload. Conversations and the API key are stored client-side, so the server only serves static assets and holds no data — a plain, horizontally scalable Deployment. The browser talks to OpenAI directly with the user own key. Serves on :80.
server http
The Chatpad server. Stateless; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/chatpad/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "chatpad" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
chhoto-url
A Chhoto URL server (a small URL shortener with a web interface and an HTTP API) on the official image, keeping its SQLite database on a PersistentVolume. The image is FROM scratch around one static musl binary — no shell, no entrypoint dropping privileges, nothing written outside the volume — so it keeps the fully restricted posture unrelaxed. CHHOTO_PASSWORD is the only thing guarding the admin interface and an unset one leaves it open, so the Secret is not optional in any deployment somebody can reach. Service links are switched off: the Service is named after the workload, and a process that reads every one of its settings from a CHHOTO_ variable would be handed CHHOTO_URL_PORT as a tcp:// URL. The probes ask for a connection rather than a page, since both the interface and the API answer with authentication. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4567.
server http
The Chhoto URL server. SQLite at /data/urls.sqlite on the volume, in WAL mode. secretName holds CHHOTO_PASSWORD (the admin password) and, where the CLI is used, CHHOTO_API_KEY. siteUrl sets CHHOTO_SITE_URL, the public URL the shortened links are built from — without it the frontend shows them against whatever host the browser used. env merges over the defaults CHHOTO_DB_URL, CHHOTO_LISTEN_PORT and CHHOTO_SQLITE_USE_WAL_MODE, so a key you set wins, and every other setting (slug style and length, redirect method, public mode, extra protocols) is an env var too; moving the database off /data means changing the store path with it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/chhoto-url/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "chhoto-url" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "chhoto-url" |
siteUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
chibisafe
A chibisafe install (a file uploader with drag-and-drop, albums, tags and shareable links) on the official images, as the three stages the application really is: server (the fastify backend, the SQLite database and the uploaded files), frontend (the Next.js application a browser renders) and proxy (the Caddy edge that joins them into one origin). The proxy is not a convenience: in a released image the backend registers no static file route, so the links chibisafe hands out are answered by the proxy or by nothing — upstream's own compose file puts Caddy in front for the same reason, and the Caddyfile here is that one restated over the stage names, since upstream's are the literal hosts chibisafe and chibisafe_server. The server owns two volumes, /app/uploads and /app/database, and the proxy mounts the uploads read-only, so a ReadWriteOnce class keeps both pods on one node. The server's start script runs prisma migrate deploy and prisma generate, which write into the image's own node_modules, so that stage runs as root with a writable root filesystem; the other two keep the hardened default. No external database, cache or broker. Single writer: the server is one replica, recreated. Expose the proxy, on :8080.
frontend http
The Next.js application on :8001, stateless and holding no configuration beyond apiUrl — the in-cluster address it renders pages against, defaulting to the sibling server stage. Do not expose it directly: the application alone is not a working install, since /api, /docs and the uploaded files have to come from the same origin.
Import: import 'github.com/metio/kurly/workloads/chibisafe/frontend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "chibisafe" |
name | string | — | — |
image | string | — | — |
replicas | int | — | 2 |
apiUrl | string | — | — |
serverPort | int | — | 8000 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
proxy http
The Caddy edge on :8080, and the stage you expose: it serves the uploaded files off the server volume, forwards /api and /docs to the server, and hands everything else to the frontend. Its Caddyfile is rendered from serverHost/frontendHost (defaulting to the sibling stages) and uploadsPath, so the workload follows namePrefix. storageClaim is the server claim it mounts read-only; without that mount nothing serves the uploads, so it is not optional.
Import: import 'github.com/metio/kurly/workloads/chibisafe/proxy.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "chibisafe" |
name | string | — | — |
image | string | — | — |
replicas | int | — | 2 |
serverHost | string | — | — |
serverPort | int | — | 8000 |
frontendHost | string | — | — |
frontendPort | int | — | 8001 |
storageClaim | string | — | — |
uploadsPath | string | — | "/app/uploads" |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
server http
The chibisafe backend on :8000: the upload API, the album and link bookkeeping, the thumbnailer and the SQLite database behind them. Two volumes — the uploaded files at /app/uploads (the first store, whose claim the proxy mounts) and the database at /app/database. secretName is the Secret it reads ADMIN_PASSWORD from, the password of the admin account created on first start; kurly authors none. Do not expose it; expose the proxy.
Import: import 'github.com/metio/kurly/workloads/chibisafe/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "chibisafe" |
name | string | — | — |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
databaseSize | quantity | — | "2Gi" |
databaseStorageClass | string | — | — |
secretName | string | — | "chibisafe" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
chronoframe
A ChronoFrame server (a personal photo gallery browsing your own pictures by map, timeline and EXIF). A plain composable http workload keeping its SQLite database, and by default the photos themselves, on a PersistentVolume. The image ships with the S3 storage provider selected, which cannot work until somebody supplies a bucket, so this stage selects the local provider instead and a default render needs no external dependency; env moves the photos to S3. Map tiles, reverse geocoding and OAuth are all fetched over the internet at runtime, so a NetworkPolicy that forgets egress leaves the map blank while everything else keeps working. Without CFRAME_ADMIN_PASSWORD the image creates its administrator with a password published in its own documentation. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The ChronoFrame server. SQLite at /app/data/app.sqlite3 and, with the default local storage provider, photos under /app/data/storage on the volume. adminEmail names the account created on first start; secretName holds NUXT_SESSION_PASSWORD (32 characters, encrypting the session cookie), NUXT_OG_IMAGE_SECRET and CFRAME_ADMIN_PASSWORD, plus the S3 credentials if the photos are moved off. env merges over the storage-provider, database and listener defaults, so a key you set wins. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/chronoframe/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "chronoframe" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
adminEmail | string | — | "admin@chronoframe.com" |
secretName | string | — | "chronoframe" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
citadel
A Citadel server (groupware: mail, calendars, address books, forums and instant messaging, reached through its own web interface or through the standard mail and chat protocols). A composable http workload with everything it keeps — the database, the message store, the configuration and the TLS material — in one directory on a PersistentVolume, so it needs no external database. ctdlvisor supervises citserver and webcit, binds the privileged mail and web ports and drops to Citadel's own account itself, so this workload is deliberately less hardened. A mail server also needs a stable public address and matching DNS, which the workload cannot arrange. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web interface on :80 with the protocol ports beside it.
server http
The Citadel server. The database, message store, configuration and TLS material all live at /citadel-data on the volume. Beside the web interface on :80 it serves HTTPS, SMTP (25/465/587), IMAP (143/993), POP3 (110/995), XMPP (5222) and the Citadel protocol (504) — compose an exposure onto the HTTP port and route the protocol ports as TCP. Probes are by connection, because the web interface redirects anonymous callers into its login flow.
Import: import 'github.com/metio/kurly/workloads/citadel/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "citadel" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cloudbeaver
A CloudBeaver Community server (the browser database console from the DBeaver project). A plain composable http workload whose entire workspace — configuration, saved connections, users and query history — lives on a PersistentVolume, so it needs no database of its own; the servers it connects to are chosen by an administrator at runtime. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the console on :8978.
server http
The CloudBeaver server. Keeps its whole workspace at /opt/cloudbeaver/workspace on the volume. Runs as the uid 8978 account the image provisions but never selects, so the hardened posture holds. JAVA_OPTS moves Eclipse's writable configuration area onto that volume, keeping the install tree read-only — override it only by appending, or the server will not start. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cloudbeaver/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cloudbeaver" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cloudflare-tunnel-ingress-controller
A Cloudflare Tunnel ingress controller (it watches Ingresses of its class, programs the tunnel routes and DNS records to match, and the tunnel dials OUT to Cloudflare, so nothing in the cluster has to be reachable from the internet) on the project's own image. A worker: it serves no traffic itself and renders no Service. Ingresses exist in every namespace, so reading them and writing their status is a ClusterRole — it creates no CustomResource and owns none, the interface being the standard Ingress with this controller's class. accountId must be set: empty renders a controller that exits on its first reconcile, which is deliberate, since an account id is a fact about the deployment and there is no value to guess. The API token needs Zone:Read, DNS:Edit and Tunnel:Edit on the account, so whatever holds it can repoint the zone; it comes from a Secret as CLOUDFLARE_API_TOKEN and reaches the flag through $(CLOUDFLARE_API_TOKEN), which Kubernetes expands from the container's own environment, so the token is never written into the manifest. Stateless: the tunnel configuration lives in the Cloudflare account.
controller worker
The controller. accountId names the Cloudflare account and must be set — empty omits the flag and the controller exits; tunnelName is created when it does not exist; ingressClass is the class this controller answers for and controllerClass the value it matches on an IngressClass. namespace must be the namespace it is deployed into, because the ClusterRoleBinding's subject needs one. secretName holds CLOUDFLARE_API_TOKEN (envFrom).
Import: import 'github.com/metio/kurly/workloads/cloudflare-tunnel-ingress-controller/controller.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cloudflare-tunnel-ingress-controller" |
image | string | — | — |
namespace | string | — | "cloudflare-tunnel" |
accountId | string | — | "" |
tunnelName | string | — | "kubernetes" |
ingressClass | string | — | "cloudflare-tunnel" |
controllerClass | string | — | "strrl.dev/cloudflare-tunnel-ingress-controller" |
secretName | string | — | "cloudflare-tunnel-ingress-controller" |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cloudreve
A Cloudreve server (self-hosted file storage and sharing with a web UI, backed by local disk or any S3-style object storage you point it at). A plain composable http workload whose SQLite database, configuration and — by default — stored files share one PersistentVolume, so it needs nothing external. Runs unprivileged despite an image that starts as root, because it needs nothing root gives it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :5212.
server http
The Cloudreve server. Database, configuration and stored files live at /cloudreve/data on the volume; point it at PostgreSQL and object storage from its own settings UI to move both off. The first start prints an administrator password to the log — read it there. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cloudreve/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cloudreve" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cnpg-cluster
A highly-available PostgreSQL cluster as a CloudNativePG Cluster custom resource (three instances, a bootstrapped database, a PodMonitor). Requires the CloudNativePG operator.
cluster cnpg
The PostgreSQL Cluster CR. Adapt it with the parameters and render with kurly.list — composed by parameter, not by + feature (it is a custom resource, not a base kind). Point it at a cnpg-image-catalog with catalog/major to keep the image choice in one place, or pin imageName directly; the two are mutually exclusive.
Import: import 'github.com/metio/kurly/workloads/cnpg-cluster/cluster.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postgres" |
instances | int | — | 3 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
walSize | quantity | — | — |
walStorageClass | string | — | — |
imageName | string | — | — |
catalog | string | — | — |
catalogScope | string | — | "namespaced" |
major | int | — | — |
database | string | — | "app" |
owner | string | — | "app" |
parameters | object | — | — |
resources | object | — | — |
enablePodMonitor | bool | — | true |
imagePullSecrets | array | — | — |
serviceAccountAnnotations | object | — | — |
labels | object | — | — |
annotations | object | — | — |
affinity | object | — | — |
topologySpreadConstraints | array | — | — |
priorityClassName | string | — | — |
schedulerName | string | — | — |
backup | object | — | — |
cnpg-image-catalog
The PostgreSQL images a fleet of CloudNativePG clusters may run, as an ImageCatalog or ClusterImageCatalog custom resource — one image per major, so a patch bump is one line and rolls every cluster on that major. Requires the CloudNativePG operator.
cluster cnpg
A cluster-scoped ClusterImageCatalog, serving every namespace from one object. Identical spec to the namespaced stage; a cnpg-cluster points at it with catalogScope=cluster.
Import: import 'github.com/metio/kurly/workloads/cnpg-image-catalog/cluster.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postgres" |
images | object | — | {"17":"ghcr.io/cloudnative-pg/postgresql:17.2"} |
componentImages | object | — | — |
labels | object | — | — |
annotations | object | — | — |
namespaced cnpg
A namespaced ImageCatalog, owned by the team that owns the databases. Keys of `images` are PostgreSQL major versions; a cnpg-cluster pins one with catalog/major and the catalog owns the patch.
Import: import 'github.com/metio/kurly/workloads/cnpg-image-catalog/namespaced.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postgres" |
images | object | — | {"17":"ghcr.io/cloudnative-pg/postgresql:17.2"} |
componentImages | object | — | — |
labels | object | — | — |
annotations | object | — | — |
cobalt
A cobalt API server (a self-hosted media-downloader backend: give it a link and it returns a clean download for supported sites) on the official image. A plain composable http workload. The API is stateless — it streams and re-muxes on demand and keeps nothing — so a plain, horizontally scalable Deployment. This is the API only; run a cobalt web frontend separately for the UI. Serves on :9000.
server http
The cobalt API server. Set apiUrl to its public URL. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cobalt/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cobalt" |
image | string | — | — |
replicas | int | — | 2 |
apiUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
code-server
A code-server instance (VS Code running in the browser, on a remote server) on the official image. A plain composable http workload — your projects, extensions, and settings live on a PersistentVolume. kurly authors no Secret; PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The code-server editor. Workspace, extensions, and settings at /home/coder on the volume. secretName holds PASSWORD (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/code-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "code-server" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "code-server" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
coder
A Coder server (it provisions remote development workspaces from Terraform templates and hands developers a browser IDE, SSH and port forwarding into them). A plain composable http workload on the official image, backed by an external PostgreSQL and keeping no state of its own — the provisioner cache is a scratch volume, so no PersistentVolume and nothing pinning the pod to a node. Two things it will not tell you about: workspace agents dial back in on CODER_ACCESS_URL, so leaving it unset starts the server on localhost and every workspace it then creates comes up and never connects; and a template that creates Kubernetes workspaces needs RBAC over the namespace those pods land in, which belongs to the template rather than to this deployment. secretName holds CODER_PG_CONNECTION_URL. Runs as an unprivileged uid on a read-only root filesystem. Coordinating more than one replica is a licensed feature upstream, hence one by default. Serves on :8080.
server http
The Coder server, serving the web interface, the API and the workspace agent endpoints on :8080. accessUrl sets CODER_ACCESS_URL — the address agents and browsers reach, never the in-cluster Service name. The provisioner unpacks Terraform providers into a scratch volume at /home/coder/.cache; everything durable is in PostgreSQL. Every setting is read from a CODER_-prefixed variable, so service links are disabled: a Service named coder would otherwise inject CODER_PORT as a tcp:// URL the server reads as its own address. secretName holds CODER_PG_CONNECTION_URL. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/coder/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "coder" |
image | string | — | — |
accessUrl | string | — | — |
port | int | — | 8080 |
secretName | string | — | "coder" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
codex-docs
A CodeX Docs server (a documentation site written in the browser on the Editor.js block editor). A plain composable http workload on the project's own image: pages go to a local file database and uploads to a directory beside it, each on its own PersistentVolume, so no external database is needed. THE CONFIGURATION FILE IS MANDATORY AND THE IMAGE SHIPS NONE — the loader opens docs-config.yaml before merging its built-in defaults, so a pod that mounts nothing exits with ENOENT rather than running on defaults; kurly always renders one carrying the site settings. Authentication is not in it, and until it is supplied the editor is open on the password printed in the upstream source: secretName names a Secret with a docs-config.yaml key holding the WHOLE file, mounted instead of the rendered one, because splitting the settings from the credentials would mean parsing and reassembling somebody's configuration. Anybody who knows that password can edit every page: there are no user accounts, no roles and no audit, which suits a small team's handbook and does not suit a public wiki. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :3000.
server http
The CodeX Docs server. Two claims, because the file database at db/ and the uploads at uploads/ are siblings inside the read-only image tree rather than one directory — storageSize sizes the first and uploadsSize the second. title and description set the site settings in the rendered configuration; secretName replaces that file entirely with one holding the credentials too. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/codex-docs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "codex-docs" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
uploadsSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | — |
title | string | — | "CodeX Docs" |
description | string | — | "Documentation powered by Editor.js" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
collabora-online
A Collabora Online server — the editing engine behind a self-hosted office suite, rendering and editing documents for a file application that embeds it over WOPI. A plain composable http workload: the documents live in the application it serves, so it claims no volume. IT IS HALF AN APPLICATION — on its own it offers an admin console and a discovery endpoint and nothing a user wants; Nextcloud, ownCloud or Seafile is where a deployment starts. wopiHosts ARE REGULAR EXPRESSIONS and the dots matter: an unescaped dot matches any character, so `files.example.com` also admits `filesXexample.com`. Empty admits nobody, which is the safe default and not a working deployment. Each document is edited in a forked, chrooted process, so memory scales with documents open rather than users — and building that sandbox is a mount-namespace operation, which is why SYS_ADMIN and MKNOD are granted BY NAME rather than by relaxing the whole posture. Serves on :9980.
server http
The Collabora Online engine. wopiHosts is the allow-list of hosts permitted to embed it, matched as regular expressions — escape the dots. serverName is the public URL it builds links against; secretName holds the admin console credentials through envFrom; extraParams passes further coolwsd settings. The sandbox roots and caches are scratch volumes, so the root filesystem stays read-only. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/collabora-online/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "collabora-online" |
image | string | — | — |
replicas | int | — | 1 |
wopiHosts | array | — | — |
serverName | string | — | — |
secretName | string | — | — |
sandbox | bool | — | false |
extraParams | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
commafeed
A CommaFeed server (a self-hosted Google Reader-style RSS/Atom feed reader) on the official image. A plain composable http workload — the H2 variant keeps its feeds in an embedded database on a PersistentVolume, no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8082. Use the PostgreSQL image variant to scale past the embedded DB.
server http
The CommaFeed server (H2 variant). Keeps its embedded database at /commafeed/data on the volume. Switch to the -postgresql image and point CF_APP_DATABASE at a cnpg-cluster to scale out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/commafeed/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "commafeed" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
configarr
A Configarr run (it reconciles the quality profiles, custom formats and naming settings of Sonarr/Radarr-style applications against TRaSH Guides and a configuration kept in Git). A composable cron workload, because that is what it is: it runs, reconciles and exits. IT WRITES TO THE APPLICATIONS IT POINTS AT — every run replaces the profiles and formats it manages with what the configuration says, so a hand-made change in the web UI is undone at the next run rather than merged; the configuration is the source of truth or Configarr should not be pointed at that instance. API keys do not belong in the ConfigMap: `!secret NAME` is resolved from a secrets.yml mounted from a Secret. The guides repository is cloned on each run, so the pod needs egress to GitHub.
sync cron
The Configarr reconcile run. schedule is the cron cadence; services is one entry per managed application, keyed by kind (sonarr, radarr, …), passed through verbatim; config merges over the rendered config.yml. secretName holds a secrets.yml key, mounted where Configarr resolves `!secret NAME` against it. The guides clone goes to a scratch volume, so the root filesystem stays read-only.
Import: import 'github.com/metio/kurly/workloads/configarr/sync.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "configarr" |
image | string | — | — |
schedule | string | — | "0 4 * * *" |
services | object | — | — |
config | object | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
continuwuity
A Continuwuity server (a Matrix homeserver written in Rust, the community-driven continuation of conduwuit and Conduit) on the official image; its embedded RocksDB lives on a PersistentVolume. Settings come from the environment under the CONDUWUIT_ prefix the fork inherited, and CONDUWUIT_CONFIG is set empty because the binary insists on a config path. The serverName is baked into every user and room id at first start and cannot be changed. Service links are off: the injected CONTINUWUITY_PORT is a tcp:// URL the binary would read as its listen port. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8008.
server http
The Continuwuity Matrix homeserver. serverName is permanent (baked into ids); allowRegistration toggles open sign-up. Data at /var/lib/conduwuit. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/continuwuity/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "continuwuity" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
serverName | string | — | — |
allowRegistration | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
convertx
A ConvertX server (a self-hosted online file converter supporting 1000+ formats) on the official image; its SQLite database and in-flight files live on a PersistentVolume. kurly authors no Secret; JWT_SECRET comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The ConvertX server. Set JWT_SECRET via env/envFromSecret. Data at /app/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/convertx/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "convertx" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
coroot
A Coroot deployment (an observability platform that builds a service map from eBPF telemetry and turns it into answers about latency, errors and cost, with no application instrumentation): a server stage and a node-agent stage. THE SERVER SEES NOTHING ON ITS OWN — every measurement comes from the agent, one per node, and a Coroot with no agents starts, serves an empty service map and reports no error worth noticing. THE AGENT IS PRIVILEGED AND THERE IS NO SMALLER VERSION: it attaches eBPF programs tracing every container's syscalls and network activity, shares the node's PID namespace to resolve a process to its container, and reads the cgroup hierarchy to attribute what it sees — upstream's own manifest is privileged with hostPID and this stage matches it rather than inventing a weaker set that would silently measure less. Kernel 5.1 or newer. Two dependencies with different jobs: Prometheus holds the metrics, ClickHouse the traces, logs and profiles; kurly carries neither, and ClickHouse is not in this catalogue because its authors sell hosting for it. Serves on :8080.
node-agent daemon
The eBPF agent, one pod per node — privileged, with hostPID and the cgroup, tracing and debug filesystems mounted, matching upstream's own manifest. collectorEndpoint ships what it collects to a collector; left unset it only exposes metrics on its port for a Prometheus to scrape. A privileged agent on every node belongs in a namespace only cluster operators can write to.
Import: import 'github.com/metio/kurly/workloads/coroot/node-agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "coroot-node-agent" |
image | string | — | — |
port | int | — | 80 |
collectorEndpoint | string | — | — |
extraArgs | array | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"200Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
server http
The Coroot web application. prometheusUrl and clickhouseAddress are what it bootstraps its configuration against on first start; secretName holds BOOTSTRAP_CLICKHOUSE_PASSWORD. Its own configuration and cache live at /data on the volume. extraArgs is appended to its flags. Compose an exposure onto the HTTP port, and deploy the node-agent stage beside it.
Import: import 'github.com/metio/kurly/workloads/coroot/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "coroot" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
prometheusUrl | string | — | — |
clickhouseAddress | string | — | — |
clickhouseUser | string | — | "default" |
clickhouseDatabase | string | — | "default" |
secretName | string | — | "coroot" |
refreshInterval | string | — | "15s" |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
couchdb
An Apache CouchDB server (a self-hosted, document-oriented NoSQL database that speaks HTTP/JSON and syncs with offline-first apps) on the official image; its data lives on a PersistentVolume. kurly authors no Secret; COUCHDB_USER and COUCHDB_PASSWORD come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5984.
server http
The CouchDB server. secretName holds COUCHDB_USER and COUCHDB_PASSWORD (envFrom). Data at /opt/couchdb/data. Usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/couchdb/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "couchdb" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "couchdb" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
crafty-controller
A Crafty Controller server (a web control panel that installs, starts, stops and backs up Minecraft servers). A composable http workload with the panel's state, the game servers and their backups each on a PersistentVolume. It serves HTTPS ON :8443 with a certificate it mints itself and has NO plaintext listener, so an exposure must talk TLS to the backend and probes are by connection. The game servers run as JVMs INSIDE this pod, so its limits are the budget for every server it hosts, and their listening ports are added with extraPort once the panel knows them. Single writer over ReadWriteOnce volumes: one replica, recreated.
server http
The Crafty Controller panel. Its config, TLS certificate and SQLite database live at /crafty/app/config, the managed servers at /crafty/servers and the archives at /crafty/backups, each on its own volume. Runs as the image own unprivileged user with the root group (every file in the image is group-writable that way) and a writable root filesystem, which the panel requires of its working directory. Compose an exposure that speaks TLS to the backend.
Import: import 'github.com/metio/kurly/workloads/crafty-controller/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "crafty-controller" |
image | string | — | — |
configSize | quantity | — | "1Gi" |
serversSize | quantity | — | "20Gi" |
backupsSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
cryptpad
A CryptPad server (end-to-end encrypted, collaborative documents and spreadsheets) on the official image. A plain composable http workload that keeps its encrypted blocks, blobs, and datastore on a PersistentVolume — no external database. The Node app writes under /cryptpad, so it relaxes read-only-rootfs while keeping non-root and dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000; needs a config.js with a main and a separate sandbox origin.
server http
The CryptPad server. Keeps the encrypted datastore at /cryptpad/data on the volume. Mount a config.js (kurly.config) setting httpUnsafeOrigin (main URL) and httpSafeOrigin (a SEPARATE sandbox domain, required for security). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cryptpad/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cryptpad" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
mainDomain | string | — | "http://localhost:3000" |
sandboxDomain | string | — | "http://localhost:3001" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cyberchef
A CyberChef server (GCHQ's "cyber Swiss-army knife" for encoding, encryption, compression and data analysis, all in the browser) on the official image. Stateless: a plain rolling Deployment. Serves on :8000.
server http
The CyberChef server. Stateless. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/cyberchef/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "cyberchef" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dagu
A Dagu server (a workflow engine running DAGs declared in YAML, with a web UI to launch, watch and retry them). A plain composable http workload on the project's own image: DAG definitions, run history and logs are files under DAGU_HOME on a PersistentVolume, so no external database or queue is needed. The image's entrypoint runs as root to reconcile uids and drops privileges with sudo, which would need a root container and a writable image tree — the stage calls the dagu binary directly instead, keeping the restricted posture, and fsGroup does the job the entrypoint's chown was doing. DAG steps run as child processes IN THIS CONTAINER, so a step can only use what the image ships. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Dagu server and scheduler in one process (`dagu start-all`). DAGs, run history and logs live under /var/lib/dagu on the volume. Steps needing tools this image does not ship belong in Dagu's Kubernetes or SSH executors rather than in a larger image here. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/dagu/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dagu" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
damselfly
A Damselfly server (server-based photo management that indexes a picture library and recognises faces and objects with local ML models) on the official image. Three PersistentVolumes, because the entrypoint runs the server against three fixed paths: /pictures (the library it indexes), /config (its SQLite database, logs and settings) and /thumbs (the generated thumbnail cache) — kurly.store composed three times. The image declares no USER, so the stage pins a non-root uid with a matching fsGroup (which is what gives the app ownership of the volumes) and adds a scratch at /tmp for the .NET runtime. Indexing runs in-process on start, so a startup probe carries the slow first boot. No external database. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :6363.
server http
The Damselfly server. pictureSize/configSize/thumbsSize size the three PVCs (/pictures, /config, /thumbs). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/damselfly/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "damselfly" |
image | string | — | — |
pictureSize | quantity | — | "100Gi" |
configSize | quantity | — | "10Gi" |
thumbsSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
dashy
A Dashy server (a highly customizable, self-hosted dashboard for your services) on the official image. A plain composable http workload — its configuration lives on a PersistentVolume, no external database. The image rebuilds assets on a config change and writes the root filesystem, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Dashy server. Configuration lives at /app/user-data/conf.yml on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/dashy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dashy" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
databasus
A Databasus server (scheduled backups for the PostgreSQL, MySQL and MongoDB databases you already run, with a web interface, restore, and notifications when a run fails). IT RUNS ITS OWN POSTGRESQL INSIDE THE POD AND THERE IS NO WAY OUT: the image starts an embedded database for its schedules and history on the same volume as the backups it takes, so one PersistentVolume holds both the backups and the only record of what was backed up, in the same cluster as the databases it protects — a defence against a dropped table, not against losing the cluster. The entrypoint needs root: it reconciles the embedded postgres uid, chowns three directories under the data volume and writes the frontend's runtime configuration into the image tree before starting anything, so four defaults are relaxed deliberately. It holds credentials for everything it backs up, so a NetworkPolicy limiting egress to exactly those is worth composing on. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4005.
server http
The Databasus server. Backups, the temporary spool and the embedded PostgreSQL all live under /databasus-data on the volume, because the image puts them there. publicUrl is what the notification mails link to and half of what turns email on; secretName holds the SMTP credentials and any OAuth client secrets through envFrom. The startup budget is long because the embedded database is initialised on first start. Compose an exposure onto the HTTP port, and a backup axis onto this volume if the copies have to survive the cluster.
Import: import 'github.com/metio/kurly/workloads/databasus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "databasus" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
publicUrl | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
davis
A Davis server (a self-hosted CalDAV and CardDAV server with a simple admin UI, built on sabre/dav) on the official image, backed by an external database (MySQL/MariaDB, PostgreSQL, or SQLite). A plain composable http workload. kurly authors no Secret; DATABASE_URL, APP_SECRET and the admin login come from a provided Secret via envFrom. Pair it with a database you run separately. Stateless: calendars and contacts live in the database, so a plain rolling Deployment. Serves on :80.
server http
The Davis server. Provide the Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/davis/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "davis" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "davis" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
davos
A davos server — a self-hosted FTP automation tool that watches remote directories and downloads on a schedule. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The A davos server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/davos/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "davos" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
defguard
The defguard core (the identity and access half: user accounts, MFA, OpenID Connect, and the WireGuard peer configuration its gateways enforce) on the project's own image, backed by an external PostgreSQL. Pairs with a cnpg-cluster named defguard-db. THIS IS THE CONTROL PLANE, NOT THE VPN — core hands each gateway its peer list, while the gateway carrying WireGuard traffic needs NET_ADMIN and the node's network and is a separate deployment decision, so core alone is a directory with no tunnel behind it. Session cookies are marked Secure, so a browser reaching it over plain HTTP discards them and the login never completes; cookieInsecure relaxes that only for a deployment terminating TLS out of the application's sight. Enterprise features live in the same repository under a separate licence and the image is one build. Stateless: a plain rolling Deployment. Serves on :8000.
core http
The defguard core. dbHost/dbPort/dbName/dbUser point at PostgreSQL, defaulting to a cnpg-cluster named defguard-db; secretName holds DEFGUARD_DB_PASSWORD and the application secrets (envFrom). url is the browser-visible URL enrollment links and the OpenID issuer are built from. Probes are by connection, because every HTTP route redirects an unauthenticated caller. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/defguard/core.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "defguard" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "defguard-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "defguard" |
dbUser | string | — | "defguard" |
url | string | — | — |
cookieInsecure | bool | — | false |
secretName | string | — | "defguard" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
deluge
A Deluge server — a lightweight, self-hosted BitTorrent client with a web UI. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8112.
server http
The A Deluge server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/deluge/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "deluge" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dex
A Dex server (an OpenID Connect / OAuth 2.0 identity provider that federates to upstream connectors — LDAP, SAML, GitHub, Google, …) on the official image. A plain composable http workload; with the SQLite storage backend its state lives on a PersistentVolume, no external database. Driven entirely by a config.yaml (issuer, storage, connectors, staticClients) mounted from a Secret — kurly authors none. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves OIDC on :5556.
server http
The Dex server (runs `dex serve /etc/dex/config.yaml`). SQLite state at /var/dex on the volume. configSecret is the Secret holding config.yaml (mounted at /etc/dex; it carries client and connector secrets). Point storage at PostgreSQL in the config, or the kubernetes backend with kurly.rbac, to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/dex/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dex" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
configSecret | string | — | "dex" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dgraph
A Dgraph cluster in two stages: zero, the coordinator that decides which alpha group owns which part of the graph, and alpha, which holds the data and answers GraphQL and DQL queries. Both are stateful, because both are Raft members with volumes that follow their identity. THE ALPHAS WILL NOT START WITHOUT A ZERO and zero holds no data of its own — deploy both, zero first. ONE ZERO IS NOT HIGH AVAILABILITY: Raft needs an odd number, so surviving a loss wants three, and one is the default because that is a single-node deployment. REPLICAS ARE SHARDS, NOT COPIES — raising alpha's replicas adds capacity, while how many copies exist of each piece of data is zero's shardReplicas, and setting one without the other is how a deployment ends up with no redundancy while looking three times bigger. Security is off until you turn it on: whitelist is the CIDR list allowed to call the admin API and defaults to nothing, because a default admitting a whole cluster network is not one anybody chose.
alpha stateful
The Dgraph data nodes. zeroHost/zeroPort point at the zero stage's headless Service, without which an alpha waits rather than serving. whitelist is the CIDR list allowed to call the admin API. The posting list and write-ahead log live under /dgraph on the volume, and the startup budget is long because a cold alpha replays that log before it answers. Compose an exposure onto :8080 for clients outside the cluster.
Import: import 'github.com/metio/kurly/workloads/dgraph/alpha.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dgraph-alpha" |
image | string | — | — |
replicas | int | — | 1 |
zeroHost | string | — | "dgraph-zero-headless" |
zeroPort | int | — | 5080 |
whitelist | array | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"8Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
zero stateful
The Dgraph coordinator. replicas is this stage's own Raft membership (odd numbers; three tolerates one loss); shardReplicas is how many alphas hold each copy of the graph, which is a different number. Serves gRPC on :5080 and its admin HTTP on :6080 — neither belongs on the internet, since :6080 will move data and drop nodes for anyone who asks. A headless Service gives each pod the stable name Raft needs.
Import: import 'github.com/metio/kurly/workloads/dgraph/zero.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dgraph-zero" |
image | string | — | — |
replicas | int | — | 1 |
shardReplicas | int | — | 1 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
directory-lister
A Directory Lister server (a PHP web index that lists a folder and serves its files, with search, sorting, README rendering and zip downloads) on the official image. A plain composable http workload: the listed folder is a PersistentVolume mounted at FILES_PATH, filled by whatever writes the content. The Apache + PHP image writes its view and file caches beside the application code, so a scratch covers /var/www/html/app/cache and another /var/run/apache2, keeping the rest of the root filesystem read-only. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Directory Lister server. Lists whatever is on the volume mounted at filesPath. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/directory-lister/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "directory-lister" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
filesPath | string | — | "/data" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
discount-bandit
A Discount Bandit server (a multi-user price tracker for Amazon, eBay, AliExpress and custom stores, notifying when a price meets the criteria a user set). A composable http workload backed by an external MySQL/MariaDB: the image ships SQLite as Laravel's default and creates no database file, so this stage runs it on MySQL instead. FRANKEN_HOST is set to 0.0.0.0 — the image bakes localhost, which Octane passes to FrankenPHP as its listen address, so the default serves the loopback interface only and every request through the Service is refused by a pod that looks healthy. The entrypoint copies .env into the app tree, migrates, and writes Laravel's compiled caches beside the code, and supervisord then runs Octane, the scheduler and the queue worker as root, so it runs as root with a writable root filesystem while dropping all capabilities. The first start migrates, seeds and warms the caches before anything listens, so it carries a startup probe rather than a stretched liveness delay. kurly authors no Secret; APP_KEY and DB_PASSWORD come from a provided Secret via envFrom. The pod also runs the scheduler and the queue worker, so a second replica would scrape every product twice: one replica, recreated. Serves on :80.
server http
The Discount Bandit server. dbHost/dbPort/dbName/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). appUrl is the public URL links and assets are built against. cron is how often the scheduler re-checks every tracked product. secretName holds APP_KEY — Laravel's 32-character encryption key, under which stored credentials are unreadable once it changes — and DB_PASSWORD, both via envFrom. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/discount-bandit/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "discount-bandit" |
image | string | — | — |
dbHost | string | — | "discount-bandit-db" |
dbPort | int | — | 3306 |
dbName | string | — | "discount-bandit" |
dbUser | string | — | "discount-bandit" |
appUrl | string | — | — |
cron | string | — | "*/5 * * * *" |
timezone | string | — | "UTC" |
secretName | string | — | "discount-bandit" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
docassemble
A docassemble server (an expert system that runs guided interviews written in YAML and Python and assembles the documents they produce). A composable http workload running the project's ALL-IN-ONE container: PostgreSQL, Redis, RabbitMQ, Apache and the background workers are all inside the one image, so it needs nothing outside the cluster. It persists through the project's OWN backup directory rather than through its data directories — the container dumps its database, Redis snapshot, configuration and uploaded files there as it stops and restores them as it starts, so the shutdown grace period has to outlast the dump. supervisord starts each of those services and drops it to its own account, which it can only do from root, so this workload is deliberately less hardened. First start is minutes long (database initialisation, migrations, asset compilation), which is a startup probe budget. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The docassemble server, running the all-in-one container. The PersistentVolume is mounted at /usr/share/docassemble/backup, which is what the container backs itself up into and restores from — the data directories themselves are not mounted, because the Python virtualenv and the packaged configuration live beside them in the same tree and an empty volume over that tree stops the server from starting. shutdownGrace is how long the dump is given before the pod is killed; too short and the instance comes back at the last dump that finished. hostname is the name browsers reach the server by, which docassemble builds its links and its outgoing mail from, and behindHttpsLoadBalancer tells it that TLS is terminated in front of it so those links say https. The first account is the published default until you change it. Probed by connection, because the root path redirects and answers differently depending on who is signed in. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/docassemble/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docassemble" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
hostname | string | — | — |
behindHttpsLoadBalancer | bool | — | true |
timezone | string | — | "UTC" |
shutdownGrace | int | — | 300 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"6Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
docker-mailserver
A full mail server in one container: Postfix for SMTP, Dovecot for IMAP, plus spam filtering, virus scanning, DKIM signing and TLS. A composable http workload whose PRIMARY PORT IS :25, with submission on :587 and :465 and IMAP on :143 and :993 beside it — none of them HTTP, so an Ingress or HTTPRoute cannot carry them and it needs a LoadBalancer or NodePort Service instead. IT REFUSES TO START WITHOUT A MAILBOX: it waits two minutes for one and then shuts down, so either `setup email add` is run inside the pod in that window or accountsSecret names a Secret holding a postfix-accounts.cf, which kurly does not author. It also needs the fully qualified name its MX record points at, since receiving servers compare the greeting name, the reverse DNS and the certificate. Maildirs, service state and the account configuration each take a ReadWriteOnce volume: one replica, recreated.
server http
The mail server. Maildirs live at /var/mail, the spam corpus, DKIM keys and account databases at /var/mail-state, and the configuration the setup command writes at /tmp/docker-mailserver — one volume each. accountsSecret names a Secret holding a postfix-accounts.cf, one address|{SCHEME}hash line per mailbox, mounted read-only over the file on that volume; leave it null to manage accounts with the setup command inside the pod instead. hostname sets the SMTP banner, the greeting name and the certificate subject and must be the fully qualified name of this server, or outbound mail is refused as spam. Postfix, Dovecot and the filters are started as root by supervisord and drop to their own accounts, and they bind privileged ports, so this workload is deliberately less hardened. fail2ban is off: it needs NET_ADMIN and would ban the shared address everything behind the cluster arrives from.
Import: import 'github.com/metio/kurly/workloads/docker-mailserver/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docker-mailserver" |
image | string | — | — |
hostname | string | — | "mail.example.com" |
mailStorageSize | quantity | — | "20Gi" |
stateStorageSize | quantity | — | "5Gi" |
configStorageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
accountsSecret | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
docker-registry-ui
A Docker Registry UI server (a clean, self-hosted web interface for browsing a Docker/OCI registry: repositories, tags, inspect and delete images) on the official image. Stateless: a plain rolling Deployment that talks to the registry you point it at. Serves on :80.
server http
The Docker Registry UI server. registryUrl points at the registry (NGINX_PROXY_PASS_URL); registryTitle sets the page title. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/docker-registry-ui/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docker-registry-ui" |
image | string | — | — |
replicas | int | — | 2 |
registryUrl | string | — | — |
registryTitle | string | — | "Docker Registry" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
docling-serve
A Docling Serve instance (an HTTP API around Docling, converting PDFs, Office documents, HTML and images into structured Markdown or JSON). A plain composable http workload on the project's own image: each request is converted and returned and nothing is kept, so it claims no volume. MEMORY, NOT CPU, IS THE LIMIT — conversion loads layout and table models and holds a whole document in memory, so the default limit is sized for that and a large scanned PDF will want more; a container killed mid-conversion looks like an application crash and is a memory limit. The bundled Gradio interface is off by default, being a second unauthenticated way in. Serves the API on :5001.
server http
The Docling Serve API. enableUi turns on the bundled Gradio interface at /ui. Model and conversion behaviour is set through DOCLING_SERVE_* in env. The caches conversion writes go to scratch volumes, so the root filesystem stays read-only. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/docling-serve/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docling-serve" |
image | string | — | — |
replicas | int | — | 1 |
enableUi | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
docmost
A Docmost server (a self-hosted, open-source collaborative wiki and documentation platform) on the official image, backed by an external PostgreSQL and Redis, with attachments on a PersistentVolume. Pairs with a cnpg-cluster named docmost-db and a Redis. kurly authors no Secret; DATABASE_URL, REDIS_URL and APP_SECRET come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Docmost server. appUrl is the public URL. secretName holds DATABASE_URL, REDIS_URL and APP_SECRET (envFrom). Attachments at /app/data/storage; point STORAGE_DRIVER at S3 to scale past the single writer. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/docmost/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docmost" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
secretName | string | — | "docmost" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
docs
A Docs deployment in three stages — the collaborative note-taking and wiki tool incubated by France's DINUM and now a Digital Public Good: frontend (the application a browser reaches), backend (the Django API), and y-provider (the collaboration server). EACH ONE MATTERS: without y-provider a deployment looks like it works until two people open the same document, and then each sees only their own typing with nothing in a log to explain it. SIGN-IN GOES THROUGH AN EXTERNAL PROVIDER — Docs authenticates over OIDC and has no local accounts, so without one configured nobody can sign in at all; DJANGO_SECRET_KEY signs the session cookie, and a value that changes on restart signs everybody out. Attachments go to object storage rather than a volume, which is why no stage claims one. y-provider holds the in-flight document in memory and hands the result to the backend, so it runs as one replica — two would hold different copies of the same page.
backend http
The Django API. dbHost/dbPort/dbName/dbUser point at PostgreSQL and redisHost/redisPort at the cache; publicUrl is the origin a browser reaches Docs at, which the links and OIDC redirects are built from. secretName holds DJANGO_SECRET_KEY, DB_PASSWORD, the OIDC client credentials and the object-storage keys.
Import: import 'github.com/metio/kurly/workloads/docs/backend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docs-backend" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "docs-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "docs" |
dbUser | string | — | "docs" |
redisHost | string | — | "docs-cache" |
redisPort | int | — | 6379 |
publicUrl | string | — | — |
secretName | string | — | "docs" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
frontend http
The application a browser reaches, served by nginx on :8080. Compose an exposure onto THIS stage, and route /api and /collaboration to the backend and y-provider through the same origin.
Import: import 'github.com/metio/kurly/workloads/docs/frontend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docs-frontend" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
y-provider http
The collaboration server, carrying Yjs updates between everyone editing a page and converting documents on export. backendHost/backendPort is where it hands finished documents; secretName must be the same Secret the backend reads, since they authenticate each other with a shared value. One replica: the in-flight document lives in this process.
Import: import 'github.com/metio/kurly/workloads/docs/y-provider.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docs-y-provider" |
image | string | — | — |
backendHost | string | — | "docs-backend" |
backendPort | int | — | 8000 |
secretName | string | — | "docs" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
docspell
A Docspell installation (a document organiser: it converts what you upload to PDF, OCRs it, guesses the correspondent and tags from what it has learned, and makes the lot full-text searchable) as two composable stages on the official images — server (the UI and API on :7880) and joex (the job executor that does the converting and indexing, addressed on :7878). Both share one external PostgreSQL, which also holds the documents, so neither stage claims a volume. Run BOTH: a server without a joex node accepts uploads and processes none of them. Configuration is entirely environment — a config key uppercased, dots to _ and dashes to __ — and the bind address is set explicitly because Docspell binds localhost by default, where neither the probe nor the Service reaches it. Signup mode carries upstream default open: anybody who can reach the URL can make an account.
joex http
The Docspell job executor: it takes every uploaded document off the queue and converts, OCRs, classifies and indexes it, carrying the toolchain (Tesseract, Ghostscript, LibreOffice, ocrmypdf) that does so — give it real memory. An http stage rather than a worker because it is ADDRESSED: a node registers its own base URL in the database and the server calls back to it, so baseUrl must resolve to this stage own Service. Do not expose it. Docspell identifies a node by appId, so a second executor is this stage rendered again with a different name and appId, never a second replica. Same PostgreSQL as the server; secretName holds DOCSPELL_JOEX_JDBC_PASSWORD.
Import: import 'github.com/metio/kurly/workloads/docspell/joex.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docspell-joex" |
image | string | — | — |
baseUrl | string | — | "http://docspell-joex:7878" |
appId | string | — | "joex1" |
jdbcUrl | string | — | "jdbc:postgresql://docspell-db-rw:5432/docspell" |
dbUser | string | — | "docspell" |
secretName | string | — | "docspell" |
poolSize | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
server http
The Docspell REST server, serving the UI and API on :7880. jdbcUrl/dbUser default to a cnpg-cluster named docspell-db; baseUrl is the public URL it builds links from. secretName holds DOCSPELL_SERVER_BACKEND_JDBC_PASSWORD (kurly mints none), pulled in via envFrom. signupMode is open, invite or closed — decide before exposing it. Compose an exposure onto the HTTP port, and run a joex stage alongside it.
Import: import 'github.com/metio/kurly/workloads/docspell/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docspell" |
image | string | — | — |
baseUrl | string | — | — |
jdbcUrl | string | — | "jdbc:postgresql://docspell-db-rw:5432/docspell" |
dbUser | string | — | "docspell" |
signupMode | string | — | "open" |
secretName | string | — | "docspell" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
docuseal
A DocuSeal server (a self-hosted document-signing platform: build fillable PDF forms and collect legally-binding e-signatures, an open alternative to DocuSign) on the official image; with the default SQLite backend its database and uploaded documents live on a PersistentVolume. Point it at an external PostgreSQL (DATABASE_URL) to scale past the single SQLite writer. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The DocuSeal server. Data at /data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/docuseal/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "docuseal" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dokuwiki
A DokuWiki server (a simple, database-less wiki that stores its pages as flat files) on the official image. A plain composable http workload — all content lives on a PersistentVolume, no external database. The nginx + PHP-FPM image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The DokuWiki server. All content lives at /storage on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/dokuwiki/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dokuwiki" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
domoticz
A Domoticz server (a home-automation system for switches, sensors, meters and weather devices, with its own event engine and dashboard) on the official image; its SQLite database, scripts and plugins live on a PersistentVolume. The entrypoint runs as root — it rsyncs the bundled plugin, template and dzVents examples onto the volume, chowns the userdata tree and touches a marker beside the application — so the workload runs as root with a writable root filesystem and grants back CHOWN and FOWNER only. WWW_PORT moves with the declared port, since the entrypoint passes it as -www. Local-network discovery and USB radios do not work through a ClusterIP; devices reachable by IP or MQTT do. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Domoticz server. Userdata (database, scripts, plugins) at /opt/domoticz/userdata. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/domoticz/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "domoticz" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
timezone | string | — | "Etc/UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
downtify
A Downtify server (paste a Spotify track, album or playlist link and it finds the audio on YouTube Music, converts it with ffmpeg and writes it out with album art and metadata attached). A plain composable http workload with two PersistentVolumes — a large one for the music library and a small one for settings and the playlist-monitor database — and no external service, account or API key. Needs egress to the public internet, which a default-deny NetworkPolicy takes away. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves the web app and API on :8000.
server http
The Downtify server. The music library is at /downloads and settings plus the SQLite monitor database at /data — sized separately because the library grows without limit. It reads no Secret: nothing has to be registered anywhere. There is no health endpoint, so the probes read /api/version rather than the single-page app at /. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/downtify/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "downtify" |
image | string | — | — |
mediaSize | quantity | — | "100Gi" |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"cpu":"2","memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
drawio
A draw.io server — the self-hosted diagrams.net web editor for flowcharts, UML and network diagrams. On the official image; stateless (a plain rolling Deployment). Serves on :8080.
server http
The A draw.io server. Stateless. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/drawio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "drawio" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dreamfactory
A DreamFactory server (it generates a documented REST API over databases and services you already have, with roles, API keys and rate limits in front of it). A composable http workload on the project's own image; its configuration and user accounts live in an external database. THE ENTRYPOINT REWRITES THE APPLICATION TREE — on every start it edits .env with sed, links an nginx site, rewrites php-fpm's pool configuration and may run composer install, all as root before the web stack drops to www-data — so root, escalation, the runtime capabilities and a writable root filesystem are relaxed deliberately rather than pretended away. APP_KEY belongs in the Secret: the image generates one on first start and keeps it inside the container, so it is regenerated on every restart and every session and encrypted field from the previous one becomes unreadable. What it can reach is the point and the risk: an operator with admin access can point it at any address this pod can reach, so a NetworkPolicy limiting egress to the databases it should expose is worth composing on. One replica, recreated: sessions and cache are on local disk. Serves on :80.
server http
The DreamFactory server. dbDriver/dbHost/dbPort/database/dbUser point at the database holding DreamFactory's OWN configuration, not the ones it exposes; secretName holds APP_KEY and DB_PASSWORD through envFrom. serverName is the hostname nginx answers for, and behindTlsProxy makes Laravel build https links when something in front terminates TLS. The startup budget is long because composer install on a cold image and the first admin request are both slow, and the gate is a connection because the first page is a setup form. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/dreamfactory/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "dreamfactory" |
image | string | — | — |
dbDriver | string | — | "mysql" |
dbHost | string | — | — |
dbPort | int | — | — |
database | string | — | "dreamfactory" |
dbUser | string | — | "dreamfactory" |
secretName | string | — | — |
serverName | string | — | — |
behindTlsProxy | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
drop
A Drop server (a distribution platform for DRM-free games: an imported library, a store front, and a desktop client that installs and updates from it) on the official image, backed by an external PostgreSQL. Pairs with a cnpg-cluster named drop-db. kurly authors no Secret; DATABASE_URL comes from a provided Secret via envFrom. The image runs nginx in front of the Nuxt server and the torrential depot, so it binds :3000 as root and writes its pid, logs and temporary bodies inside its own tree — root with a writable root filesystem, capabilities kept. Prisma migrates the database before anything listens, so the first boot is the slow one. Probed by connection. The imported games and the file-system object store live on two ReadWriteOnce volumes: one replica, recreated. Serves on :3000.
server http
The Drop server. externalUrl is the public URL handed to the desktop client — behind an exposure it must be the address clients can reach. secretName holds DATABASE_URL (envFrom). The game library at /library and the object store at /data, each on its own volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/drop/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "drop" |
image | string | — | — |
librarySize | quantity | — | "100Gi" |
dataSize | quantity | — | "10Gi" |
storageClass | string | — | — |
externalUrl | string | — | — |
secretName | string | — | "drop" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
druid
An Apache Druid cluster (the column-oriented analytics store for interactive queries over event data) as five composable stages: coordinator (which also runs as Overlord), historical, middle-manager, broker and router. Deploy them in that order — the coordinator creates the metadata tables the rest read. It runs WITHOUT ZOOKEEPER: every stage loads druid-kubernetes-extensions and uses HTTP-based segment and task management, so the services discover one another and elect leaders through the Kubernetes API, which is why each stage declares a Role over pods and configmaps and all stages of one cluster must share a namespace and a clusterIdentifier. Metadata lives in an external PostgreSQL and deep storage in an S3-compatible bucket — deep storage is written and read by several stages at once, which a ReadWriteOnce volume cannot serve. Only the historical claims a volume, and that volume is a segment CACHE rather than the record. Every stage's configuration is set through the image's druid_* environment translation; the credentials arrive the same way from a provided Secret, and kurly authors none. The configuration baked into the image is sized for dedicated machines, so each stage overrides its heap, direct memory and processing buffers. Druid ships no authentication until the basic-security extension is configured, so an exposure on the router publishes a console that can drop datasources and read every row — keep it in-cluster or authenticate in front of it.
broker http
The Druid Broker: the service a query is sent to. It reads the timeline of published segments, fans the query out to the historicals and the running ingestion tasks that hold them, and merges what comes back. Deploy it after the coordinator and the historical. It holds no state, so scale it with replicas. The direct memory budget must cover (numMergeBuffers + numThreads + 1) processing buffers, which is why heap and directMemory are set together with them. Serves the native and SQL query APIs on :8082.
Import: import 'github.com/metio/kurly/workloads/druid/broker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "druid-broker" |
image | string | — | — |
clusterIdentifier | string | — | "druid" |
dbHost | hostname | — | "druid-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "druid" |
dbUser | string | — | "druid" |
bucket | string | — | "druid" |
s3Endpoint | string | — | — |
s3Region | string | — | "us-east-1" |
secretName | string | — | "druid" |
heap | string | — | "512m" |
directMemory | string | — | "400m" |
varSize | quantity | — | "1Gi" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
coordinator http
The Druid Coordinator, which the image also runs as Overlord: it assigns segments to the historicals, balances them, and accepts ingestion tasks for the middle-managers. The FIRST stage — it creates the metadata tables the others read. clusterIdentifier must match across every stage of one Druid cluster. heap and directMemory replace the machine-sized budgets baked into the image. Serves the Coordinator/Overlord API on :8081; the console is on the router, so an exposure normally goes there.
Import: import 'github.com/metio/kurly/workloads/druid/coordinator.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "druid-coordinator" |
image | string | — | — |
clusterIdentifier | string | — | "druid" |
dbHost | hostname | — | "druid-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "druid" |
dbUser | string | — | "druid" |
bucket | string | — | "druid" |
s3Endpoint | string | — | — |
s3Region | string | — | "us-east-1" |
secretName | string | — | "druid" |
heap | string | — | "512m" |
directMemory | string | — | "128m" |
varSize | quantity | — | "2Gi" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
historical http
The Druid Historical: it downloads the segments the coordinator assigns it out of deep storage into a local segment cache and answers the broker over them. Deploy it after the coordinator. The volume is a CACHE, not the record — the segments are in the bucket and a historical that loses its cache downloads them again — but one process writes it, so this is one replica, recreated. segmentCacheSize is a hard budget Druid refuses to load past and belongs below storageSize. Serves its query API on :8083, an internal address rather than something to expose.
Import: import 'github.com/metio/kurly/workloads/druid/historical.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "druid-historical" |
image | string | — | — |
clusterIdentifier | string | — | "druid" |
dbHost | hostname | — | "druid-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "druid" |
dbUser | string | — | "druid" |
bucket | string | — | "druid" |
s3Endpoint | string | — | — |
s3Region | string | — | "us-east-1" |
secretName | string | — | "druid" |
heap | string | — | "768m" |
directMemory | string | — | "400m" |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
segmentCacheSize | string | — | "15g" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1536Mi"}} |
labels | object | — | — |
annotations | object | — | — |
middle-manager http
The Druid MiddleManager: it takes ingestion tasks from the Overlord and runs each in a forked peon process that builds segments and publishes them to deep storage. Without it a deployment queries what it holds and ingests nothing. THE PEONS RUN INSIDE THIS POD, each its own JVM, so workerCapacity multiplied by peonHeap is memory this container needs on top of its own — which is why its limit sits well above its heap. The task tree is ephemeral: a task that dies with the pod is retried by the Overlord. Serves the worker API on :8091 and each running task from :8100 up, neither of them something to expose.
Import: import 'github.com/metio/kurly/workloads/druid/middle-manager.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "druid-middle-manager" |
image | string | — | — |
clusterIdentifier | string | — | "druid" |
dbHost | hostname | — | "druid-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "druid" |
dbUser | string | — | "druid" |
bucket | string | — | "druid" |
s3Endpoint | string | — | — |
s3Region | string | — | "us-east-1" |
secretName | string | — | "druid" |
heap | string | — | "256m" |
directMemory | string | — | "128m" |
workerCapacity | int | — | 2 |
peonHeap | string | — | "512m" |
peonDirectMemory | string | — | "256m" |
varSize | quantity | — | "10Gi" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
router http
The Druid Router: one front door. It serves the web console, routes queries to the brokers, and proxies the Coordinator and Overlord APIs so an operator needs one address rather than three. Deploy it last. It holds no state, so scale it with replicas. Druid ships NO AUTHENTICATION until the basic-security extension is configured, and the console can drop datasources, submit ingestion tasks and read every row — an exposure on its own publishes an administrative interface to whoever reaches it. Serves the console and the proxied APIs on :8888.
Import: import 'github.com/metio/kurly/workloads/druid/router.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "druid-router" |
image | string | — | — |
clusterIdentifier | string | — | "druid" |
dbHost | hostname | — | "druid-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "druid" |
dbUser | string | — | "druid" |
bucket | string | — | "druid" |
s3Endpoint | string | — | — |
s3Region | string | — | "us-east-1" |
secretName | string | — | "druid" |
heap | string | — | "256m" |
directMemory | string | — | "128m" |
varSize | quantity | — | "1Gi" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
drupal
A Drupal server — a powerful, self-hosted open-source CMS and content framework (backed by MySQL/PostgreSQL). On the official image, backed by an external database, with data on a PersistentVolume (the image populates it on first run). kurly authors no Secret; the DB credentials come from a provided Secret via envFrom. The Apache master runs as root then serves as www-data. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Drupal server. secretName holds the DB credentials (envFrom). Data at /var/www/html/sites. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/drupal/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "drupal" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "drupal" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
duplicati
A Duplicati server — a self-hosted, encrypted backup client for cloud and remote storage, managed from the browser. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8200.
server http
The A Duplicati server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/duplicati/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "duplicati" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "duplicati" |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ech0
An Ech0 server (a lightweight publishing platform for short posts, federating with other instances). A plain composable http workload keeping its SQLite database and locally stored media under one PersistentVolume; pointing it at S3 object storage from its own settings moves the media out but not the database, so the volume stays either way. JWT_SECRET signs the tokens users hold and the published run command carries a documented value for it, so an instance left with that default is one anybody can mint an administrator token for — kurly authors no Secret, supply one and it is read via envFrom. The probes are connection probes rather than page requests, so a redirect or an authentication gate cannot leave the pod permanently unready. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6277.
server http
The Ech0 server. SQLite database and uploaded media at /app/data on the volume. secretName holds JWT_SECRET, which signs the tokens users hold — the image documents a fixed value for it, so supplying your own is what keeps administrator tokens unmintable. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ech0/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ech0" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "ech0" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ejabberd
An ejabberd server (a robust, scalable XMPP/messaging server) on the official community image. A plain composable http workload that keeps its Mnesia database and uploads on a PersistentVolume — no external database by default. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves XMPP client :5222, s2s :5269, and admin/HTTP :5280; mount ejabberd.yml at /home/ejabberd/conf.
server http
The ejabberd server. Keeps its Mnesia database at /home/ejabberd/database on the volume; mount ejabberd.yml at /home/ejabberd/conf (kurly.config; credentials from a Secret). Route the XMPP ports as TCP and expose :5280 for admin.
Import: import 'github.com/metio/kurly/workloads/ejabberd/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ejabberd" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
element-web
An Element Web server (the popular self-hosted web client for the Matrix network) on the official image. It serves a static app configured by a single config.json, mounted into the web root via a subPath ConfigMap mount beside the app assets, so it is stateless. Element is a client only; point it at a Matrix homeserver (e.g. matrix-conduit). Serves on :80.
server http
The Element Web server. homeserverUrl/serverName populate config.json default_server_config; brand sets the title; config overrides/extends Element's config.json verbatim. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/element-web/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "element-web" |
image | string | — | — |
replicas | int | — | 2 |
homeserverUrl | string | — | — |
serverName | string | — | — |
brand | string | — | "Element" |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
endurain
An Endurain server (a self-hosted fitness and training-activity tracker) backed by an external PostgreSQL and Redis, with uploads on a PersistentVolume. Pairs with a cnpg-cluster named endurain-db and a valkey named endurain-cache. kurly authors no Secret; DB_PASSWORD and SECRET_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Endurain server. dbHost/dbName/dbUser default to a cnpg-cluster named endurain-db; redisHost to a valkey named endurain-cache. endurainHost is the public URL. secretName holds DB_PASSWORD and SECRET_KEY (envFrom). Uploads at /app/backend/app on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/endurain/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "endurain" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "endurain-db-rw" |
dbName | string | — | "endurain" |
dbUser | string | — | "endurain" |
redisHost | string | — | "endurain-cache" |
endurainHost | string | — | — |
secretName | string | — | "endurain" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
enigma-12-bbs
An ENiGMA½ bulletin board system (message bases, file areas, ANSI art and legacy door games). A composable http workload on the project's own image, with configuration, SQLite databases and the file base each on their own PersistentVolume. Callers arrive over TELNET on :8888, a raw TCP protocol no HTTP exposure recipe can route — reach it with a LoadBalancer/NodePort Service or a Gateway TCPRoute, and probe it by connection. The image's entrypoint builds its configuration by asking questions on a terminal, which a cluster does not provide, so an init container writes the menu set and a minimal config.hjson non-interactively instead, only ever creating what is missing so later edits on the volume survive a restart. Runs as root on a writable root filesystem: the image declares no user and the engine writes inside its own install tree. Single writer over ReadWriteOnce volumes: one replica, recreated.
server http
The ENiGMA½ bulletin board. Configuration and generated menus live at /enigma-bbs/config, the SQLite databases at /enigma-bbs/db, and the file base at /enigma-bbs/filebase, one volume each. boardName is written into the configuration on the FIRST boot only; change it on the volume afterwards. The telnet login server listens on :8888 — expose it as TCP, not as HTTP. SSH and the web content server are off until a key pair and the listeners are configured on the volume.
Import: import 'github.com/metio/kurly/workloads/enigma-12-bbs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "enigma-12-bbs" |
image | string | — | — |
boardName | string | — | "ENiGMA BBS" |
configSize | quantity | — | "1Gi" |
databaseSize | quantity | — | "5Gi" |
fileBaseSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ente
Ente — a self-hosted, end-to-end-encrypted photo and video backup (the Google Photos alternative). The museum server is a stateless API keeping metadata in PostgreSQL and encrypted blobs in S3-compatible object storage; the Ente clients (mobile, desktop, web) point at it. Compose it with a cnpg-cluster and an S3 store such as seaweedfs.
server http
The Ente museum API on :8080, stateless (metadata in PostgreSQL, blobs in S3). Reads its base config from the image and merges the operator-supplied credentials file (`credentialsSecret`, a Secret with a credentials.yaml key carrying the DB DSN, the S3 endpoint/bucket/keys, and the app secrets); ENTE_CREDENTIALS_FILE points at it. Any value is also overridable by an ENTE_-prefixed env var.
Import: import 'github.com/metio/kurly/workloads/ente/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ente-server" |
image | string | — | — |
replicas | int | — | 1 |
credentialsSecret | string | — | "ente" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
web http
The Ente web front end — one image bundling every web app (Photos on :3000, Albums :3002, Cast :3004, Share :3005, Embed :3006). Stateless; it talks to the museum from the browser, so apiOrigin/albumsOrigin are the PUBLIC URLs the browser reaches, not in-cluster Services. Expose :3000 for the main UI; route the extra ports for the public-album/cast/share apps.
Import: import 'github.com/metio/kurly/workloads/ente/web.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ente-web" |
image | string | — | — |
apiOrigin | string | — | "https://ente-api.example.com" |
albumsOrigin | string | — | "https://albums.example.com" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
eonvelope
An Eonvelope server (a Django application that fetches mail over IMAP, POP, Exchange or JMAP and keeps it, with its attachments and correspondents, searchable indefinitely). A composable http workload backed by an external MySQL/MariaDB, with the archive on a PersistentVolume. It speaks HTTPS, not HTTP: gunicorn binds :443 with the self-signed certificate the image bakes in, so an exposure in front of it must talk TLS to the backend and must not verify that certificate; probes go by connection, which is also what Django's ALLOWED_HOSTS check needs. One container runs several services — s6-overlay supervises the web server, a RabbitMQ broker, celery worker and beat, the migrations and the creation of the initial admin account — so it runs as root with a writable root filesystem and a generous startup budget. SECRET_KEY, DATABASE_PASSWORD and DJANGO_SUPERUSER_PASSWORD all have PUBLISHED defaults in the project's own compose file, so supplying the Secret is the difference between an archive of your mail and everybody's. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Eonvelope server. The archived mail lives at /mnt/archive on the volume; everything else is in the external MySQL/MariaDB named by dbHost. databaseType switches to 'postgresql' or 'sqlite3'. allowedHosts must name every host the instance answers on — it defaults to '*', because Django refuses every request otherwise. secretName holds SECRET_KEY, DATABASE_PASSWORD and DJANGO_SUPERUSER_PASSWORD, all three of which ship with published defaults upstream. s6-overlay supervises the web server, the broker and celery in the one container and drops privileges from root, so this workload is deliberately less hardened. Compose a TLS-speaking exposure onto :443.
Import: import 'github.com/metio/kurly/workloads/eonvelope/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "eonvelope" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
dbHost | string | — | "eonvelope-db" |
dbPort | int | — | 3306 |
database | string | — | "email_archive_django" |
dbUser | string | — | "eonvelope" |
databaseType | string | — | "mysql" |
allowedHosts | string | — | "*" |
secretName | string | — | "eonvelope" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ergo
An Ergo server (a modern IRCv3 daemon written in Go, with account services, a bouncer and message history built into the server rather than bolted on beside it) on the official image. A plain composable http workload needing nothing else: configuration, database and TLS material all live in /ircd on a PersistentVolume. First boot writes ircd.yaml there and prints a generated admin oper password to the log ONCE, then never touches the file again — read the password out of the first pod and edit the file on the volume afterwards; the :6697 certificate it makes itself is self-signed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves plaintext IRC on :6667 and IRC-over-TLS on :6697 — route both as TCP.
server http
The Ergo server. Configuration, database and TLS material live at /ircd on the volume, which is also the working directory the shipped configuration resolves its relative paths against. /tmp is a scratch because the entrypoint builds the first configuration there before moving it onto the volume. Route :6667 and :6697 as TCP.
Import: import 'github.com/metio/kurly/workloads/ergo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ergo" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
esphome
An ESPHome dashboard server — the web dashboard and compiler for ESP8266/ESP32 smart-home firmware; runs as root, configs on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6052.
server http
The An ESPHome dashboard server. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/esphome/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "esphome" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
etherpad
An Etherpad server (a real-time collaborative document editor) on the official image, backed by an external PostgreSQL. Its documents live in the database, so it is stateless and can run several replicas. Pairs with a cnpg-cluster named etherpad-db. kurly authors no Secret; DB_PASS, ADMIN_PASSWORD, and APIKEY come from a provided Secret via envFrom. Serves on :9001.
server http
The Etherpad server. dbHost/dbName/dbUser default to a cnpg-cluster named etherpad-db. secretName holds DB_PASS, ADMIN_PASSWORD, and APIKEY (envFrom). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/etherpad/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "etherpad" |
image | string | — | — |
dbHost | string | — | "etherpad-db-rw" |
dbName | string | — | "etherpad" |
dbUser | string | — | "etherpad" |
secretName | string | — | "etherpad" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
evcc
An evcc server (a solar-aware charging controller for electric vehicles: it reads inverters, meters and wallboxes and shifts charging into the hours own production covers) on the official image. Its whole behaviour is its config (site, meters, chargers, vehicles, loadpoints, tariffs), mounted verbatim as /etc/evcc.yaml; with no config file it boots into its configuration UI and stores what is entered in the database. The SQLite database is pinned onto the volume with EVCC_DATABASE_DSN. Devices addressed by IP work as they are; the discovery protocols (mDNS, SMA Speedwire, KEBA, EEBus) are broadcast UDP a pod network does not carry, so discovery needs host networking. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :7070.
server http
The evcc server. config is evcc's own settings (site/meters/chargers/vehicles/loadpoints/tariffs), rendered to the mounted /etc/evcc.yaml; null starts the configuration UI instead. Database at /data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/evcc/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "evcc" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
config | object | — | — |
timezone | string | — | "Europe/Berlin" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
evershop
An EverShop server (an e-commerce platform: product catalogue, cart, checkout and an admin back office). A plain composable http workload backed by an external PostgreSQL, with product images and other uploaded media on a PersistentVolume. It MIGRATES the schema on every start before it listens, so a first boot against an empty database takes minutes — the startup probe allows ten. The image EXPOSEs 80 and never binds it: the port comes from PORT and is 3000. The first administrator is created with the image's own CLI, not by kurly. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The EverShop server: storefront, GraphQL API and admin interface in one process. Uploaded media lives at /app/media on the volume; everything else is in PostgreSQL. secretName holds DB_PASSWORD. Schema migrations run before the server listens, so probes are by connection and the first boot is given room. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/evershop/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "evershop" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "evershop-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "evershop" |
dbUser | string | — | "evershop" |
dbSslMode | string | — | "disable" |
secretName | string | — | "evershop" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
excalidraw
An Excalidraw server (a virtual hand-drawn-style whiteboard) on the official image. Excalidraw is a client-side app — the container serves static assets and drawings live in the browser — so this workload is stateless and scales via replicas. The nginx image binds :80 as root, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Serves on :80.
server http
The Excalidraw static server on :80 (stateless — scale via replicas). The image tag is an immutable sha (Excalidraw ships no semver tags). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/excalidraw/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "excalidraw" |
image | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
expenseowl
An ExpenseOwl server (a simple, self-hosted expense tracker). A plain composable http workload that keeps its expenses in a file-backed store on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :8080.
server http
The ExpenseOwl server. Keeps its expenses at /app/data on the volume, so it needs nothing external. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/expenseowl/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "expenseowl" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ezbookkeeping
An ezBookkeeping server (a lightweight personal finance and accounting app: accounts, transactions, categories and reports, with a mobile-friendly UI). A plain composable http workload keeping its SQLite database on a PersistentVolume — no external database. The image already runs unprivileged, so nothing is relaxed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The ezBookkeeping server. Keeps its SQLite database at /ezbookkeeping/data on the volume; point the EBK_DATABASE_* settings at MySQL or PostgreSQL through env to move it off. secretName holds EBK_SECURITY_SECRET_KEY, which signs the tokens users hold — supply it, because the image ships a PUBLISHED default and an instance running with it accepts tokens anybody can mint. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ezbookkeeping/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ezbookkeeping" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "ezbookkeeping" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
falco
A Falco agent (runtime security: it watches the system calls every container on a node makes and raises an alert when one matches a rule — a shell in a container, a write to /etc, an unexpected outbound connection). A composable daemon workload, because the thing it watches is the node. LEAST PRIVILEGE, NOT PRIVILEGED: Falco is usually deployed fully privileged and does not need to be with the modern eBPF driver, which this stage uses — BPF, PERFMON, SYS_RESOURCE and SYS_PTRACE cover it, the set Falco's own chart documents for least-privileged mode, and the rest of the hardened posture stands including the read-only root filesystem. Compose kurly.privileged() only where a kernel is too old for the modern driver. It sees every syscall from every container on the node, which is why it is a cluster add-on rather than a tenant workload. Rules are the product: the shipped ruleset is a starting point, not a policy. Output goes to stdout as JSON.
agent daemon
The Falco agent, one pod per node, using the modern eBPF driver. namespace is where its ServiceAccount lives — it reads pod and namespace metadata so an alert names the container it came from rather than a container id. config merges over the rendered falco.yaml; rules adds files beside the shipped ruleset. /sys/kernel is mounted for the tracefs and debugfs the driver attaches through.
Import: import 'github.com/metio/kurly/workloads/falco/agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "falco" |
image | string | — | — |
namespace | string | — | "falco" |
config | object | — | — |
rules | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
ferretdb
A FerretDB server (an open-source, MongoDB-compatible database) — the Apache-2.0 alternative to MongoDB Community (SSPL) for a platform that monetizes hosting. A stateless proxy that speaks the MongoDB wire protocol and stores everything in a PostgreSQL backend (with the DocumentDB extension), so it needs no volume and can run several replicas. kurly authors no Secret; FERRETDB_POSTGRESQL_URL comes from a provided Secret via envFrom. Serves MongoDB wire on :27017.
server http
The FerretDB proxy. secretName holds FERRETDB_POSTGRESQL_URL (with the backend password, envFrom). The backend is a PostgreSQL with the DocumentDB extension — run one with cnpg-cluster pinned to imageName=ghcr.io/ferretdb/postgres-documentdb. Scales horizontally via replicas. Route :27017 as TCP for MongoDB clients.
Import: import 'github.com/metio/kurly/workloads/ferretdb/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ferretdb" |
image | string | — | — |
secretName | string | — | "ferretdb" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ferron
A Ferron web server (a fast, memory-safe HTTP server written in Rust, configured in KDL). Stateless: no database, no volume and no Secret, so it scales out freely. The stage writes its own KDL configuration on :8080 instead of the image's privileged :80, which keeps every capability dropped and the root filesystem read-only; logs go to stdout because no log file is configured. Out of the box it serves the image's placeholder site — point `root` at a kurly.store, a kurly.config or your own image to serve real content, or pass `config` for a whole KDL document.
server http
The Ferron server. Serves `root` on `port` with a generated KDL configuration mounted as a ConfigMap; `config` replaces that document verbatim, in which case `port` must match what it binds because it also governs the container and Service ports. Runs as uid 65534 with a read-only root filesystem and no scratch. replicas is a plain knob because there is no state to share. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ferron/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ferron" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 8080 |
root | string | — | "/var/www/ferron" |
config | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
fider
A Fider server (an open-source platform to collect and prioritize customer feedback) on the official image, backed by an external PostgreSQL. Stateless — its state lives in the database, so it can run several replicas. Pairs with a cnpg-cluster named fider-db. kurly authors no Secret; DATABASE_URL, JWT_SECRET, and SMTP creds come from a provided Secret via envFrom. Serves on :3000.
server http
The Fider server. baseUrl is the public URL. secretName holds DATABASE_URL (with the DB password), JWT_SECRET, and EMAIL_* (envFrom). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/fider/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "fider" |
image | string | — | — |
baseUrl | string | — | — |
secretName | string | — | "fider" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
filebrowser
A File Browser server (a self-hosted web file manager: browse, upload, edit and share files through a clean UI) on the official image; its SQLite database lives on a PersistentVolume. It manages the directory mounted at /srv — compose the volume to serve onto that path. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The File Browser server. Database at /database; compose the volume to serve onto /srv. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/filebrowser/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "filebrowser" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
filestash
A Filestash server (a self-hosted web file manager with a modern UI in front of many storage backends: SFTP, FTP, S3, WebDAV, Git) on the official image (pinned by digest; Renovate maintains it); config on a PersistentVolume. Files live on the configured backends, not here. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8334.
server http
The Filestash server. Add storage backends in the admin console. Config at /app/data/state. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/filestash/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "filestash" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
firefly-iii
A Firefly III server (a free, self-hosted personal-finance manager) on the official image, backed by an external PostgreSQL, with uploads on a PersistentVolume. Pairs with a cnpg-cluster named firefly-iii-db. The Apache + PHP image starts as root and binds :8080, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; DB_PASSWORD and APP_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Firefly III server. dbHost/dbName/dbUser default to a cnpg-cluster named firefly-iii-db. appUrl is the public URL. secretName holds DB_PASSWORD and APP_KEY (a 32-char key, envFrom). Uploads at /var/www/html/storage. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/firefly-iii/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "firefly-iii" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
dbHost | string | — | "firefly-iii-db-rw" |
dbName | string | — | "firefly" |
dbUser | string | — | "firefly" |
appUrl | string | — | — |
secretName | string | — | "firefly-iii" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
fittrackee
A FitTrackee server (a self-hosted workout and activity tracker: GPX/FIT uploads, maps, statistics) on the official image, backed by an EXTERNAL PostgreSQL that must have the PostGIS extension — v1+ stores geospatial data, and on a plain PostgreSQL the start-up migration fails and the pod restarts forever. Uploads and the static-map cache live on a PersistentVolume. Redis is optional: without it the API rate limits, the background workers and e-mail are off and the application carries on. kurly authors no Secret; DATABASE_URL (with the embedded database password) and APP_SECRET_KEY come from a provided Secret via envFrom. uiUrl is the origin browsers reach it at and the application does not start without it. The image ships no CMD, so its start-up script is passed as arguments to tini. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5000.
server http
The FitTrackee server. Uploads at /data/uploads and the Static Map 3 cache at /data/staticmap_cache on the volume. uiUrl is the public origin, baked into generated e-mails and feeds. redisUrl points at an optional Redis/valkey for rate limits, background workers and e-mail. workers is the gunicorn worker count in the pod. secretName holds DATABASE_URL and APP_SECRET_KEY (envFrom). The first start migrates the database before anything answers, so it is probed for start-up separately. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/fittrackee/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "fittrackee" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
uiUrl | string | — | "https://fittrackee.example.com" |
redisUrl | string | — | — |
workers | int | — | 1 |
secretName | string | — | "fittrackee" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
flame
A Flame server (a self-hosted, minimalist start page and application/bookmark dashboard for your homelab, with a built-in editor) on the official image; its SQLite database lives on a PersistentVolume. Set the admin PASSWORD via a Secret (kurly authors none). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5005.
server http
The Flame server. Set PASSWORD via env/envFromSecret. Data at /app/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/flame/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "flame" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
flaresolverr
A FlareSolverr server (a proxy that solves Cloudflare and DDoS-GUARD browser challenges by driving a headless browser, so scrapers and the *arr indexers can reach protected sites) on the official image. Stateless: a plain rolling Deployment. An internal helper — other workloads point their FlareSolverr URL at http://flaresolverr:8191, so it usually needs no exposure. Serves on :8191.
server http
The FlareSolverr server. logLevel sets LOG_LEVEL. Stateless; usually needs no exposure. Compose an exposure onto the HTTP port only if reached from outside the cluster.
Import: import 'github.com/metio/kurly/workloads/flaresolverr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "flaresolverr" |
image | string | — | — |
replicas | int | — | 1 |
logLevel | string | — | "info" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
flatnotes
A flatnotes server (a self-hosted, database-less note-taking app that stores everything as flat markdown files) on the official image. A plain composable http workload — your notes live on a PersistentVolume, no external database. kurly authors no Secret; the username, password, and secret key come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The flatnotes server. Notes live at /data on the volume. secretName holds FLATNOTES_USERNAME, FLATNOTES_PASSWORD, and FLATNOTES_SECRET_KEY (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/flatnotes/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "flatnotes" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "flatnotes" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
flowctl
A flowctl server (a self-service workflow execution platform with approvals, remote execution and scheduling) on the official image, backed by an external PostgreSQL, keeping its flow definitions and its execution logs on PersistentVolumes. Configuration is environment only: without a config.toml beside the binary flowctl builds everything from FLOWCTL_-prefixed variables and then validates it, so a required setting left out stops the process, which is why the stage writes the full block. The keystore URL is assembled at start from KEYSTORE_KEY in the Secret — flowctl wants base64key://<32 base64url bytes> and a Secret can carry the key material but not a scheme — and the entrypoint refuses to start without it rather than accepting the empty base64key:// that gocloud reads as a fresh random key, under which every stored flow secret becomes unreadable at the next restart. The schema install runs as an idempotent init container, and it builds its own connection string from the discrete FLOWCTL_DB__ settings rather than the DSN, so those are what the stage sets. The probes ask for a connection rather than a page, since every route is behind a session or a token. Service links are switched off, since the Service is named after the workload and the process reads FLOWCTL_-prefixed variables as configuration. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :7000.
server http
The flowctl server. dbHost/dbPort/dbName/dbUser/dbSslMode default to a cnpg-cluster named flowctl-db; adminUser is the superadmin, which cannot be changed after the first install. secretName holds FLOWCTL_DB__PASSWORD, FLOWCTL_APP__ADMIN_PASSWORD and KEYSTORE_KEY (envFrom); KEYSTORE_KEY is 32 base64url-encoded bytes and the entrypoint composes the base64key:// URL from it, so replacing it makes the stored flow secrets unreadable. rootUrl is the URL a browser reaches this install at, which the interface builds its links and OIDC redirects against — set it to whatever the exposure answers on. Flow definitions live at /app/flows and execution logs at /var/log/flowctl, neither rotated nor deleted until an operator says how long they are kept. flowctl validates its email messenger before it will even install, so smtpHost/smtpPort/smtpFrom/smtpMaxConns carry defaults that pass validation and send nowhere — point them at a real relay to get approval notifications. env merges over the defaults, so a key you set wins. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/flowctl/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "flowctl" |
image | string | — | — |
flowsSize | quantity | — | "1Gi" |
flowsStorageClass | string | — | — |
logsSize | quantity | — | "2Gi" |
logsStorageClass | string | — | — |
dbHost | string | — | "flowctl-db-rw" |
dbPort | string | — | "5432" |
dbName | string | — | "flowctl" |
dbUser | string | — | "flowctl" |
dbSslMode | string | — | "disable" |
adminUser | string | — | "flowctl_admin" |
rootUrl | string | — | "http://localhost:7000" |
secretName | string | — | "flowctl" |
smtpHost | string | — | "localhost" |
smtpPort | string | — | "25" |
smtpFrom | string | — | "flowctl@flowctl.invalid" |
smtpMaxConns | string | — | "1" |
workers | string | — | "10" |
flowExecutionTimeout | string | — | "1h" |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
fluidd
A Fluidd server (the web interface for Klipper-based 3D printers) on the official image. Stateless: a plain rolling Deployment serving a static single-page app. It talks to a Moonraker API on the printer host, which the browser reaches, not the pod. Serves on :80.
server http
The Fluidd web interface. Stateless. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/fluidd/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "fluidd" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
focalboard
A Focalboard server — a self-hosted project-management and kanban tool, an open alternative to Trello/Notion/Asana. On the official image; with the default SQLite backend its data lives on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The A Focalboard server. Data on the config volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/focalboard/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "focalboard" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
foldingathome
A Folding@home client — the distributed-computing client for disease research, with its web control panel. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :7396.
server http
The A Folding@home client. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/foldingathome/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "foldingathome" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
forgejo
A Forgejo Git forge (a maintained Gitea fork): repository hosting, issues, pull requests, and a package registry. A plain composable http workload on the rootless image, with its data on a PersistentVolume and its database external — pairs with the cnpg-cluster workload. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI/git-over-HTTP on :3000 and git-over-SSH on :2222.
server http
The Forgejo server. dbHost/dbName/dbUser/dbSecret default to a cnpg-cluster named forgejo-db (its -rw Service and the -app Secret CNPG mints, key password read via a file). rootUrl is the public base URL for links/clone URLs. env carries extra FORGEJO__section__KEY settings — provide SECRET_KEY/JWT_SECRET there (from a Secret) so sessions survive restarts. kurly authors no Secret. Compose an exposure onto the HTTP port; route TCP :2222 for SSH.
Import: import 'github.com/metio/kurly/workloads/forgejo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "forgejo" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "forgejo-db-rw" |
dbName | string | — | "forgejo" |
dbUser | string | — | "forgejo" |
dbSecret | string | — | "forgejo-db-app" |
rootUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
fossbilling
A FOSSBilling server (hosting and billing automation with a client area, an admin panel and a full API) on the official Apache/PHP image, which also runs the five-minute cron inside the container. Needs an external MySQL/MariaDB — the mysql-cluster workload provides one — whose coordinates are entered in the web installer on first visit and written into config.php inside the install tree, so kurly authors no Secret and the stage reads none. The whole install tree lives on a PersistentVolume, seeded from the image by an init container because a PersistentVolume arrives empty and would hide the application. Apache starts as root to bind :80 and drops its workers to www-data. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app on :80.
server http
The FOSSBilling server. The install tree — application, config.php and uploads — lives at /var/www/html on the volume, seeded from the image on first boot. Point it at a MySQL/MariaDB in the web installer. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/fossbilling/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "fossbilling" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
freshrss
A FreshRSS server (a free, self-hosted RSS and Atom feed aggregator) on the official image. A plain composable http workload that keeps its feeds and articles in SQLite on a PersistentVolume by default — no external database. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The FreshRSS server. Keeps its SQLite database at /var/www/FreshRSS/data on the volume; baseUrl is the public URL. Point it at external PostgreSQL/MySQL via the setup wizard to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/freshrss/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "freshrss" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
frigate
Frigate — a self-hosted NVR with real-time object detection. Keeps its config and SQLite database on one volume and recordings on another, decodes frames through a shared-memory scratch, and exposes an authenticated UI on :8971. Ships a minimal starter config (CPU detector, no cameras); replace it with your own. Hardware detectors (Coral, GPU) need device access added with the raw + escape hatch.
server http
The Frigate NVR: authenticated UI/API on :8971 (plain UI on 5000, RTSP restream on 8554, WebRTC on 8555 TCP/UDP), config + database on /config and recordings on /media (two ReadWriteOnce volumes, one replica, recreated). config.yml mounts read-only over /config; FRIGATE_RTSP_PASSWORD comes from `secretName`.
Import: import 'github.com/metio/kurly/workloads/frigate/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "frigate" |
image | string | — | — |
config | string | — | — |
storageSize | quantity | — | "1Gi" |
mediaSize | quantity | — | "100Gi" |
storageClass | string | — | — |
shmSize | quantity | — | "256Mi" |
cacheSize | quantity | — | "1Gi" |
secretName | string | — | "frigate" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
fusio
A Fusio server (an open-source API management platform) on the official Apache/PHP image, backed by an external MySQL/MariaDB or PostgreSQL (the mysql-cluster and cnpg-cluster workloads provide one). Everything it keeps lives in the database, so it claims no volume and scales horizontally. The entrypoint waits for the database, migrates the schema, creates the initial backend user and starts cron, supervisor and Apache as root, relaxing non-root and read-only-rootfs while keeping dropped capabilities and no privilege escalation; that first run takes minutes, so it has a startup probe. kurly authors no Secret; the Doctrine DSN in FUSIO_CONNECTION, FUSIO_PROJECT_KEY and FUSIO_BACKEND_PW come from a provided Secret via envFrom. Serves on :80.
server http
The Fusio server. secretName holds FUSIO_CONNECTION (a Doctrine DSN pointing at a MySQL/MariaDB or PostgreSQL), FUSIO_PROJECT_KEY and FUSIO_BACKEND_PW (envFrom). appUrl/appsUrl are the public URLs; backendUser/backendEmail describe the initial backend user. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/fusio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "fusio" |
image | string | — | — |
replicas | int | — | 1 |
appUrl | string | — | — |
appsUrl | string | — | — |
backendUser | string | — | "admin" |
backendEmail | string | — | "admin@example.com" |
secretName | string | — | "fusio" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gameap
A GameAP panel (the web UI, REST API and WebSocket endpoint that administer game servers, talking to a GameAP daemon on every node over gRPC) on the project's own image — a single static binary running as the unprivileged user the image ships, with its database and uploaded files on a PersistentVolume. It speaks SQLite, PostgreSQL or MySQL/MariaDB and defaults to SQLite on that volume, so it starts with nothing external; databaseDriver switches engine and the connection URL then comes from the Secret, because it carries the password. kurly authors no Secret; ENCRYPTION_KEY (which encrypts the credentials stored for managed nodes) and AUTH_SECRET (which signs session tokens) come from a provided one via envFrom. Nothing is relaxed: non-root, read-only rootfs, no privilege escalation and all capabilities dropped. Service links are disabled, since every variable it reads is unprefixed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8025.
server http
The GameAP panel. databaseDriver is sqlite (the default, on the volume at /var/lib/gameap), postgres or mysql; databaseUrl is the connection URL and is left to the Secret for an external server, since it carries the password. secretName holds ENCRYPTION_KEY and AUTH_SECRET (envFrom). Uploaded files live under the same volume. Probed on /api/health, which answers unauthenticated; the first start creates the schema, which the startup probe waits out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gameap/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gameap" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
databaseDriver | string | — | "sqlite" |
databaseUrl | string | — | — |
secretName | string | — | "gameap" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gameyfin
A Gameyfin server (a game library manager that scans directories of games, enriches them with metadata and cover art, and serves the result as a browsable web library users can download from). It refuses to start without APP_KEY, the AES key it encrypts stored credentials with, and generates none itself, so the Secret is a hard prerequisite rather than an option. A plain composable http workload keeping its H2 database, its stored content and its logs on a PersistentVolume. The games themselves are NOT on that volume — Gameyfin scans library directories an operator configures at runtime, so mount them where they really are. It is a Spring Boot application and publishes its management endpoints, including an unauthenticated restart endpoint, on a SEPARATE port 8081, which is deliberately not in the Service: the probes ask /actuator/health there in-pod and nothing outside the pod should reach it. The entrypoint aligns the gameyfin account to PUID/PGID, chowns the install tree and su-execs the JVM, so it runs as root with a writable root filesystem and five named capabilities while the application itself never runs as root. A JVM, a Vaadin front end and the Flyway migrations against a fresh database make the first start slow, so it carries a startup probe rather than a stretched liveness delay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Gameyfin server. H2 database, stored content and logs on the volume at /opt/gameyfin (data, db and logs). puid/pgid own the files the entrypoint hands over. secretName holds APP_KEY, without which it exits on startup. The game library is mounted separately — this volume holds only Gameyfin state. Compose an exposure onto the HTTP port; leave the management port 8081 unexposed.
Import: import 'github.com/metio/kurly/workloads/gameyfin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gameyfin" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
secretName | string | — | "gameyfin" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
garagehq
A Garage node (S3-compatible object storage written for copies that live in different buildings, so it tolerates slow and intermittent links rather than assuming a rack) as a stateful workload: a StatefulSet with a per-pod PVC and a headless Service, S3 on :3900, the static-website server on :3902, the admin API on :3903 and node-to-node RPC on :3901. A FRESH NODE STORES NOTHING — capacity is assigned by a cluster layout the operator applies against the running pod (`garage layout assign` then `garage layout apply`), and keys and buckets are created the same way, so a Ready pod is not yet a usable bucket and no probe can say otherwise. kurly authors no Secret; GARAGE_RPC_SECRET comes from a provided Secret via envFrom and Garage refuses to start without it, the same value on every node of a cluster. Configuration is one TOML file mounted by subPath at /etc/garage.toml; rpc_public_addr is left unset so each node advertises its own pod IP, since a shared ConfigMap cannot hold a different address per replica. Metadata (an LMDB index) and data share one volume by default — Garage would rather have the metadata on an SSD of its own, which is a second claim a consumer composes. Replicas are not the way to grow it: a real Garage cluster is one node per site, each in its own layout zone.
server stateful
A Garage node. replicationFactor is how many copies of every object the cluster keeps and must be met by the layout — 1 for a single node, 3 for the three-site deployment Garage is written for. s3Region is the region name S3 clients must send; s3RootDomain and webRootDomain enable virtual-host-style bucket addressing for the S3 API and the website server. secretName holds GARAGE_RPC_SECRET (envFrom), which every node of one cluster shares. config replaces the whole garage.toml when the defaults are not enough. After the pod is Ready, assign and apply a layout before anything can be stored. Compose an exposure onto the S3 port, or onto the website port to serve buckets as sites.
Import: import 'github.com/metio/kurly/workloads/garagehq/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "garage" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
replicationFactor | int | — | 1 |
s3Region | string | — | "garage" |
s3RootDomain | string | — | ".s3.garage.localhost" |
webRootDomain | string | — | ".web.garage.localhost" |
secretName | string | — | "garage" |
config | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gatus
A Gatus server (a self-hosted, developer-oriented health dashboard and status page: it probes endpoints on a schedule, evaluates conditions and alerts) on the official image. Its whole behaviour is its config (endpoints, conditions, alerting, storage), mounted as a ConfigMap and passed verbatim; its history database lives on a PersistentVolume. The default watches one sample endpoint and persists history to SQLite. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Gatus server. config is Gatus's own settings (endpoints/conditions/alerting/storage), rendered to the mounted config.yaml. History at /data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gatus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gatus" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gerbera
A Gerbera server (a UPnP/DLNA media server) on the official image. A plain composable http workload that keeps config.xml and its SQLite database on a PersistentVolume and reads the library from /content on the same volume — no external database. The entrypoint generates the config, chowns the volume and drops to the image account with su-exec, so the container starts as root with CHOWN/FOWNER/SETGID/SETUID granted back. SSDP discovery is multicast on 1900/udp and does not cross a pod network, so players must be pointed at the exposed address. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :49494.
server http
The Gerbera server. Config and database at /var/run/gerbera, media library at /content, both on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gerbera/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gerbera" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gerrit
A Gerrit Code Review server — git hosting where every push becomes a change others vote on before it merges. A plain composable http workload on the official image. The site directory also holds the program, so the four directories that must survive a restart get one PVC each (repositories, index, H2 databases, configuration) and the regenerable ones are emptyDir; Gerrit refuses to start on a missing index. First start initialises the site, installs every bundled plugin and builds the index, so the startup probe carries the wait. The batch init leaves auth.type at OpenID — decide the authentication before exposing it. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves the web UI and git-over-HTTP on :8080 and git-over-SSH on :29418.
server http
The Gerrit server. storageSize/indexSize/dbSize/etcSize size the four site PVCs (/var/gerrit/git, /index, /db, /etc). canonicalWebUrl is the address people reach it at, written to gerrit.canonicalWebUrl on every start — absent, Gerrit uses the pod hostname, which changes with every pod. env carries further entrypoint settings. Compose an exposure onto the HTTP port; route TCP :29418 for SSH.
Import: import 'github.com/metio/kurly/workloads/gerrit/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gerrit" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
indexSize | quantity | — | "5Gi" |
dbSize | quantity | — | "2Gi" |
etcSize | quantity | — | "1Gi" |
storageClass | string | — | — |
canonicalWebUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
ghostfolio
A Ghostfolio server (a self-hosted, open-source wealth-management and portfolio tracker for stocks, ETFs, crypto and more) on the official image, backed by an external PostgreSQL and Redis. A plain composable http workload. kurly authors no Secret; DATABASE_URL, the Redis settings, ACCESS_TOKEN_SALT and JWT_SECRET_KEY come from a provided Secret via envFrom. Pairs with a cnpg-cluster named ghostfolio-db and a Redis. Stateless: a plain rolling Deployment. Serves on :3333.
server http
The Ghostfolio server. Provide the Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ghostfolio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ghostfolio" |
image | string | — | — |
replicas | int | — | 2 |
redisHost | string | — | "ghostfolio-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "ghostfolio" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gitea-mirror
A Gitea Mirror server (mirrors GitHub repositories into a Gitea or Forgejo instance on a schedule, with a web UI to pick what is mirrored). A plain composable http workload on the project's own image: jobs, mirror history and the account live in a SQLite database on a PersistentVolume, so no external database is needed. Left to itself the image mints its session and encryption keys on first start and keeps them in dot-files on the volume; secretName takes that over for a deployment that wants them backed up and rotated elsewhere — the encryption key protects the stored forge tokens, so losing it loses them. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4321.
server http
The Gitea Mirror server. The SQLite database and the generated keys live under /app/data on the volume. publicUrl is what session cookies and OAuth callbacks are signed against, so it has to be the URL a browser actually reaches — a login that appears to succeed and bounces back to the form is this value disagreeing with the address bar. secretName holds BETTER_AUTH_SECRET and ENCRYPTION_SECRET through envFrom. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gitea-mirror/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gitea-mirror" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
publicUrl | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gitness
A Harness Open Source server — the project Gitness was renamed to — carrying git repositories, pull requests, an artifact registry and a web interface in one binary with SQLite underneath, so no database is needed beside it. PIPELINES AND GITSPACES NEED A DOCKER DAEMON the pod does not have: the binary drives a Docker API for pipeline steps and development environments, so repositories, the registry and the interface work while starting a pipeline reports Docker unreachable — gitspaces defaults to off rather than advertising a half that cannot work. The image reports usage to the project by default and metrics defaults to off here. :3000 is HTTP and :3022 is git-over-SSH, a raw TCP protocol needing a route of its own. Single writer over a ReadWriteOnce volume holding the database, the repositories and the registry blobs: one replica, recreated. Serves on :3000.
server http
The Harness Open Source server. url is the browser-visible base the clone URLs it prints are built from, so a wrong one hands every user a remote that does not resolve. gitspaces and metrics are both off by default. storageSize sizes the volume holding the SQLite database, the git repositories and the registry blobs. The first start creates all three before it listens, so the wait is a startup probe. Compose an exposure onto the HTTP port; the ssh port needs a TCP route.
Import: import 'github.com/metio/kurly/workloads/gitness/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gitness" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
url | string | — | — |
gitspaces | bool | — | false |
metrics | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gitproxy
A FINOS Git Proxy server (it sits between a developer and an upstream git host, holds each outgoing push, applies rules to it and releases it only once somebody approves). A composable http workload with the approval records and the held pushes on a PersistentVolume. TWO PORTS with different audiences: :8080 is the web UI and API to compose an exposure onto, :8000 is the git endpoint a developer pushes to, published beside it as the 'git' port. The configuration baked into the image is the project's own example — the session cookie is signed with a PUBLISHED secret, the local backend creates the well-known admin account, and the authorised list names only the project's own repository — so supply your own configuration and change that password before anything can reach it. The root filesystem is writable because the entrypoint writes the UI's runtime configuration into the asset directory it serves from. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Git Proxy server. The file-backed database and the held pushes live at /app/.data on the volume. The web UI and API are on :8080 (compose an exposure onto it) and the git endpoint developers push to is on :8000, published on the Service as the 'git' port. Change the published cookie secret, the default admin password and the example authorised list before exposing it.
Import: import 'github.com/metio/kurly/workloads/gitproxy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gitproxy" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
glance
A Glance server (a self-hosted dashboard that puts feeds, RSS, weather, markets, monitoring and homelab widgets on one fast page) on the official image. Its layout is its glance.yml, mounted as a ConfigMap and passed verbatim; it keeps no persistent state. Stateless: the default shows a minimal page. Serves on :8080.
server http
The Glance server. config is Glance's own glance.yml (pages/columns/widgets), rendered to the mounted glance.yml. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/glance/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "glance" |
image | string | — | — |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
godoxy
A GoDoxy server (a reverse proxy whose routes are edited from a web interface and whose certificates are issued for it) on the official image, keeping its route configuration on a PersistentVolume. Upstream's own deployment discovers routes from Docker container labels through a socket a pod does not have, so nothing is discovered here: the routes are the files under /app/config, written through the WebUI or placed there beforehand, and no DOCKER_HOST or socket-proxy sidecar is rendered. The listen addresses are moved to unprivileged ports and the HTTPS listener is switched off with HTTP/3, because the cluster already terminates TLS in the exposure composed onto this workload; GoDoxy's own autocert wants a DNS-01 token and a wildcard record, which is a second certificate authority in a cluster that has one. The API port is published rather than left on the loopback address the image defaults to, since the WebUI's browser and any probe reach it from outside the pod. kurly authors no Secret; GODOXY_API_USER and GODOXY_API_PASSWORD are required unless OIDC is configured — without them the process exits — and GODOXY_API_JWT_SECRET keeps sessions across a restart. NET_BIND_SERVICE is granted back because the binary carries it as a file capability and cannot be exec'd at all once the bounding set drops it. Probes ask the port: the proxy answers with whatever a route names, or 404 for an unknown host, and the API answers 401. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The GoDoxy server. Route configuration, middlewares and config.yml at /app/config on the volume. env is merged over the defaults GODOXY_HTTP_ADDR=:8080, GODOXY_HTTPS_ADDR= (empty, no TLS listener), GODOXY_HTTP3_ENABLED=false, GODOXY_API_ADDR=:8888 and GODOXY_METRICS_DISABLE_SENSORS=true, so a key you set wins — that is also how the HTTPS listener is given back, with a matching extraPort. secretName holds GODOXY_API_USER, GODOXY_API_PASSWORD and GODOXY_API_JWT_SECRET. Compose an exposure onto the HTTP port, which is where the WebUI is answered by hostname.
Import: import 'github.com/metio/kurly/workloads/godoxy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "godoxy" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "godoxy" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gogs
A Gogs server (a painless, self-hosted Git service: a lightweight, fast Git server with a clean web UI) on the official image; with the default SQLite backend its repositories and data live on a PersistentVolume. Git-over-SSH uses :22, a separate port to add a Service for. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Gogs server. Data at /data. Git-over-SSH (:22) needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gogs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gogs" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gokapi
A Gokapi server (a self-hosted, lightweight file-sharing server with expiring links and a download limit, similar to the discontinued Firefox Send) on the official image; its database, configuration and (by default) stored files on a PersistentVolume under /app/data. A plain composable http workload. Uploaded files can instead go to S3 when the AWS_* settings are provided. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :53842.
server http
The Gokapi server. Data at /app/data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gokapi/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gokapi" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gonic
A gonic server (a music streaming server speaking the Subsonic API) on the official image. A plain composable http workload that keeps its SQLite database at /data and reads the music library, cache, podcasts and playlists from the same volume — no external database. The image listens on :80, so the stage moves the listener to :8080 (GONIC_LISTEN_ADDR) and runs as an ordinary user instead of granting NET_BIND_SERVICE; service links are off because gonic reads every setting from a GONIC_-prefixed variable. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The gonic server. Database at /data, music at /music (read-only), cache, podcasts and playlists beside them on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gonic/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gonic" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
port | int | — | 8080 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gotenberg
A Gotenberg server (a stateless API for converting HTML, Markdown and Office documents to PDF, powered by Chromium and LibreOffice) on the official image. Stateless: a plain rolling Deployment. The PDF-conversion companion apps like paperless-ngx and DocuSeal expect. Serves on :3000.
server http
The Gotenberg server. Stateless; usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/gotenberg/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gotenberg" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gotify
A Gotify server (a simple server for sending and receiving push notifications). A plain composable http workload that keeps its messages, apps, and clients in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app and API on :80.
server http
The Gotify server. Keeps everything in SQLite at /app/data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gotify/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gotify" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gotosocial
A GoToSocial server (a lightweight, self-hosted ActivityPub/Fediverse social server, an alternative to Mastodon that federates with it) on the official image; with the default SQLite backend its database and stored media live on a PersistentVolume. The host (part of every @handle) is fixed at first run and cannot be changed later. Point it at an external PostgreSQL (GTS_DB_TYPE=postgres + GTS_DB_*) to scale past SQLite. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The GoToSocial server. host is the permanent instance domain. Data at /gotosocial/storage. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gotosocial/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gotosocial" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
host | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gpustack
A GPUStack server (the control plane of a GPU cluster: the model catalogue, the scheduler that places inference on workers, and an OpenAI-compatible API in front of them) on the project's own image. THE SERVER NEEDS NO GPU AND THE WORKERS ARE NOT THIS — upstream starts a worker with --privileged, the host network, the host Docker socket and the NVIDIA runtime, which is a node agent rather than a tenant deployment, so only the server is packaged. The first start writes an initial admin password and a worker join token under /var/lib/gpustack, which a deployment must read out of the volume to use. A Service named after the workload makes Kubernetes inject GPUSTACK_PORT as a tcp:// URL, which the entrypoint reads as its --port flag and rejects as an integer, so service links are off. The image is an s6-overlay supervision tree that raises its own file-descriptor limit and writes runtime configuration under /run, so root, privilege escalation, capabilities and a writable root filesystem are relaxed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The GPUStack server. storageSize sizes the volume holding the database and the model metadata. extraArgs is appended to the entrypoint verbatim. The supervision tree takes its time coming up, so the wait is a startup probe. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/gpustack/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gpustack" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
grav
A Grav server (a modern, flat-file CMS: fast, database-free content management with a Markdown-driven admin) on the LinuxServer.io image; because Grav is flat-file, its whole site (content, config, plugins, cache) lives on a PersistentVolume — no external database. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Grav server. puid/pgid own the mounted files; timezone sets TZ. Site at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/grav/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "grav" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
gravity
A Gravity server (replicated DNS, DHCP and TFTP for a local network, backed by an embedded etcd, with a web interface for the zones, records and leases). A composable stateful workload, because each member is an etcd peer with a volume that follows its identity. IT RUNS ON THE NODE'S NETWORK AND IT HAS TO: DHCP is answered from broadcast traffic that never reaches a pod behind a Service, which is why upstream's own compose file uses host networking. Two consequences — the ports it opens ARE the node's ports, so two members cannot share a node, and nothing about it is isolated by a Service. Serving :53 and :67 is the privilege, granted as NET_BIND_SERVICE by name rather than by running as root, so the rest of the hardened posture stands. DNS is useful alone; DHCP wants to be the only server on its segment, which is a fact about the network rather than about this workload, so it is off by default. Serves the web interface on :8008.
server stateful
The Gravity server. replicas are etcd peers (odd numbers). dhcp and tftp turn on the services that are off by default — a DHCP server turned on by accident breaks a network rather than this workload. Its data lives at /data on the volume. Compose an anti-affinity rule, because host networking means two members cannot share a node.
Import: import 'github.com/metio/kurly/workloads/gravity/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "gravity" |
image | string | — | — |
replicas | int | — | 1 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dhcp | bool | — | false |
tftp | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
greenlight
A Greenlight 3 server (the official BigBlueButton front-end: a Rails app for scheduling and joining BBB rooms and meetings) on the official image, backed by an external PostgreSQL and Redis. It reaches an existing BigBlueButton server over the network; kurly does not run BBB itself. Pairs with a cnpg-cluster named greenlight-db and a Redis. kurly authors no Secret; DATABASE_URL, REDIS_URL, SECRET_KEY_BASE and the BIGBLUEBUTTON_* endpoint/secret come from a provided Secret via envFrom. Stateless (recordings live on the BBB server): a plain rolling Deployment. Serves on :3000.
server http
The Greenlight server. secretName holds DATABASE_URL, REDIS_URL, SECRET_KEY_BASE and the BIGBLUEBUTTON_ENDPOINT/BIGBLUEBUTTON_SECRET (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/greenlight/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "greenlight" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "greenlight" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
grocy
A Grocy server — a self-hosted groceries and household management tool: stock, shopping lists, chores and recipes. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Grocy server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/grocy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "grocy" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
guacamole
An Apache Guacamole server (a clientless remote-desktop gateway for RDP/VNC/SSH from a browser) on the official image, backed by an external PostgreSQL or MySQL. Guacamole is two processes — the web app and the guacd proxy daemon — so this runs guacd as a SIDECAR in the same pod (reached on localhost:4822). kurly authors no Secret; the database connection (POSTGRESQL_*/MYSQL_*) comes from a provided Secret via envFrom, and the schema must be initialised. Pairs with a cnpg-cluster named guacamole-db. Stateless: a plain rolling Deployment. Serves on :8080.
server http
The Guacamole server plus its guacd sidecar. guacdImage sets the sidecar image; secretName holds the database connection (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/guacamole/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "guacamole" |
image | string | — | — |
guacdImage | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "guacamole" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
habitat
A Habitat server (a message board for one place: residents post about their neighbourhood, and a post can be pinned to a location so people find the conversations happening nearby). A composable http workload — a Symfony application served by FrankenPHP — backed by an external PostgreSQL, with images attached to posts on a PersistentVolume. Caddy is told to serve plain HTTP on an unprivileged port, since a SERVER_NAME carrying a hostname makes it obtain its own certificate; TLS belongs to the exposure. The entrypoint waits for the database and runs the Doctrine migrations before it serves, so a first start against an empty database takes minutes and is covered by a startup probe rather than a longer liveness delay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Habitat server. Images attached to posts live at /uploads on the volume, by a path the application hard-codes; everything else is in PostgreSQL. secretName holds DATABASE_URL (the password is embedded in it), APP_SECRET, ENCRYPTION_KEY and the two Mercure JWT keys — all of them ship with published placeholders upstream, and ENCRYPTION_KEY is what the stored instance settings are encrypted with, so a later value makes them unreadable. url is the address the instance is reached at, which Symfony generates links from wherever there is no request to derive them from. Probed by connection, because the front page redirects to sign-in. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/habitat/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "habitat" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
port | int | — | 8080 |
url | string | — | — |
secretName | string | — | "habitat" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
halo
A Halo server (a website and blog publishing platform with a plugin and theme system). A plain composable http workload keeping its work directory — the embedded H2 database, uploaded attachments, themes and plugins — on a PersistentVolume, so it deploys with nothing else in the namespace; point the SPRING_R2DBC_* settings at PostgreSQL for anything larger. The initial administrator password has a PUBLISHED default upstream, so the Secret is the difference between an account you own and one anybody can log into, and links are built from an external URL that is wrong until it is set. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8090.
server http
The Halo server. The work directory lives at /halo2 on the volume, moved off the image default /root/.halo2 so an unprivileged process can write it, with Spring's optional config location following it. externalUrl sets the URL links and stored attachment paths are built from — set it before publishing anything, since correcting it later does not rewrite what was stored. secretName holds HALO_SECURITY_INITIALIZER_SUPERADMINPASSWORD, and any database credentials when this is pointed away from H2. The first boot initialises the database and unpacks the bundled plugins, so the startup budget is a startup probe rather than a slack liveness delay. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/halo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "halo" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
externalUrl | string | — | — |
secretName | string | — | "halo" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
harbor
A Harbor registry as its four processes, one stage each: core (the API and token service), portal (the web UI), registry (the upstream distribution server plus its registryctl sidecar) and jobservice (garbage collection, replication, retention, webhooks). All four read the same consumer-provided Secret and point at the same PostgreSQL and Redis — core on Redis DB 0, jobservice on 1, the registry on 2 — so a default deployment needs no wiring; pairs with the cnpg-cluster and valkey workloads. Only core faces outside: it answers the API, the token service and every registry path a client uses, and forwards UI requests to the portal Service, so one exposure carries all of Harbor and externalUrl must be that same address (the token realm a docker login follows is built from it). registry and jobservice each own a ReadWriteOnce volume and are one replica, recreated; pointing registry at object storage makes it stateless. Two Secret values cannot be generated: the registry htpasswd line and the token service CA keypair. Image scanning stays off — Harbor scans through a separate Trivy adapter these stages do not carry.
core http
The Harbor API and token service: projects, users, robot accounts, replication and the bearer tokens a client trades its credentials for. externalUrl is the address clients reach Harbor at — the token realm a docker login follows is built from it, so a wrong value fails only at login time. portalName/registryName/jobserviceName address the other three stages in-cluster; dbHost/dbName/dbUser default to a cnpg-cluster named harbor-db holding the registry database, redisHost to a valkey named harbor-cache (core on Redis DB 0). secretName is the shared Secret; kurly authors none. Stateless — everything is in PostgreSQL and Redis. Compose the exposure onto this stage.
Import: import 'github.com/metio/kurly/workloads/harbor/core.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "harbor-core" |
image | string | — | — |
replicas | int | — | 1 |
externalUrl | string | — | "https://harbor.example.com" |
portalName | string | — | "harbor-portal" |
registryName | string | — | "harbor-registry" |
jobserviceName | string | — | "harbor-jobservice" |
dbHost | string | — | "harbor-db-rw" |
dbPort | string | — | "5432" |
dbName | string | — | "registry" |
dbUser | string | — | "harbor" |
dbSslMode | string | — | "disable" |
redisHost | string | — | "harbor-cache" |
redisPort | string | — | "6379" |
registryUser | string | — | "harbor_registry_user" |
secretName | string | — | "harbor" |
logLevel | string | — | "info" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
jobservice http
The Harbor worker: garbage collection, replication runs, retention, scans and webhook delivery. It takes its work from the Redis core uses (DB 1) and core calls it back on :8080, which is why it is an http stage rather than a worker. Reads the same Secret as core — CORE_SECRET and JOBSERVICE_SECRET are how the two authenticate to each other. jobLogs=file writes a job log to a ReadWriteOnce volume the UI reads back, so the stage is one replica, recreated; jobLogs=database keeps the logs in PostgreSQL and claims no volume.
Import: import 'github.com/metio/kurly/workloads/harbor/jobservice.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "harbor-jobservice" |
image | string | — | — |
coreName | string | — | "harbor-core" |
registryName | string | — | "harbor-registry" |
redisHost | string | — | "harbor-cache" |
redisPort | string | — | "6379" |
registryUser | string | — | "harbor_registry_user" |
secretName | string | — | "harbor" |
jobLogs | string | — | "file" |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
maxJobWorkers | int | — | 10 |
logLevel | string | — | "info" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
portal http
The Harbor web UI: an nginx serving the compiled Angular application, with no state and nothing to talk to. Its nginx configuration comes from a ConfigMap rather than the image so every temporary path lands under /tmp and the root filesystem stays read-only. Reached through core, which forwards the UI requests it does not answer itself — do not expose it separately. Scales horizontally.
Import: import 'github.com/metio/kurly/workloads/harbor/portal.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "harbor-portal" |
image | string | — | — |
replicas | int | — | 1 |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
registry http
The OCI registry Harbor puts its API in front of: the upstream distribution server on :5000 plus registryctl, the controller core drives for garbage collection and blob deletion, on :8080 as a sidecar in the same pod (the two share the image data, so a second pod could not mount the volume). Images land on a ReadWriteOnce volume by default — one replica, recreated — and passing a distribution storage stanza points them at object storage instead, which makes the stage stateless. REGISTRY_HTTP_SECRET signs the upload state a client carries between requests; REGISTRY_HTPASSWD is the bcrypt line for the basic-auth user core authenticates with, so it must match core registryUser and REGISTRY_CREDENTIAL_PASSWORD. Nothing outside the cluster talks to this stage — do not compose an exposure onto it.
Import: import 'github.com/metio/kurly/workloads/harbor/registry.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "harbor-registry" |
image | string | — | — |
controllerImage | string | — | "docker.io/goharbor/harbor-registryctl:v2.15.0@sha256:463172f71d3a1e8d4f9e3b4e687a447f41fbc3126316d8c150dba04a903bbc47" |
redisHost | string | — | "harbor-cache" |
redisPort | string | — | "6379" |
secretName | string | — | "harbor" |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
storage | object | — | — |
logLevel | string | — | "info" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
controllerResources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
hatsu
A Hatsu server (a bridge that speaks ActivityPub on behalf of a static site: it turns the site's JSON feed into a Fediverse actor, accepts follows, pushes new posts to the followers and collects the replies). A plain composable http workload keeping its SQLite database on a PersistentVolume. domain and primaryAccount are REQUIRED — Hatsu panics on either being unset, so a pod without them crash-loops by design — and creating the primary account reads that site's feed over the internet on first start, so a NetworkPolicy that forgets the egress stops it STARTING rather than merely federating. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3939.
server http
The Hatsu server. SQLite at /data on the volume, named absolutely and with ?mode=rwc so it is created on an empty one. domain is the domain in every @handle this instance mints and primaryAccount the static site being bridged; both are unset by default because neither has a sane one. secretName holds HATSU_ACCESS_TOKEN, which is what makes the admin API exist at all — without it the instance is limited to the primary account. The startup probe is generous: the migrations and the first feed fetch both happen before anything listens. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hatsu/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hatsu" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
domain | string | — | — |
primaryAccount | string | — | — |
secretName | string | — | "hatsu" |
logLevel | string | — | "info" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
hedgedoc
A HedgeDoc server (real-time, collaborative markdown notes) on the official image, backed by an external PostgreSQL, with uploaded files on a PersistentVolume. Pairs with a cnpg-cluster named hedgedoc-db. kurly authors no Secret; CMD_DB_URL and CMD_SESSION_SECRET come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The HedgeDoc server. domain is the public domain. secretName holds CMD_DB_URL (with the DB password) and CMD_SESSION_SECRET (envFrom). Uploads at /hedgedoc/public/uploads. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hedgedoc/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hedgedoc" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
domain | string | — | — |
secretName | string | — | "hedgedoc" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
heimdall
A Heimdall server — an elegant dashboard and application launcher for your self-hosted services. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Heimdall server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/heimdall/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "heimdall" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
hister
A Hister server (a personal web search engine indexing the sites you visit, with offline previews and optional semantic search) on the official image. A plain composable http workload — index, previews and config.yml share one PersistentVolume, so no external database is needed. Hister binds loopback by default, so the stage sets HISTER__SERVER__ADDRESS to 0.0.0.0 on the declared port; everything else is an HISTER__<SECTION>__<KEY> variable or an edit to /hister/data/config.yml. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4433.
server http
The Hister server. baseUrl is the public URL it builds links from behind a reverse proxy. Index, previews and config.yml at /hister/data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hister/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hister" |
image | string | — | — |
port | int | — | 4433 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
hollama
A Hollama server (a minimal, self-hosted web UI for Ollama and OpenAI-compatible LLMs) on the official image. A plain composable http workload. Sessions and settings are stored client-side, so the server holds no data — a plain, horizontally scalable Deployment. The browser talks to your Ollama / OpenAI endpoint directly. Serves on :4173.
server http
The Hollama server. Stateless; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hollama/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hollama" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
homarr
A Homarr server (a sleek, self-hosted dashboard for your homelab) on the official image; its SQLite database and config live on a PersistentVolume, so it needs no external database. kurly authors no Secret; SECRET_ENCRYPTION_KEY comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :7575.
server http
The Homarr server. secretName holds SECRET_ENCRYPTION_KEY (a 64-character hex string, envFrom). Data at /appdata. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/homarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "homarr" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "homarr" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
home-assistant
A Home Assistant server (Home Assistant Core: the leading self-hosted home-automation platform) on the official image; its configuration and state database live on a PersistentVolume. It runs as root with a writable root filesystem. Local-network discovery (mDNS/SSDP) does not work through a ClusterIP; devices reachable by IP/MQTT/cloud work, and USB radios need a network coordinator. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8123.
server http
The Home Assistant server. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/home-assistant/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "home-assistant" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
homebox
A Homebox server (a simple home/household inventory and asset manager). A plain composable http workload on the rootless image that keeps its inventory in SQLite and attachments on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :7745.
server http
The Homebox server. Keeps inventory and attachments at /data on the volume (HBOX_STORAGE_*), so it needs nothing external. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/homebox/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "homebox" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "homebox" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
homegallery
A HomeGallery server (a self-hosted photo and video gallery that indexes the folders the media already lives in and offers a timeline, similarity search and face detection) on the official image. Configuration, the database file and the generated preview storage all sit under /data on one PersistentVolume, together with the media itself under /data/Pictures, so it needs no external database — the image sets HOME, GALLERY_BASE_DIR and GALLERY_CACHE_DIR to /data and GALLERY_CONFIG_DIR to /data/config, and the configuration is written on first start when none exists. The entrypoint is the CLI and the image carries no default command, so the stage passes `run server` as args; without it the process prints usage and exits. `run server` refuses to start when the configuration file is missing and writes none itself, so an init container runs `run init --source …` on the empty volume, naming the sources to index; it is skipped once the file exists, since re-running it needs --force and would discard edited sources. Similarity search and face detection are computed by a separate embedding API, which apiServer points at. The server binds :3000 as the image own node user, so the hardened default posture stands with only /tmp writable for ffmpeg and vips temporary files. kurly authors no Secret and the server needs none. First start imports the whole library before it serves, so the startup probe carries that wait. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The HomeGallery server. storageSize sizes the single volume at /data (configuration, database, preview storage and the media library under /data/Pictures); sources names the media directories the init container writes into the configuration; apiServer points at the embedding API used for similarity search and face detection. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/homegallery/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "homegallery" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
sources | array | — | ["/data/Pictures"] |
apiServer | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
homepage
A Homepage server (a modern, fully static, highly-configurable application dashboard with service/bookmark widgets and live status) on the official image; its YAML configuration lives on a PersistentVolume, so it needs no external database. Recent releases refuse requests whose Host header is not in HOMEPAGE_ALLOWED_HOSTS. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Homepage server. allowedHosts sets HOMEPAGE_ALLOWED_HOSTS (comma-separated) — required by recent releases. Config at /app/config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/homepage/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "homepage" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
allowedHosts | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
homer
A Homer server (a simple, static dashboard for your self-hosted services) on the official image. A plain composable http workload — its configuration and custom assets live on a PersistentVolume, no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the dashboard on :8080.
server http
The Homer server. Configuration and custom assets live at /www/assets on the volume (edit config.yml there; the image seeds defaults via INIT_ASSETS). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/homer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "homer" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"64Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
hortusfox
A HortusFox server (collaborative plant management with watering schedules and a photo log) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with plant photos and attachments on PersistentVolumes. The Apache + PHP image starts as root, binds :80 and chowns the mounted directories before dropping to www-data, relaxing non-root and read-only-rootfs; logs, backups and migration state are written beside the code inside the image tree. kurly authors no Secret; DB_PASSWORD and APP_ADMIN_PASSWORD (the administrator seeded on first boot) come from a provided Secret via envFrom. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :80.
server http
The HortusFox server. dbHost/dbPort/database/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). Plant photos live at /var/www/html/public/img and attachments at /var/www/html/public/attachments, each on its own volume. secretName holds DB_PASSWORD and APP_ADMIN_PASSWORD, the password of the administrator account the first boot seeds. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hortusfox/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hortusfox" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
attachmentsStorageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "hortusfox-db" |
dbPort | int | — | 3306 |
database | string | — | "hortusfox" |
dbUser | string | — | "hortusfox" |
dbCharset | string | — | "utf8mb4" |
adminEmail | string | — | "admin@example.com" |
timezone | string | — | "UTC" |
secretName | string | — | "hortusfox" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
hypermind
A Hypermind server (a peer-to-peer deployment counter and ephemeral chat: every instance joins a Hyperswarm DHT, discovers the others running the same image and shows how many there are) on the project's own image. No database, no volume and no external service. IT DIALS STRANGERS ON THE PUBLIC INTERNET, which is the entire point: a cluster with a default-deny egress policy gives it nothing to count, and one without lets it hold connections to peers nobody vetted — both are honest outcomes rather than misconfiguration. Nothing is kept: messages last while a peer is connected and a restart starts over. Replicas are peers of one another rather than copies, so more of them raise the count instead of adding capacity. Upstream calls it a toy. Stateless: a plain rolling Deployment. Serves on :3000.
server http
The Hypermind server. Nothing to configure but the port and the replica count, and replicas are peers rather than copies. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/hypermind/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "hypermind" |
image | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
imgcompress
An imgcompress server (compresses, converts, resizes and batch-processes images through a web interface, including HEIC/WebP/PDF conversion and background removal that runs locally). A plain composable http workload that persists NOTHING: uploads and results live in a scratch volume the app sweeps an hour after writing them, so there is no PersistentVolume and nothing to back up. One replica, because a download link only resolves against the pod that produced it. It has no authentication of its own. Serves on :5000.
server http
The imgcompress server. Uploads and rendered results pass through the scratch volume at /tmp, sized by tempSize — the app accepts a 40 GiB upload by default, so that limit is what a single batch may cost the node. The image runs as its own unprivileged account with no shell; the root filesystem is writable only because the entrypoint writes the frontend runtime.json inside the image tree, at a path too long to name a volume after. Compose an exposure onto the HTTP port, and an authenticating proxy if it is reachable from the internet.
Import: import 'github.com/metio/kurly/workloads/imgcompress/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "imgcompress" |
image | string | — | — |
tempSize | quantity | — | "4Gi" |
env | object | — | — |
resources | object | — | {"limits":{"cpu":"2","memory":"2Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
imgproxy
An imgproxy server (fast, secure on-the-fly image processing). A plain composable http workload on the project's own image: it stores nothing, fetching each source image over HTTP or from object storage and streaming the result back, so it claims no volume and scales horizontally. secretName carries the IMGPROXY_KEY/IMGPROXY_SALT pair that makes imgproxy demand a signature on every URL; left unset it processes any URL it is handed, which is a bandwidth and SSRF liability on a publicly reachable instance. Serves on :8080.
server http
The imgproxy server. secretName holds IMGPROXY_KEY and IMGPROXY_SALT (both hex), read through envFrom — with them set imgproxy rejects unsigned URLs. Everything else imgproxy reads is IMGPROXY_*, through env. Stateless, so replicas scale freely. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/imgproxy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "imgproxy" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
immich
Immich — a self-hosted photo and video backup. The server keeps the media library on a PersistentVolume and reaches a PostgreSQL with the VectorChord extension plus a Redis; the machine-learning stage serves inference (smart search, faces) from a model cache. Compose both with a cnpg-cluster on a VectorChord image and a valkey.
machine-learning http
Immich's inference service on :3003, model cache on a ReadWriteOnce volume at /cache (one replica, recreated). The server reaches it through its Service at http://<name>:3003.
Import: import 'github.com/metio/kurly/workloads/immich/machine-learning.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "immich-machine-learning" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
server http
The Immich API and web app on :2283, media library on a ReadWriteOnce volume at /data (one replica, recreated). Reaches PostgreSQL/VectorChord and Redis by the given hosts; the password comes from `secretName` as DB_PASSWORD (an Immich-shaped Secret the operator supplies, e.g. via kurly.externalSecret from the CNPG cluster Secret).
Import: import 'github.com/metio/kurly/workloads/immich/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "immich" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
database | string | — | "immich" |
dbUser | string | — | "immich" |
dbHost | string | — | "immich-db-rw" |
redisHost | string | — | "immich-cache-headless" |
machineLearningUrl | string | — | "http://immich-machine-learning:3003" |
secretName | string | — | "immich" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
inspircd
An InspIRCd server (a modular IRC daemon) on the official image. A plain composable http workload that keeps its runtime data (logs, TLS material) on a PersistentVolume and reads its configuration from a mounted config. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves IRC-over-TLS on :6697; needs an inspircd.conf mounted at /inspircd/conf.
server http
The InspIRCd server. Keeps runtime data at /inspircd/data on the volume; mount its configuration at /inspircd/conf (kurly.config, or a Secret for oper/link credentials). Route the port as TCP.
Import: import 'github.com/metio/kurly/workloads/inspircd/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "inspircd" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
invidious
An Invidious server (a privacy-preserving alternative front end for YouTube) on the official image, backed by an external PostgreSQL. It keeps no state of its own and claims no volume, so any replica count is safe. Pairs with a cnpg-cluster named invidious-db. The image ships config/config.yml and overrides every key from an INVIDIOUS_<KEY> environment variable, so there is no config document to author; kurly authors no Secret, and INVIDIOUS_DATABASE_URL and INVIDIOUS_HMAC_KEY come from a provided Secret via envFrom. Service links are disabled, since an injected INVIDIOUS_PORT would be read as the listen port. Serves on :3000.
server http
The Invidious server. domain is the public domain the pages link to, externalPort the port those URLs carry (443 behind TLS). checkTables lets the server create and migrate its own schema on first start. secretName holds INVIDIOUS_DATABASE_URL and INVIDIOUS_HMAC_KEY (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/invidious/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "invidious" |
image | string | — | — |
domain | string | — | — |
externalPort | int | — | — |
checkTables | bool | — | true |
secretName | string | — | "invidious" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
invoiceshelf
An InvoiceShelf server (self-hosted invoicing and estimates for freelancers and small businesses; the maintained continuation of Crater). A plain composable http workload that keeps its SQLite database, uploads and PDF templates on a PersistentVolume — no external database. An init container seeds the volume from the image, because storage/ ships content a PersistentVolume would otherwise hide. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app on :8080.
server http
The InvoiceShelf server. Keeps the SQLite database, uploads and PDF templates at /var/www/html/storage on the volume; DB_CONNECTION and DB_DATABASE are set explicitly because the entrypoint and the framework otherwise disagree about where the database lives. appUrl is the public URL; secretName holds APP_KEY, which must be stable or every encrypted value becomes unreadable. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/invoiceshelf/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "invoiceshelf" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
secretName | string | — | "invoiceshelf" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
isso
An Isso server (a small comment server for static sites: a script tag on the page, comments in SQLite, no accounts and no third party). A plain composable http workload with a starter configuration whose `host` cannot be defaulted — Isso serves comments only for the origins listed there. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the API and widget on :8080.
server http
The Isso server. Comments live in SQLite at /db on the volume. host is the site whose pages may embed the widget and is required — Isso rejects requests from any other origin, so a wrong value gives a widget that loads and then refuses every comment. config replaces the starter isso.cfg wholesale; it mounts as a single file, so the rest of /config is untouched. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/isso/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "isso" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
host | string | yes | — |
config | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
it-tools
An IT-Tools server — a large collection of handy client-side online tools for developers and sysadmins. On the official image; stateless (a plain rolling Deployment). Serves on :80.
server http
The An IT-Tools server. Stateless. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/it-tools/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "it-tools" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jackett
A Jackett server — a proxy server that exposes many tracker indexers through one API. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9117.
server http
The A Jackett server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jackett/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jackett" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jaeger
A Jaeger server (distributed tracing: it receives spans over OTLP, stores them and serves the UI that reconstructs a request's path). A plain composable http workload running the Jaeger v2 binary with its built-in all-in-one configuration, so it is one container with no dependencies. TRACES ARE IN MEMORY AND DO NOT SURVIVE A RESTART — a rollout, an eviction or an OOM kill loses everything collected so far, which is a fair trade for development and a staging cluster and is not a trace archive; keeping traces means configuring a storage backend through config. One replica, because with the memory store each pod holds its own traces and a query answered by one cannot see spans the other received. Serves the UI on :16686 and accepts OTLP on :4317 and :4318.
server http
The Jaeger v2 all-in-one server: OTLP receivers, the span store and the UI in one process. config is a Jaeger v2 (OpenTelemetry Collector) configuration rendered as a ConfigMap and passed with --config — null leaves the built-in all-in-one configuration, whose store is in memory. The probes read the collector health endpoint on :13133, which is not published on the Service. Compose an exposure onto the UI port, and point instrumented workloads at the Service OTLP ports.
Import: import 'github.com/metio/kurly/workloads/jaeger/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jaeger" |
image | string | — | — |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jellyfin
A Jellyfin server — a free, self-hosted media server for streaming your movies, shows and music. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8096.
server http
The A Jellyfin server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jellyfin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jellyfin" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jellyseerr
A Jellyseerr server — a request-management and media-discovery companion for Jellyfin, Emby and Plex. On the official image; its SQLite configuration and database live on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5055.
server http
The A Jellyseerr server. Config at /app/config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jellyseerr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jellyseerr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jelu
A Jelu server (a self-hosted book tracker) on the official image. A plain composable http workload keeping its SQLite database, Lucene search index and uploaded files on a PersistentVolume — no external database. The search index is pointed off the read-only root filesystem through the environment, and service links are disabled so an injected JELU_PORT cannot be read as configuration. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :11111.
server http
The Jelu server. Database and log at /database, cover images and imports at /files, both on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jelu/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jelu" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jenkins
A Jenkins controller (the self-hosted automation server for building, testing and deploying software) on the official LTS image. A plain composable http workload that keeps JENKINS_HOME (jobs, plugins, config, build history) on a PersistentVolume. Single controller over a ReadWriteOnce volume: one replica, recreated. Inbound agents connect over the same HTTP port (websocket). Serves on :8080.
server http
The Jenkins controller. JENKINS_HOME at /var/jenkins_home on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jenkins/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jenkins" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
joomla
A Joomla server — a popular self-hosted CMS for building websites and online applications (backed by MySQL). On the official image, backed by an external database, with data on a PersistentVolume (the image populates it on first run). kurly authors no Secret; the DB credentials come from a provided Secret via envFrom. The Apache master runs as root then serves as www-data. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Joomla server. secretName holds the DB credentials (envFrom). Data at /var/www/html. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/joomla/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "joomla" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "joomla-db" |
dbName | string | — | "joomla" |
dbUser | string | — | "joomla" |
dbType | string | — | "mysqli" |
secretName | string | — | "joomla" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
joplin
A Joplin Server (the self-hosted sync target for the Joplin note-taking apps) on the official image, backed by an external PostgreSQL. kurly authors no Secret; the POSTGRES_* connection comes from a provided Secret via envFrom. Pairs with a cnpg-cluster named joplin-db. Stateless (notes live in PostgreSQL): a plain rolling Deployment. Serves on :22300.
server http
The Joplin Server. appBaseUrl is the public URL; secretName holds the POSTGRES_* connection (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/joplin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "joplin" |
image | string | — | — |
replicas | int | — | 2 |
appBaseUrl | string | — | — |
dbHost | string | — | "joplin-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "joplin" |
dbUser | string | — | "joplin" |
secretName | string | — | "joplin" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jotty
A jotty server (personal notes and checklists, stored as Markdown and JSON files rather than in a database, with rich text editing, task boards, sharing and PGP encryption). A plain composable http workload on TWO volumes: /app/data holds the notes, users, sessions and encryption keys, /app/config holds what an administrator customises from the settings page. The image ships defaults for the second which the mount hides, and jotty falls back to an empty configuration, so a fresh volume is an uncustomised instance rather than a broken one. Nothing external to provision. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :3000.
server http
The jotty server. Notes, checklists, users, sessions and encryption keys live at /app/data; themes, emojis and custom CSS at /app/config. The entrypoint only reaches for su-exec when it starts as root, so running as 1000:1000 — the account the image builds and chowns its own tree to — it execs the server directly and keeps the hardened posture. appUrl is absent by default: relative links work until the instance is exposed and a wrong absolute base is worse than none. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jotty/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jotty" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
configSize | quantity | — | "1Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
jupyterlab
A JupyterLab server (the web-based interactive development environment for notebooks, code and data) on the Jupyter project's own base-notebook image. A plain composable http workload — the workspace under /home/jovyan/work lives on a PersistentVolume, no external database. kurly authors no Secret; JUPYTER_TOKEN comes from a provided Secret via envFrom, and whoever holds it runs arbitrary code as this pod. Probes check the connection, since every path redirects or answers 403 without the token. The root filesystem is writable because the server keeps its runtime state and settings in its own home directory. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8888.
server http
The JupyterLab server. The workspace lives at /home/jovyan/work on the volume; the environment itself is the image, so a package installed from a notebook is gone at the next restart. secretName holds JUPYTER_TOKEN (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/jupyterlab/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "jupyterlab" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "jupyterlab" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
k8up
Backs up every PersistentVolume in a namespace on a schedule, as K8up Schedule and Restore custom resources — restic underneath, one resource per namespace rather than per claim, so a volume is protected the moment it appears rather than when somebody remembers to declare it (a claim annotated k8up.io/backup: "false" is left out). Requires the K8up operator. Authors the CRs directly like cnpg-cluster; composed by parameter, not by + feature. kurly authors no Secret: the repository password and the object-storage credentials come from a provided one.
backup k8up
A one-off Backup: one snapshot of every volume in the namespace, taken now rather than on a cron. Runs on apply and finishes, so a caller can gate on the outcome — it signals completion with status.finished and a Completed condition, never readiness. Tag it, or the snapshot can only be found again by timestamp.
Import: import 'github.com/metio/kurly/workloads/k8up/backup.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "backup" |
s3 | object | — | {"bucket":"backups","endpoint":"http://seaweedfs:8333"} |
repoPasswordSecret | string | — | "k8up-repository" |
repoPasswordKey | string | — | "password" |
tags | array | — | — |
activeDeadlineSeconds | int | — | — |
failedJobsHistoryLimit | int | — | 3 |
successfulJobsHistoryLimit | int | — | 1 |
promURL | string | — | — |
statsURL | string | — | — |
resources | object | — | — |
podSecurityContext | object | — | — |
podConfigRef | object | — | — |
volumes | array | — | — |
backend | object | — | — |
labels | object | — | — |
annotations | object | — | — |
restore k8up
A Restore: the half that proves the other half. Runs once when applied — into a claim (recovery) or to object storage as a tarball (inspection). K8up cannot stage one, so applying it is starting it.
Import: import 'github.com/metio/kurly/workloads/k8up/restore.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "restore" |
claim | string | — | "data" |
s3 | object | — | {"bucket":"backups","endpoint":"http://seaweedfs:8333"} |
repoPasswordSecret | string | — | "k8up-repository" |
repoPasswordKey | string | — | "password" |
snapshot | string | — | — |
restoreTo | object | — | — |
resources | object | — | — |
podSecurityContext | object | — | — |
backend | object | — | — |
restoreMethod | object | — | — |
labels | object | — | — |
annotations | object | — | — |
schedule k8up
A Schedule: the nightly backup, plus the prune that makes the retention policy real and the check that notices a repository has rotted before a restore does. Backends kurly does not model pass through verbatim via `backend`, which is merged last.
Import: import 'github.com/metio/kurly/workloads/k8up/schedule.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "backup" |
s3 | object | — | {"bucket":"backups","endpoint":"http://seaweedfs:8333"} |
repoPasswordSecret | string | — | "k8up-repository" |
repoPasswordKey | string | — | "password" |
backupSchedule | string | — | "0 2 * * *" |
pruneSchedule | string | — | "0 3 * * 0" |
checkSchedule | string | — | "0 4 * * 0" |
archiveSchedule | string | — | — |
keepDaily | int | — | 7 |
keepWeekly | int | — | 4 |
keepMonthly | int | — | 6 |
retention | object | — | — |
failedJobsHistoryLimit | int | — | 3 |
successfulJobsHistoryLimit | int | — | 1 |
resources | object | — | — |
podSecurityContext | object | — | — |
backend | object | — | — |
labels | object | — | — |
annotations | object | — | — |
kanboard
A Kanboard server (a minimalist kanban project-management board) on the official image. A plain composable http workload that keeps board data in SQLite and uploads on a PersistentVolume — no external database by default. The nginx + PHP-FPM image starts as root and binds :80, so it relaxes non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Kanboard server. Keeps board data and uploads at /var/www/app/data on the volume. Point DATABASE_URL (env) at external PostgreSQL to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kanboard/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kanboard" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kapowarr
A Kapowarr server (a manager for a comic book library: it tracks the volumes you follow, downloads missing issues, renames them into your naming scheme and converts them between formats). A plain composable http workload on TWO volumes, because a comic collection and a few megabytes of SQLite want different sizes: the database at /app/db and the library at libraryMountPath. The library still has to be registered as a root folder in the settings after the first start — kurly mounts it, Kapowarr does not find it. In-flight downloads land in an emptyDir at /app/temp_downloads and move into the library when they finish, so size downloadSizeLimit past the largest download you expect. Metadata comes from ComicVine and issues from the sources you configure, so the pod needs internet egress. The entrypoint drops privileges with gosu only when PUID is set to something other than 0; at the default it execs the app as whoever we already are, so this keeps the hardened posture. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :5656.
server http
The Kapowarr server. SQLite and settings at /app/db, the comic library at libraryMountPath — both PersistentVolumes, sized separately. downloadSizeLimit caps the emptyDir in-flight downloads occupy at /app/temp_downloads. Needs egress for metadata and downloads. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kapowarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kapowarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
libraryStorageSize | quantity | — | "100Gi" |
libraryMountPath | string | — | "/comics" |
storageClass | string | — | — |
downloadSizeLimit | quantity | — | "10Gi" |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
karakeep
A Karakeep server (a self-hosted "bookmark everything" app, formerly Hoarder: save links, notes and images and search them with AI tagging) on the official image; its SQLite database and stored assets live on a PersistentVolume. Expects two companion side services it does not bundle: a Meilisearch instance (MEILI_ADDR + MEILI_MASTER_KEY) and a headless Chrome (BROWSER_WEB_URL). kurly authors no Secret; NEXTAUTH_SECRET, MEILI_MASTER_KEY and AI provider keys come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Karakeep server. nextauthUrl is the public URL. meiliAddr/browserWebUrl point at the Meilisearch and headless-Chrome companions. secretName holds NEXTAUTH_SECRET, MEILI_MASTER_KEY and AI provider keys (envFrom). Database and assets at /data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/karakeep/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "karakeep" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
nextauthUrl | string | — | — |
meiliAddr | string | — | "http://meilisearch:7700" |
browserWebUrl | string | — | "http://chrome:9222" |
secretName | string | — | "karakeep" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
karaoke-eternal
A Karaoke Eternal Server (hosts a karaoke party: the room screen plays the songs while guests queue them from their own phone browser). A plain composable http workload keeping its SQLite database at /config and the song library at /mnt/karaoke, on two separate volumes — the library is sized and classed on its own because it outgrows the database by orders of magnitude and is often a ReadWriteMany claim filled by something else. The FIRST ACCOUNT REGISTERED BECOMES THE ADMINISTRATOR, so create it before publishing the address. The image declares no USER, so the run-as user is pinned to node's own account. Single writer over a ReadWriteOnce database volume: one replica, recreated. Serves on :8080.
server http
The Karaoke Eternal server. The SQLite database and the server's own state live at /config on one volume; the song library lives at /mnt/karaoke on another, which is the path to name when adding a media folder in the app. Set KES_SCAN=1 in env to run the media scanner at startup. The first account registered becomes the administrator. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/karaoke-eternal/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "karaoke-eternal" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
mediaSize | quantity | — | "50Gi" |
mediaStorageClass | string | — | — |
mediaAccessModes | array | — | ["ReadWriteOnce"] |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kavita
A Kavita server (a fast, cross-platform reading server for comics, manga, and ebooks) on the official image. A plain composable http workload that keeps its database on a PersistentVolume and serves a library from /library on the same volume — no external database. The .NET app writes temp files to the rootfs, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5000.
server http
The Kavita server. Database and settings at /kavita/config, library at /library, both on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kavita/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kavita" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
keycloak
A Keycloak identity server as an official keycloak-operator `Keycloak` custom resource. Authors the CR (like loki and tempo) for the operator to reconcile into a StatefulSet, Services, and the admin credentials Secret. Requires the keycloak-operator (whose recent releases let one operator manage instances across many namespaces) and a PostgreSQL database — pairs with the cnpg-cluster workload.
server keycloak
The Keycloak server. It needs a PostgreSQL database: dbHost/dbName/dbSecret default to a cnpg-cluster named keycloak-db (its -rw Service and the -app Secret CNPG mints, keys username/password). hostname is the public URL for production; tlsSecret names the cert Keycloak terminates, or plain HTTP (httpEnabled) behind a TLS-terminating proxy. The operator chooses the Keycloak image unless image pins one. kurly authors no Secret — the database and TLS Secrets are the consumer's.
Import: import 'github.com/metio/kurly/workloads/keycloak/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "keycloak" |
instances | int | — | 1 |
image | string | — | — |
dbHost | string | — | "keycloak-db-rw" |
dbName | string | — | "keycloak" |
dbSecret | string | — | "keycloak-db-app" |
hostname | hostname | — | — |
tlsSecret | string | — | — |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
khoj
A Khoj server (searches your own documents and the web and answers questions from them). A composable http workload backed by an external PostgreSQL that must carry the PGVECTOR extension — the embeddings are stored and queried as vectors, so a plain PostgreSQL starts and then fails every search. Two volumes: /root/.khoj for the configuration and the index, /root/.cache for the embedding models downloaded on first start, which is not state you must keep but on an emptyDir is re-downloaded in full at every restart. Django validates the Host header, so the probes are CONNECTION probes and KHOJ_DOMAIN must name the host an exposure serves before a login through it works. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :42110.
server http
The Khoj server. Configuration and index at /root/.khoj, the embedding models at /root/.cache. secretName holds POSTGRES_PASSWORD, KHOJ_DJANGO_SECRET_KEY, KHOJ_ADMIN_EMAIL and KHOJ_ADMIN_PASSWORD — the Django key signs sessions and ships with a published default upstream, the admin pair creates the first superuser. Set domain to the host an exposure serves, or Django rejects the login. anonymousMode serves without accounts, for an instance behind its own authentication. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/khoj/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "khoj" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
cacheSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "khoj-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "khoj" |
dbUser | string | — | "khoj" |
domain | string | — | — |
secretName | string | — | "khoj" |
anonymousMode | bool | — | false |
args | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"6Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
kimai
A Kimai server (a self-hosted, professional time-tracking application for freelancers and teams) on the official Apache image, backed by an external MySQL/MariaDB. kurly authors no Secret; DATABASE_URL and APP_SECRET come from a provided Secret via envFrom. Pairs with a mysql-cluster named kimai-db. Stateless (timesheets live in MySQL): a plain rolling Deployment. Serves on :8001.
server http
The Kimai server. secretName holds DATABASE_URL and APP_SECRET (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kimai/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kimai" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "kimai" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kite
A Kite dashboard (live resource views, logs, exec, and multi-cluster management). A composable http workload on the project's own image, holding its users, sessions and audit log in a SQLite database on a PersistentVolume. IT READS EVERYTHING IN THE CLUSTER, which is the point and the risk: the grant is get/list/watch on every resource in every API group — Secrets in every namespace included, since Kubernetes cannot say 'every kind except that one' — so anyone who reaches this page sees what the cluster holds and it belongs behind authentication. `*` rather than a list of groups, because a dashboard that enumerates them stops showing a CRD the day somebody installs one, and invisible gaps are worse than an honest broad grant. WRITES ARE NOT GRANTED: extraRules is where a deployment that wants an editing dashboard says so. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Kite dashboard. namespace is where its ServiceAccount lives and is required, because a ClusterRoleBinding naming no namespace grants nothing. extraRules are ADDED to the read-only grant. prometheusUrl fills the resource graphs; without it the dashboard works and the charts stay empty. The SQLite database lives at /data on the volume. Compose an exposure onto the HTTP port, behind authentication.
Import: import 'github.com/metio/kurly/workloads/kite/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kite" |
image | string | — | — |
namespace | string | — | "kite" |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
extraRules | array | — | — |
prometheusUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
koillection
A Koillection server (a collection manager: whatever you collect, described with the fields you decide it has, plus wishlists, loans and a tag index). A composable http workload backed by an external PostgreSQL, with uploaded images on a PersistentVolume. The entrypoint INVENTS APP_SECRET and JWT_PASSPHRASE when they are unset, which reads as a safe default and is not: a fresh value on every start invalidates every session and API token whenever the pod moves, so supplying the Secret is what makes logins survive a reschedule. The entrypoint also runs usermod and chowns the volume from root and nginx binds :80, so this workload is deliberately less hardened. The first start migrates the database and generates a JWT keypair before anything answers. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Koillection server: nginx and php-fpm in one pod. Uploaded images live at /uploads on the volume; everything else is in PostgreSQL. dbVersion is the server version Doctrine generates SQL for. secretName holds DB_PASSWORD, APP_SECRET and JWT_PASSPHRASE — the last two are generated at random on every start when absent, so omitting them logs everybody out at each reschedule. httpsEnabled sets session.cookie_secure, which is right behind TLS and discards the session cookie in front of plain HTTP. Probed by connection, because / redirects to the login page. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/koillection/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "koillection" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "koillection-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "koillection" |
dbUser | string | — | "koillection" |
dbVersion | string | — | "16" |
corsAllowOrigin | string | — | "^https?://(localhost|127[.]0[.]0[.]1)(:[0-9]+)?$" |
uploadMaxFilesize | string | — | "100M" |
httpsEnabled | bool | — | true |
secretName | string | — | "koillection" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
komga
A Komga server (a media server for comics, manga, and digital books) on the official image. A plain composable http workload that keeps its database on a PersistentVolume and serves a library from /books on the same volume — no external database. The Java app writes temp files to the rootfs, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :25600.
server http
The Komga server. Database at /config, library at /books, both on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/komga/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "komga" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
krakend
A KrakenD gateway (a stateless API gateway that composes several backend calls into one endpoint and applies rate limiting, authentication and response filtering along the way). A plain composable http workload on the project's own image: the whole gateway is its configuration, rendered as a ConfigMap, so it keeps nothing and replicas scale freely. THE CONFIGURATION IS READ ONCE, AT STARTUP — KrakenD compiles its routes when the process starts and never re-reads the file, so a changed ConfigMap does nothing until the pods restart and the old routes keep serving. Serves on :8080.
server http
The KrakenD gateway. endpoints are KrakenD endpoint definitions, passed through verbatim; timeout is how long it waits on a backend; config merges over the rendered krakend.json for any of its other settings, including the extra_config middleware. Roll the Deployment after changing any of them — the configuration is only read at startup. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/krakend/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "krakend" |
image | string | — | — |
replicas | int | — | 2 |
endpoints | array | — | — |
timeout | string | — | "3s" |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kresus
A Kresus server (a personal finance manager: it aggregates bank accounts through woob, categorises what arrives and budgets against it). A composable http workload backed by an external PostgreSQL, with woob's downloaded bank modules and Kresus' data directory on a PersistentVolume. SQLite is deliberately not offered: upstream discourages it because it cannot carry the migrations across an upgrade. The image's entrypoint reaches the internet at EVERY start to pip-install woob and exits the container when that fails, so a cluster without egress to PyPI cannot start this pod; the entrypoint's second half, a yarn upgrade past the pinned tag, is switched off so the image runs the version it names. It starts from root with a writable root filesystem because that entrypoint renumbers an account, chowns the home directory and drops privileges with su. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9876.
server http
The Kresus server. Its data directory — woob's downloaded bank modules, the sources list and the log — is at /home/user/data on the volume; everything else is in PostgreSQL. secretName holds KRESUS_DB_PASSWORD and KRESUS_SALT, and the salt encrypts exports, so an export taken under one salt cannot be read back under another. Installing woob and migrating a fresh database is minutes rather than seconds, which is why the budget is a startup probe. Probed by connection. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kresus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kresus" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
dbHost | string | — | "kresus-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "kresus" |
dbUser | string | — | "kresus" |
secretName | string | — | "kresus" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kubetail
A Kubetail deployment in three stages: dashboard (the browser front end), cluster-api (what it asks), and cluster-agent (a DaemonSet reading log files off each node). DEPLOY ALL THREE — a dashboard alone lists workloads and shows no logs, which looks like a broken cluster rather than a missing component. The grants are split on purpose: the dashboard reads only what workloads EXIST, while pods/log — every line every workload prints — belongs to cluster-api, so the dashboard can be exposed while that one stays inside. Whoever reaches the dashboard reads whatever the cluster prints, which routinely includes tokens and personal data nobody meant to publish, so put authentication in front of it. The agent reads the node's log directory READ-ONLY, because one that could write those files could alter the record it exists to serve; reading them directly rather than through the apiserver is what makes following many pods at once cheap, and the cost is a DaemonSet with a hostPath.
cluster-agent daemon
The per-node agent, speaking gRPC on :50051. A DaemonSet publishes no Service of its own and the cluster-api needs to reach EVERY agent rather than one, so a headless Service resolving to all their addresses is rendered beside it. /var/log is mounted read-only.
Import: import 'github.com/metio/kurly/workloads/kubetail/cluster-agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kubetail-cluster-agent" |
image | string | — | — |
port | int | — | 50051 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
cluster-api http
The middle piece: the dashboard asks it for logs and it fans out to the agents through their headless Service. This is the stage holding pods/log across the cluster, and it is internal — nothing outside needs to reach it.
Import: import 'github.com/metio/kurly/workloads/kubetail/cluster-api.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kubetail-cluster-api" |
image | string | — | — |
namespace | string | — | "kubetail" |
replicas | int | — | 1 |
clusterAgentHost | string | — | "kubetail-cluster-agent-headless" |
clusterAgentPort | int | — | 50051 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
dashboard http
The Kubetail web interface. clusterApiHost/clusterApiPort point at the cluster-api stage. namespace is where the ServiceAccount lives and is required. Its grant covers what workloads exist, not their log content. Compose an exposure onto the HTTP port, behind authentication.
Import: import 'github.com/metio/kurly/workloads/kubetail/dashboard.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kubetail-dashboard" |
image | string | — | — |
namespace | string | — | "kubetail" |
replicas | int | — | 1 |
clusterApiHost | string | — | "kubetail-cluster-api" |
clusterApiPort | int | — | 8080 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kutt
A Kutt server (a free, self-hosted modern URL shortener with custom domains, link analytics and an API) on the official image, backed by an external PostgreSQL and Redis. kurly authors no Secret; the PostgreSQL/Redis connection, JWT_SECRET and DEFAULT_DOMAIN come from a provided Secret via envFrom. Pairs with a cnpg-cluster named kutt-db and a Redis. Stateless: a plain rolling Deployment. Serves on :3000.
server http
The Kutt server. secretName holds the PostgreSQL/Redis connection, JWT_SECRET and DEFAULT_DOMAIN (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kutt/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kutt" |
image | string | — | — |
replicas | int | — | 2 |
dbHost | string | — | "kutt-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "kutt" |
dbUser | string | — | "kutt" |
redisHost | string | — | "kutt-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "kutt" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
kyoo
Kyoo's API (a media browser and streaming server for films and television) on the official image, backed by an external PostgreSQL, with downloaded artwork on a PersistentVolume. Pairs with a cnpg-cluster named kyoo-db; the database needs the pg_trgm extension a stock PostgreSQL ships, and the API creates its own kyoo schema on first start. kurly authors no Secret; PGPASSWORD comes from a provided Secret via envFrom. Kyoo's front, auth and transcoder services are separate images: point authServer at the auth service and publicUrl at the issuer it stamps. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3567.
server http
The Kyoo API. publicUrl is the public URL and the JWT issuer; authServer points at the Kyoo auth service whose JWKS verifies the tokens. dbHost/database/dbUser are the connection settings, secretName holds PGPASSWORD (envFrom). Artwork at /images. The first start runs the migrations, which the startup probe waits out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/kyoo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "kyoo" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
publicUrl | string | — | — |
authServer | string | — | "http://kyoo-auth:4568" |
dbHost | string | — | "kyoo-db-rw" |
database | string | — | "kyoo" |
dbUser | string | — | "kyoo" |
secretName | string | — | "kyoo" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lancache
A LanCache monolithic server (a caching proxy for game downloads on a local network) on the official image, keeping its cache and logs on PersistentVolumes. It caches on :80 and forwards TLS untouched through an SNI proxy on :443, with the image metrics vhost on :8080. DNS is the half it does NOT do: a client only reaches the cache if a resolver points the content-delivery hostname at this Service, so run blocky, pihole or dnsmasq with the uklans cache-domains list beside it or the workload runs healthy and caches nothing. cacheDiskSize is what nginx believes it may fill, a separate number from the volume and in nginx units, so leave headroom. The entrypoint generates its nginx configuration at boot and drops workers to www-data, so it runs as root with a writable root filesystem; it also refreshes the cache-domains list over git and fetches every miss from the upstream CDN, so it needs egress. kurly authors no Secret; it holds no credential. Single writer on a ReadWriteOnce cache volume: one replica, recreated.
server http
The LanCache monolithic server, caching on :80 with the SNI proxy on :443. cacheDiskSize/minFreeDisk are what nginx may fill, in nginx units, and must leave headroom below cacheSize. upstreamDns resolves the misses. Needs a resolver pointing clients at it, and egress to the CDNs.
Import: import 'github.com/metio/kurly/workloads/lancache/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lancache" |
image | string | — | — |
cacheSize | string | — | "50Gi" |
cacheDiskSize | string | — | "45g" |
minFreeDisk | string | — | "5g" |
cacheMaxAge | string | — | "3560d" |
cacheSliceSize | string | — | "1m" |
logsSize | string | — | "2Gi" |
storageClass | string | — | — |
upstreamDns | string | — | "8.8.8.8 8.8.4.4" |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
leantime
A Leantime server (a self-hosted, open-source project-management system for non-project-managers: goals, ideas, tasks, time tracking) on the official image, backed by an external MySQL/MariaDB; uploaded files on a PersistentVolume under /var/www/html/userfiles. A plain composable http workload. kurly authors no Secret; LEAN_DB_* and LEAN_SESSION_PASSWORD come from a provided Secret via envFrom. Pair it with a MySQL/MariaDB you run separately. Single writer over a ReadWriteOnce uploads volume: one replica, recreated. Serves on :8080.
server http
The Leantime server. Uploads at /var/www/html/userfiles; provide the Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/leantime/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "leantime" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "leantime" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ledgersmb
A LedgerSMB server (double-entry accounting and ERP: receivables, payables, the general ledger, invoicing, inventory and fixed assets, as a Perl/starman web application). A plain composable http workload backed by an external PostgreSQL and claiming no PersistentVolume — every company's books, documents and attachments live in the database. It has no application-level database credentials, so it reads no Secret at all: an operator visits /setup.pl, logs in with a PostgreSQL SUPERUSER to create each company database, and users afterwards log in with their own PostgreSQL roles. The image already runs as www-data with a read-only root filesystem; two scratch volumes carry what it genuinely writes, /tmp for the generated ledgersmb.conf and the LaTeX document path, and /srv/ledgersmb/local for the application's own tree. Probed by connection, because an instance with no company database yet redirects or answers 401 on every path. Service links are disabled: a Service named postgres would inject POSTGRES_PORT as a tcp:// URL, which the entrypoint copies into the database port of the generated configuration. Serves on :5762.
server http
The LedgerSMB server. dbHost/dbPort are the only database configuration there is — LedgerSMB authenticates users as PostgreSQL roles, so no credential is passed to the pod and no Secret is read. database is the company database offered on the login screen; create it first through /setup.pl with a PostgreSQL superuser. proxyIp is which client addresses may set X-Forwarded-For, defaulting to the private space a pod network is carved out of because the image ships a Docker bridge range no cluster resolves. workers is the number of starman workers, i.e. concurrent requests one pod serves. Compose an exposure onto the HTTP port, and keep /setup.pl no wider than you must.
Import: import 'github.com/metio/kurly/workloads/ledgersmb/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ledgersmb" |
image | string | — | — |
dbHost | string | — | "ledgersmb-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "lsmb" |
proxyIp | string | — | "10.0.0.0/8" |
workers | int | — | 5 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lemmy
Lemmy (a self-hosted, open-source link aggregator and forum for the Fediverse, a Reddit alternative) as three workloads backed by an external PostgreSQL: a backend (API + federation on :8536), a ui (web frontend on :1234), and pictrs (image storage on :8080). The backend reads its config (PostgreSQL connection and pict-rs API key) from /config/config.hjson, mounted from an existing Secret you provide; kurly never mints key material. pictrs stores images on a PersistentVolume. Pairs with a cnpg-cluster named lemmy-db.
backend http
The Lemmy backend (API + federation) on :8536. configSecret is an existing Secret holding config.hjson, mounted at /config. Reached in-cluster by the ui.
Import: import 'github.com/metio/kurly/workloads/lemmy/backend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lemmy" |
image | string | — | — |
replicas | int | — | 1 |
configSecret | string | — | "lemmy" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pictrs http
The pict-rs image server on :8080. secretName holds PICTRS__SERVER__API_KEY (envFrom). Images at /mnt. Reached in-cluster by the backend.
Import: import 'github.com/metio/kurly/workloads/lemmy/pictrs.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pictrs" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "lemmy-pictrs" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ui http
The Lemmy web frontend (user-facing) on :1234. internalHost reaches the backend by Service name; externalHost is the public domain. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lemmy/ui.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lemmy-ui" |
image | string | — | — |
replicas | int | — | 2 |
internalHost | string | — | "lemmy:8536" |
externalHost | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
librebooking
A LibreBooking server (resource and room scheduling: who booked which room, meeting space or piece of equipment, and when). A composable http workload backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with the generated configuration and the uploaded images and attachments on PersistentVolumes. The schema is created by the application's OWN installer at /Web/install/, guarded by LB_INSTALL_PASSWORD, so a fresh deployment is not finished when the pod is Ready. Apache listens on 8080 as www-data, and the entrypoint links the configuration and every plugin's config back into the install tree, so the root filesystem is writable while non-root, dropped capabilities and no privilege escalation stand. scriptUrl has no sane default: every link and every e-mail is built against it. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :8080.
server http
The LibreBooking server. Configuration at /config and uploads at /var/www/html/Web/uploads, each on its own volume. dbHost/database/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). secretName holds LB_DATABASE_PASSWORD and LB_INSTALL_PASSWORD (envFrom) — the install password guards the installer that creates and upgrades the schema, so it is as load-bearing as the database credential. scriptUrl is the absolute URL of the Web directory. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/librebooking/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "librebooking" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
uploadsSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "librebooking-db" |
database | string | — | "librebooking" |
dbUser | string | — | "librebooking" |
scriptUrl | string | — | — |
timezone | string | — | "UTC" |
logLevel | string | — | "none" |
secretName | string | — | "librebooking" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
librechat
A LibreChat server (one chat interface in front of many model providers, keeping the conversations, presets and uploaded files that go with them) on the official image, backed by an external MongoDB. Pairs with a mongodb-cluster named librechat-db. kurly authors no Secret; MONGO_URI, CREDS_KEY, CREDS_IV, JWT_SECRET and JWT_REFRESH_SECRET come from a provided Secret via envFrom — CREDS_KEY and CREDS_IV encrypt the provider API keys users store, so replacing them makes every stored key unreadable. Conversation search needs a MeiliSearch, which is not carried here and is off by default. Uploads and generated images sit on two ReadWriteOnce volumes: one replica, recreated. Serves on :3080.
server http
The LibreChat server. Uploaded files at /app/uploads and avatars and generated images at /app/client/public/images, each on its own volume; everything else is in MongoDB. domain is the public URL absolute links and OAuth callbacks are built from. secretName holds MONGO_URI, CREDS_KEY, CREDS_IV, JWT_SECRET and JWT_REFRESH_SECRET (envFrom); the model API keys themselves are a deployment decision and go through env or the same Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/librechat/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "librechat" |
image | string | — | — |
uploadsSize | quantity | — | "10Gi" |
imagesSize | quantity | — | "10Gi" |
storageClass | string | — | — |
domain | string | — | — |
secretName | string | — | "librechat" |
search | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
libredb-studio
A LibreDB Studio server (a browser SQL client for PostgreSQL, MySQL, SQLite, MongoDB, Redis and more, with SSO and an audit trail). A plain composable http workload; the databases it connects to are entered by a user at runtime and are not dependencies. Its own state — accounts, saved connections and query history — is SQLite on a PersistentVolume, which STORAGE_PROVIDER=sqlite selects over the image default of keeping it in the browser. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The LibreDB Studio server. Keeps accounts, saved connections and query history in SQLite at /app/data on the volume. Runs as the uid 1001 account the image provisions, so its gosu entrypoint execs straight through and the hardened posture holds. secretName supplies JWT_SECRET and ADMIN_PASSWORD (envFrom); unset, both are generated on first start and written to the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/libredb-studio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "libredb-studio" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "libredb-studio" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
libredesk
A Libredesk server (a self-hosted customer support desk: shared inboxes, conversations, assignment rules, canned responses and SLAs, in one binary). A composable http workload backed by an EXTERNAL PostgreSQL — the cnpg-cluster workload provides one — and holding no PersistentVolume of its own, since everything lives in the database. Two init containers install and then upgrade the schema, because install alone leaves it at v0.0.0 and the server refuses to start. Serves the agent UI and API on :9000.
server http
The Libredesk server. It claims no volume: conversations, users and settings all live in PostgreSQL. The env names follow the shipped config.toml's own sections, and the database one is a TOP-LEVEL [db] table rather than under [app] — LIBREDESK_DB__* not LIBREDESK_APP__DB__* — which matters because an unmatched variable is ignored silently and the server then tries to reach a PostgreSQL called `db`. secretName holds LIBREDESK_DB__PASSWORD. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/libredesk/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "libredesk" |
image | string | — | — |
dbHost | string | — | "libredesk-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "libredesk" |
dbUser | string | — | "libredesk" |
appUrl | string | — | — |
secretName | string | — | "libredesk" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
librephotos
A LibrePhotos install (a self-hosted photo library with face, place and object recognition) on the official images, as the three stages the application really is: backend (Django/gunicorn API plus the django-q worker and the ML models it loads), frontend (the built React app served as static files) and proxy (the nginx edge that joins them into one origin and hands out the media the backend delegates with X-Accel-Redirect). PostgreSQL is external and pairs with the cnpg-cluster workload; the task queue is django-q on the ORM broker, so no Redis. The image ships an nginx.conf whose upstreams are the literal names backend and frontend, so the proxy stage renders its own from the stage names instead — the whole workload follows namePrefix. One volume at /librephotos holds the photo library, the generated media and Django secret.key; the backend owns it and the proxy mounts it read-only, so a ReadWriteOnce class keeps both pods on one node. Single writer: the backend is one replica, recreated. Expose the proxy, on :8080.
backend http
The LibrePhotos backend: the REST API on :8001, the django-q worker and the machine-learning services. dbHost/dbName/dbUser default to a cnpg-cluster named librephotos-db. secretName is the Secret it reads DB_PASS, SECRET_KEY and ADMIN_PASSWORD from — kurly authors none. One volume at /librephotos carries BASE_DATA (data, protected_media) and BASE_LOGS, which is where secret.key lives. workerConcurrency sizes the django-q pool, which otherwise sizes itself from the node. Do not expose it; expose the proxy.
Import: import 'github.com/metio/kurly/workloads/librephotos/backend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "librephotos" |
name | string | — | — |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
dbHost | string | — | "librephotos-db-rw" |
dbName | string | — | "librephotos" |
dbUser | string | — | "librephotos" |
dbPort | string | — | "5432" |
secretName | string | — | "librephotos" |
adminUsername | string | — | "admin" |
adminEmail | string | — | "admin@example.com" |
workerConcurrency | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"6Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
frontend http
The built React application, served as static files on :3000. Stateless and unconfigured — the browser reaches the API on the same origin, through the proxy. Do not expose it directly: the assets alone are not a working install.
Import: import 'github.com/metio/kurly/workloads/librephotos/frontend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "librephotos" |
name | string | — | — |
image | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"64Mi"},"requests":{"cpu":"20m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
proxy http
The nginx edge on :8080, and the stage you expose: it serves the frontend at /, forwards /api and /media to the backend, and serves the originals and generated media the backend delegates with X-Accel-Redirect. Its nginx.conf is rendered from backendHost/frontendHost (defaulting to the sibling stages) and dataRoot, so the workload follows namePrefix. storageClaim is the backend claim it mounts read-only; serveMedia=false drops that mount, and with it the downloads and full-size images.
Import: import 'github.com/metio/kurly/workloads/librephotos/proxy.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "librephotos" |
name | string | — | — |
image | string | — | — |
replicas | int | — | 1 |
backendHost | string | — | — |
backendPort | int | — | 8001 |
frontendHost | string | — | — |
frontendPort | int | — | 3000 |
storageClaim | string | — | — |
serveMedia | bool | — | true |
dataRoot | string | — | "/librephotos" |
maxBodySize | string | — | "500m" |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
librespeed
A LibreSpeed server — a lightweight, self-hosted network speed test you run in the browser. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A LibreSpeed server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/librespeed/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "librespeed" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
libretime
The LibreTime API server (radio broadcast automation) as one composable kurly.http stage on the project libretime-api image: gunicorn on :9001, the media library on a PersistentVolume, PostgreSQL and RabbitMQ external. Every component reads the same /etc/libretime/config.yml, so the config parameter is LibreTime own schema rendered to that file verbatim, from a ConfigMap by default or from a consumer Secret when secretName is set — the document carries the database and broker passwords. A full station also runs playout, liquidsoap, analyzer, a worker and the legacy PHP interface; this stage carries the API the others talk to. Single writer over a ReadWriteOnce media volume: one replica, recreated.
server http
The LibreTime API server, serving the REST API on :9001. publicUrl is the station public URL LibreTime derives stream URLs from; apiKey and secretKey are the internal API key and Django SECRET_KEY every component shares. dbHost/dbName/dbUser/dbPassword default to a cnpg-cluster named libretime-db; brokerHost/brokerVhost/brokerUser/brokerPassword to a RabbitMQ named libretime-broker. config is LibreTime own config.yml schema, rendered verbatim; secretName mounts a consumer Secret holding a complete config.yml over the ConfigMap, which is how the passwords stay out of it. kurly authors no Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/libretime/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "libretime" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
publicUrl | string | — | "https://libretime.example.com" |
apiKey | string | — | "changeme" |
secretKey | string | — | "changeme" |
timezone | string | — | "UTC" |
dbHost | string | — | "libretime-db-rw" |
dbName | string | — | "libretime" |
dbUser | string | — | "libretime" |
dbPassword | string | — | "libretime" |
brokerHost | string | — | "libretime-broker" |
brokerVhost | string | — | "/libretime" |
brokerUser | string | — | "libretime" |
brokerPassword | string | — | "libretime" |
config | object | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lidarr
A Lidarr server — a music collection manager for Usenet and BitTorrent users. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8686.
server http
The A Lidarr server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lidarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lidarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
linkding
A linkding server (a minimal, self-hosted bookmark manager). A plain composable http workload that keeps its bookmarks in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :9090.
server http
The linkding server. Keeps bookmarks in SQLite at /etc/linkding/data on the volume. Point LD_DB_ENGINE at external PostgreSQL through env to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/linkding/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "linkding" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
linkstack
A LinkStack server (a self-hosted, customizable "link in bio" page, a private alternative to Linktree) on the official image (pinned by digest; Renovate maintains it); with the default SQLite backend its data lives on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The LinkStack server. Data at /htdocs. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/linkstack/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "linkstack" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
listmonk
A listmonk server (a self-hosted newsletter and mailing-list manager) on the official image, backed by an external PostgreSQL, with uploaded media on a PersistentVolume. Pairs with a cnpg-cluster named listmonk-db. kurly authors no Secret; the DB and admin passwords come from a provided Secret via envFrom. Run the one-time schema install before first use. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9000.
server http
The listmonk server. dbHost/dbName/dbUser default to a cnpg-cluster named listmonk-db; adminUser is the admin. secretName holds LISTMONK_db__password and LISTMONK_app__admin_password (envFrom). Uploads at /listmonk/uploads. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/listmonk/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "listmonk" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
dbHost | string | — | "listmonk-db-rw" |
dbName | string | — | "listmonk" |
dbUser | string | — | "listmonk" |
adminUser | string | — | "admin" |
secretName | string | — | "listmonk" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
livebook
A Livebook server (interactive and collaborative code notebooks for Elixir, run and edited in the browser). A plain composable http workload with the notebooks and the hubs it remembers on a PersistentVolume. A notebook runs ARBITRARY CODE INSIDE THE POD, with its ServiceAccount, network and volume, so anyone who can sign in has a shell in the cluster. Without a password Livebook prints a one-time token into the pod log and accepts nothing else, which stops working the moment the pod is replaced. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Livebook server. Notebooks and Livebook's own configuration live at /data on the volume; HOME and /tmp are scratch, since the mix and hex caches a notebook fills are worth nothing after the pod. secretName holds LIVEBOOK_PASSWORD (at least 12 characters) and LIVEBOOK_SECRET_KEY_BASE (at least 64). The image binds :: by default, which fails on an IPv4-only pod network, so the stage sets LIVEBOOK_IP explicitly. The BEAM compiles the notebook runtime before it serves, hence the startup probe. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/livebook/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "livebook" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "livebook" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
liwan
A Liwan server (privacy-first web analytics: a one-line script per site, no cookies and no persistent identifiers, with everything kept in an embedded DuckDB beside it). A plain composable http workload keeping that database on a PersistentVolume, so no external database is needed. baseUrl is unset by default and Liwan falls back to http://localhost:9042 rather than refusing to start, which decides the origin the tracking script is served for and whether session cookies are marked secure — set it before publishing. While no user exists it logs a one-time /setup?t=… link built from that base URL, which is what creates the first administrator. Visitor IPs are read from the connection unless LIWAN_TRUSTED_PROXIES and LIWAN_CLIENT_IP_HEADERS name the proxy in front, so behind an ingress every visitor is otherwise grouped as one. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9042.
server http
The Liwan server. The DuckDB database lives at /data on the volume; /tmp is a scratch because DuckDB spills there when a query exceeds its memory budget. baseUrl is the public URL this instance answers on, scheme included — unset leaves Liwan on its own http://localhost:9042 fallback. disableFavicons keeps the referrer list from fetching icons from DuckDuckGo, i.e. from a third party, and is on by default. GeoIP is off until a MaxMind account or the headers of a proxy in front are configured. Probes hit the static tracking script, which needs no session and does not redirect. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/liwan/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "liwan" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
logLevel | string | — | "info" |
disableFavicons | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lldap
An LLDAP server (a light LDAP implementation for authentication: a simple user/group directory with a friendly web UI, a lightweight stand-in for OpenLDAP) on the official image; with the default SQLite backend its directory lives on a PersistentVolume. Apps bind over LDAP on :3890, a separate port to add a Service for. kurly authors no Secret; LLDAP_JWT_SECRET and LLDAP_LDAP_USER_PASS come from a provided Secret via envFrom. Point it at an external PostgreSQL/MySQL (LLDAP_DATABASE_URL) to scale past SQLite. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI on :17170.
server http
The LLDAP server. baseDn sets LLDAP_LDAP_BASE_DN; secretName holds LLDAP_JWT_SECRET and LLDAP_LDAP_USER_PASS (envFrom). Directory at /data; LDAP (:3890) needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lldap/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lldap" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
baseDn | string | — | — |
secretName | string | — | "lldap" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
llmkube
The LLMKube controller manager — the operator itself, not one of the resources it reconciles: it watches Model, InferenceService and ModelRouter custom resources and creates the Deployments, Services, PVCs and jobs that download weights and serve an OpenAI-compatible API. A plain composable http workload carrying a ServiceAccount, a ClusterRole/Binding and the namespaced leader-election Role/Binding. The CRDs are not part of it: controller-runtime watches the kinds at start-up, so apply the upstream CRDs of the pinned release first or the manager exits before it is ever ready. namespace MUST match where you deploy. Protected metrics on :8443, probes on :8081.
controller http
The controller manager. namespace MUST match where you deploy — it names the ServiceAccount in the ClusterRoleBinding, which a cluster-scoped object cannot inherit later. leaderElect keeps a second replica idle instead of reconciling twice. Writes only per-pod scratch (/tmp for the self-signed metrics certificate, /models for staging a locally sourced weight file); the model cache the inference pods read is a PVC it provisions at reconcile time. Serves protected metrics on :8443 and health on :8081.
Import: import 'github.com/metio/kurly/workloads/llmkube/controller.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "llmkube" |
namespace | string | — | "llmkube-system" |
image | string | — | — |
replicas | int | — | 1 |
leaderElect | bool | — | true |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"10m","memory":"512Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
podLabels | object | — | — |
podAnnotations | object | — | — |
lobe-chat
A LobeChat server (a self-hosted, open-source AI chat UI supporting many LLM providers, plugins and multimodal input) on the official image. A plain composable http workload. In its default mode conversations are stored client-side, so the server holds no data — a plain, horizontally scalable Deployment. Point it at your LLM providers with the documented environment variables; kurly authors no Secret. Serves on :3210.
server http
The LobeChat server. Stateless; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lobe-chat/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lobe-chat" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
localai
A LocalAI server (an OpenAI-compatible API in front of locally run language, image and audio models) on the official CPU image. A plain composable http workload that keeps models, downloaded backends and generated content on a PersistentVolume — no external database. No model ships in the image; the first pull downloads gigabytes onto the volume. The API is unauthenticated until LOCALAI_API_KEY is set from a Secret you provide. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The LocalAI server. Models at /models on the volume, backends, generated content and configuration on emptyDir. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/localai/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "localai" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"8Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
loki
Grafana Loki in microservices mode as a loki-operator `LokiStack` custom resource: one CR reconciles the whole distributed topology (distributor, ingester, querier, query-frontend, compactor, index-gateway, gateway), with `size` scaling the replicas. Authors the CR (like cnpg-cluster) for the operator to own the components, config, and ring. Requires the loki-operator and an object-storage Secret. Pairs with the seaweedfs workload for S3.
server loki
The LokiStack. size is the operator's t-shirt scaling (1x.demo is the smallest, for a test cluster; production wants 1x.extra-small+). storageSecret names the object-storage Secret you create (keys bucketnames/endpoint/access_key_id/access_key_secret/region) — point it at the seaweedfs workload's S3. The operator chooses the Loki image, so there is none to pin. Reach it at the gateway Service lokistack-gateway-http.
Import: import 'github.com/metio/kurly/workloads/loki/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "loki" |
size | string | — | "1x.demo" |
storageSecret | string | — | "loki-storage" |
storageClass | string | — | — |
schemaVersion | string | — | "v13" |
schemaEffectiveDate | string | — | "2024-01-01" |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
lubelogger
A LubeLogger server (vehicle maintenance records: services, fuel stops, repairs and reminders per vehicle, with receipts attached). A plain composable http workload keeping its LiteDB database and uploaded documents on a PersistentVolume. HOME points at that volume so ASP.NET keeps its DataProtection keys there — regenerated keys do not fail, they just log everybody out. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The LubeLogger server. LiteDB and uploads live at /App/data on the volume, and HOME points there too so ASP.NET writes its DataProtection keys onto the volume rather than into the container — upstream mounts a second volume for those; this needs only the one. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lubelogger/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lubelogger" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lychee
A Lychee server (a self-hosted photo-management and gallery system) on the official image. A plain composable http workload — with the SQLite backend its config, database, and photos live on a PersistentVolume, no external database. The nginx + PHP-FPM image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; APP_KEY comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Lychee server. Photos at /uploads, config at /conf, symlinks at /sym, all on the volume. appUrl is the public URL. secretName holds APP_KEY (envFrom). Point DB_CONNECTION at external MySQL/PostgreSQL to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/lychee/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lychee" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
secretName | string | — | "lychee" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
lyrion-music-server
A Lyrion Music Server (the music server formerly known as Logitech Media Server: it indexes a music library and streams it to Squeezebox hardware, software players and mobile clients) on the project's own image. A plain composable http workload that keeps its preferences, cache and scanned database on a PersistentVolume and reads the music library from /music on the same volume — no external database. The entrypoint starts as root to renumber its own user to PUID/PGID, chown /config and /playlist and then drop to that user with su, so root, privilege escalation, capabilities and a writable image tree are all relaxed deliberately; kurly keeps seccomp, user namespaces and the rest. Players speak slimproto on :3483 (TCP, and UDP for discovery) and the telnet CLI listens on :9090, both published on the Service beside the web port. Probed by connection, because the web UI redirects into a setup wizard on a fresh volume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9000.
server http
The Lyrion Music Server. Preferences, cache and the scanned database at /config, the music library at /music (read-only) and playlists at /playlist, all on the volume. puid/pgid are the user the entrypoint drops to and must own the mounted files. Compose an exposure onto the HTTP port; route the slimproto and CLI ports separately if players on the LAN need them.
Import: import 'github.com/metio/kurly/workloads/lyrion-music-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "lyrion-music-server" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mafl
A Mafl server (a minimalistic, configurable homepage for your services) on the official image. A plain composable http workload — its configuration lives on a PersistentVolume, no external database. The Nitro server writes only its data directory and /tmp, so the hardened default posture stands whole: non-root, read-only root filesystem, dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Mafl server. Configuration lives at /app/data/config.yml on the volume, seeded with a default on first start. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mafl/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mafl" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mailhog
A MailHog server (a self-hosted email-testing tool for developers that captures the mail your apps send and shows it in a web inbox instead of delivering it) on the official image (pinned by digest; Renovate maintains it). Keeps captured mail in memory: a plain stateless Deployment. Apps send to its SMTP listener on :1025 (needs an extra Service). Serves the web inbox on :8025.
server http
The MailHog server. SMTP on :1025 needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mailhog/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mailhog" |
image | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mailpit
A Mailpit server (a self-hosted email- and SMTP-testing tool: it catches every message your apps send and shows them in a web UI, with a real SMTP sink and an API) on the official image. A plain composable http workload listening on TWO ports — the web UI/API on :8025 and the SMTP sink on :1025 (via kurly.extraPort). Its message store (SQLite) lives on a PersistentVolume under /data. Single writer over a ReadWriteOnce volume: one replica, recreated. Point your apps at the Service on port 1025 for SMTP.
server http
The Mailpit server. Store at /data on the volume; SMTP on port 1025, web on 8025. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mailpit/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mailpit" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | {"MP_DATABASE":"/data/mailpit.db"} |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mailu
A Mailu mail server (SMTP, IMAP/POP3, webmail, antispam) as six coordinated http stages — front (the edge), admin (config + API + DB + DKIM), imap (Dovecot), smtp (Postfix), antispam (Rspamd), and webmail (Roundcube). Run all six pointed at the same namePrefix, secretName, and a shared ReadWriteMany storageClaim, plus a Redis (the valkey workload). Mailu images run as root with a writable root filesystem, so these relax kurly restricted defaults while keeping dropped capabilities and no privilege escalation. Each service is one replica, recreated; expose only front.
admin http
The Mailu administration service: web admin UI, the internal API the other services query, and the SQLite database and DKIM keys behind them (on the shared volume at /data and /dkim). front proxies /admin to it.
Import: import 'github.com/metio/kurly/workloads/mailu/admin.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resolverAddress | string | — | "" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
antispam http
The Mailu filter (Rspamd): screens mail on :11332, serves its web UI on :11334, and signs outbound mail with the DKIM keys admin generates. Learned state lives at /var/lib/rspamd on the shared volume.
Import: import 'github.com/metio/kurly/workloads/mailu/antispam.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resolverAddress | string | — | "" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
front http
The Mailu edge (nginx): terminates SMTP/IMAP/POP3/ManageSieve and the web UI and proxies to the other services. The one stage you expose. domain/hostnames identify the mail server; secretName carries SECRET_KEY (envFrom, kurly mints none); storageClaim is the shared RWM volume; redisAddress points at a valkey. Publishes 25/465/587/110/995/143/993/4190/80/443.
Import: import 'github.com/metio/kurly/workloads/mailu/front.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resolverAddress | string | — | "" |
tlsFlavor | string | — | "mail" |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
imap http
The Mailu mail store (Dovecot): holds the maildirs at /mail on the shared volume and serves IMAP/POP3 to front and LMTP delivery to postfix.
Import: import 'github.com/metio/kurly/workloads/mailu/imap.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resolverAddress | string | — | "" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
smtp http
The Mailu MTA (Postfix): relays mail between the edge, the filter, and the store. The queue is transient; only user overrides live on the shared volume.
Import: import 'github.com/metio/kurly/workloads/mailu/smtp.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resolverAddress | string | — | "" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
webmail http
The Mailu webmail client (Roundcube): front proxies /webmail to it. Optional — drop it if you only want IMAP/SMTP clients. Keeps its settings at /data on the shared volume.
Import: import 'github.com/metio/kurly/workloads/mailu/webmail.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
namePrefix | string | — | "mailu" |
name | string | — | — |
image | string | — | — |
domain | string | — | "example.com" |
hostnames | array | — | ["mail.example.com"] |
secretName | string | — | "mailu" |
storageClaim | string | — | "mailu-storage" |
subnet | string | — | "10.0.0.0/8" |
redisAddress | string | — | "mailu-cache" |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mainsail
A Mainsail server (the popular web interface for managing and controlling Klipper-based 3D printers) on the official unprivileged image. Mainsail is a browser app: the printer's Moonraker API is called from the BROWSER, not from this pod, so moonrakerHost has to be an address the browser can resolve. It writes the config.json Mainsail reads on load — via a subPath ConfigMap mount into the web root beside the app assets — so nobody types the address in; left out, no ConfigMap is emitted and Mainsail asks in the browser instead. Stateless: a plain, horizontally scalable rolling Deployment. Serves on :8080.
server http
The Mainsail server. moonrakerHost/moonrakerPort populate config.json's instance list so the browser connects to a printer on load; config overrides or extends the rest of that file verbatim. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mainsail/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mainsail" |
image | string | — | — |
replicas | int | — | 2 |
moonrakerHost | string | — | — |
moonrakerPort | int | — | 7125 |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
maloja
A Maloja server (a self-hosted music scrobble database and listening-statistics server, an alternative to Last.fm) on the official image. A plain composable http workload that keeps its database (SQLite) and configuration on a PersistentVolume under /mljdata. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :42010.
server http
The Maloja server. Data at /mljdata on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/maloja/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "maloja" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
manage-my-damn-life
An MMDL server (a front end for the CalDAV servers you already run: tasks and calendars from several accounts in one place, with labels, filters and reminders). A composable http workload backed by an external database and nothing else — it keeps no files of its own and claims no volume, so everything durable is in the database. The image starts the Next.js server directly and migrates nothing on boot: a fresh database answers but has no tables until somebody walks /install once, which is why the probes test the connection rather than a path that redirects there. Set `baseUrl` to the URL people visit, since absolute links and NextAuth callbacks are built from it. `dbDialect` chooses between postgres, mysql and sqlite, because MMDL speaks all three. The image runs as the unprivileged account it creates and drops nothing, so the hardened posture stands. Serves on :3000.
server http
The MMDL server. Accounts, labels, filters and the CalDAV credentials it stores on a user's behalf all live in the database named by dbDialect/dbHost/dbPort/dbName/dbUser; the password comes from the Secret. That Secret also holds AES_PASSWORD, the key those CalDAV credentials are encrypted with — changing it later makes every stored account unreadable rather than re-encrypting it. baseUrl is the public URL a browser reaches this instance at, absent by default because a wrong one is worse than none. Probed by connection, since every path redirects to /install or to the login page depending on how far setup has got. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/manage-my-damn-life/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "manage-my-damn-life" |
image | string | — | — |
dbDialect | string | — | "postgres" |
dbHost | string | — | "manage-my-damn-life-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "mmdl" |
dbUser | string | — | "mmdl" |
baseUrl | string | — | — |
secretName | string | — | "manage-my-damn-life" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
manticore
A Manticore Search server (a full-text search and analytics database: SQL over a MySQL-compatible port, JSON over HTTP, full-text and vector search in one engine). A plain composable http workload whose indexes live on a PersistentVolume, needing nothing external. It has no authentication of its own, so an exposure on the JSON port publishes an unauthenticated database — keep it in-cluster or authenticate in front of it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves JSON on :9308, the MySQL protocol on 9306 and the replication protocol on 9312.
server http
The Manticore Search server. Indexes live at /var/lib/manticore on the volume. Runs as the uid 999 account the image builds, which is what makes its entrypoint skip the root chown-and-gosu path. Compose an exposure onto the JSON port if you have authentication in front of it; the MySQL and replication ports are not HTTP and need their own routes.
Import: import 'github.com/metio/kurly/workloads/manticore/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "manticore" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mastodon
Mastodon (the leading self-hosted ActivityPub/Fediverse microblogging platform) as three workloads backed by an external PostgreSQL and Redis, with media in S3-compatible object storage: a web/API server (:3000), a streaming server for real-time timelines (:4000), and a sidekiq background worker. LOCAL_DOMAIN is baked into every @handle and cannot be changed. kurly authors no Secret; the PostgreSQL/Redis connection, SECRET_KEY_BASE, OTP_SECRET, the VAPID keys and the S3 settings come from a provided Secret via envFrom, shared by all three stages. Pairs with a cnpg-cluster named mastodon-db, a Redis and an S3 bucket (e.g. seaweedfs).
sidekiq worker
The Mastodon sidekiq background worker (federation, media, scheduled jobs). Shares the web stage's Secret via envFrom. No Service.
Import: import 'github.com/metio/kurly/workloads/mastodon/sidekiq.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mastodon-sidekiq" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "mastodon-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "mastodon" |
dbUser | string | — | "mastodon" |
redisHost | string | — | "mastodon-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "mastodon" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
streaming http
The Mastodon streaming server (real-time timelines over WebSockets) on :4000. Shares the web stage's Secret via envFrom; route /api/v1/streaming to it.
Import: import 'github.com/metio/kurly/workloads/mastodon/streaming.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mastodon-streaming" |
image | string | — | — |
replicas | int | — | 2 |
dbHost | string | — | "mastodon-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "mastodon" |
dbUser | string | — | "mastodon" |
redisHost | string | — | "mastodon-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "mastodon" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
web http
The Mastodon web/API server on :3000. localDomain is the permanent instance domain (@handle); secretName holds the shared connection and secrets (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mastodon/web.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mastodon" |
image | string | — | — |
replicas | int | — | 2 |
localDomain | string | — | — |
dbHost | string | — | "mastodon-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "mastodon" |
dbUser | string | — | "mastodon" |
redisHost | string | — | "mastodon-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "mastodon" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1536Mi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
matchering
A Matchering Web server (audio mastering by reference: upload a track and a reference, and it matches the loudness, frequency balance and stereo width). A plain composable http workload with uploads and results on a PersistentVolume. Mastering saturates whatever CPU it is given for the length of a track, so the limit is what stops one upload starving its neighbours. Django SECRET_KEY arrives as a single-file Secret mount, because the app reads it from a file it would otherwise regenerate on every restart. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8360.
server http
The Matchering Web server. Uploads and rendered results live at /app/data on the volume. secretName provides a `secret_key` entry mounted as the single file /app/.secret_key — settings.py reads SECRET_KEY from that path and nowhere else, and the entrypoint generates one when absent, so without the mount every restart silently invalidates every session. supervisord drops privileges to its own account and refuses to start when already unprivileged, so this runs as root by necessity rather than by default. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/matchering/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "matchering" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "matchering" |
env | object | — | — |
resources | object | — | {"limits":{"cpu":"2","memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
matrix-alertmanager-receiver
A matrix-alertmanager-receiver server (an Alertmanager posts its alerts to it over a webhook and it forwards each one into a Matrix room) on the official image. Stateless: alerts are forwarded as they arrive and nothing is kept, so no volume and more than one replica is fine. The configuration is rendered into a ConfigMap, but the Matrix access token is NOT: the file references ${MATRIX_ACCESS_TOKEN} and the value arrives from a Secret via envFrom, because a ConfigMap is readable by anything with get on the namespace and the token is a full credential for the posting account. The same mechanism carries the basic-auth password when basicAuth is on, which is what an Alertmanager outside the cluster should be made to send. homeserverUrl and userId have no defaults — a placeholder would authenticate against nothing. Probes read the socket rather than a path: every route either expects an Alertmanager payload or is the metrics endpoint that basic auth may guard. Serves on :12345; an exposure is optional and only wanted when the Alertmanager runs elsewhere.
server http
The matrix-alertmanager-receiver server. homeserverUrl and userId name the account it posts as; roomMapping gives short names to room ids so an Alertmanager configuration reads /alerts/pager rather than /alerts/!id:example.com — a room id in the URL works without it. externalUrlMapping and generatorUrlMapping rewrite the in-cluster URLs an Alertmanager and Prometheus report themselves by into ones a person clicking the alert can follow. firingTemplate, resolvedTemplate and computedValues are passed through; empty leaves the service defaults. basicAuth guards the alerts endpoint, with the password from the Secret as BASIC_PASSWORD. secretName holds MATRIX_ACCESS_TOKEN via envFrom.
Import: import 'github.com/metio/kurly/workloads/matrix-alertmanager-receiver/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "matrix-alertmanager-receiver" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 12345 |
homeserverUrl | string | — | — |
userId | string | — | — |
roomMapping | object | — | — |
alertsPathPrefix | string | — | "/alerts" |
metricsPath | string | — | "/metrics" |
metricsEnabled | bool | — | true |
basicAuth | bool | — | false |
basicUsername | string | — | "alertmanager" |
externalUrlMapping | object | — | — |
generatorUrlMapping | object | — | — |
firingTemplate | string | — | — |
resolvedTemplate | string | — | — |
computedValues | object | — | — |
logLevel | string | — | "info" |
secretName | string | — | "matrix-alertmanager-receiver" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
matrix-conduit
A Conduit server (a lightweight, self-hosted Matrix homeserver written in Rust that federates with the Matrix network) on the official image; its embedded database lives on a PersistentVolume. The serverName is baked into every user and room id at first start and cannot be changed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6167.
server http
The Conduit Matrix homeserver. serverName is permanent (baked into ids); allowRegistration toggles open sign-up. Data at /var/lib/matrix-conduit. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/matrix-conduit/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "matrix-conduit" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
serverName | string | — | — |
allowRegistration | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mautic
A Mautic server (open-source marketing automation) on the official Apache image, backed by an external MySQL/MariaDB, with configuration and media on a PersistentVolume. kurly ships no MySQL recipe — bring your own. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; MAUTIC_DB_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Mautic server. dbHost/dbName/dbUser point at a MySQL/MariaDB you provide. siteUrl is the public URL; runCronJobs runs Mautic background jobs in-container. secretName holds MAUTIC_DB_PASSWORD (envFrom). Config at /var/www/html/config, media on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mautic/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mautic" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "mautic-db" |
dbName | string | — | "mautic" |
dbUser | string | — | "mautic" |
siteUrl | string | — | — |
secretName | string | — | "mautic" |
runCronJobs | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mayan-edms
A Mayan EDMS server (document management: ingests scans and files, generates previews, OCRs them, and files them away under cabinets, tags and metadata indexes). A composable http workload on the official all-in-one image — gunicorn and the five Celery worker classes under one supervisord — with an external PostgreSQL and Redis broker, pairing with the cnpg-cluster and valkey workloads. Django takes its database as ONE setting holding the password inline, so the connection is assembled inside the container by the image's own pre-setup hook rather than rendered into a manifest. The entrypoint chowns the document store and starts every process through runuser, so root, privilege escalation, capabilities and a writable root filesystem are relaxed. First boot migrates, builds the font cache and installs document types before anything binds, so it is startup-probed by socket. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The Mayan EDMS server, serving the UI and API on :8000 and running every Celery worker class in the same pod. dbHost/dbPort/database/dbUser default to a cnpg-cluster named mayan-edms-db; secretName is read as environment and holds MAYAN_DATABASE_PASSWORD plus the two Celery URLs pointing at a valkey named mayan-edms-cache. workers is the gunicorn worker count. Documents, previews, caches and the SECRET_KEY Mayan generates on first boot all live on the volume at /var/lib/mayan. kurly authors no Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mayan-edms/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mayan-edms" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
dbHost | string | — | "mayan-edms-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "mayan" |
dbUser | string | — | "mayan" |
secretName | string | — | "mayan-edms" |
workers | int | — | 3 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
mazanoke
A MAZANOKE server (a self-hosted, client-side image optimizer that compresses and converts images entirely in the browser) on the official image. A plain composable http workload. All processing happens client-side; the server only serves static assets and holds no data — a plain, horizontally scalable Deployment. Serves on :80.
server http
The MAZANOKE server. Stateless; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mazanoke/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mazanoke" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mcp-context-forge
An MCP Context Forge gateway (a registry and proxy federating MCP servers, A2A servers and REST APIs behind one endpoint, with authentication, rate limiting and an admin UI). A plain composable http workload on the project's own image; the registry lives in a SQLite database on a PersistentVolume, so a single instance needs no external database. secretName carries JWT_SECRET_KEY — which signs the tokens the gateway issues, so a value that changes on restart invalidates every one of them — and the platform admin's credentials; the gateway also stores the credentials of every upstream server it federates, which is what makes the database worth backing up. Its whole job is calling other servers, so it reaches whatever the pod can reach: a NetworkPolicy limiting egress to the servers it should federate is worth composing on. Single writer over a ReadWriteOnce volume: one replica, recreated — databaseUrl pointed at PostgreSQL is what more than one needs. Serves on :4444.
server http
The MCP Context Forge gateway. databaseUrl is a SQLAlchemy URL; the default keeps SQLite at /app/data on the volume. secretName holds JWT_SECRET_KEY and the platform admin credentials through envFrom. Everything else the gateway reads is environment, through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mcp-context-forge/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mcp-context-forge" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
databaseUrl | string | — | "sqlite:////app/data/mcp.db" |
secretName | string | — | "mcp-context-forge" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mealie
A Mealie server (a self-hosted recipe manager and meal planner with a recipe scraper, shopping lists and a REST API) on the official image; with the default SQLite backend its database and uploaded assets live on a PersistentVolume. Point it at an external PostgreSQL (DB_ENGINE=postgres + POSTGRES_*) to scale past the single SQLite writer. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9000.
server http
The Mealie server. baseUrl is the public URL. Data at /app/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mealie/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mealie" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
medama-analytics
A Medama server (privacy-first website analytics: a tracker under a kilobyte that sets no cookie and keeps no IP address, with a dashboard over the results). A plain composable http workload keeping both of its embedded databases — SQLite for the application, DuckDB for the analytics — on one PersistentVolume. The tracker script is loaded by the browsers visiting the measured sites, so the exposure has to be reachable from the public internet or nothing is collected at all. The first boot seeds a single account, admin with the password CHANGE_ME_ON_FIRST_LOGIN, and there is no Secret to mint: whoever reaches the dashboard first owns the instance, so change it before the exposure is public. AUTO_SSL is deliberately not wired — it makes the process bind :80 and :443 and obtain certificates itself, which is the ingress controller's job here. HOME points at the volume because DuckDB resolves its extension directory under it and the root filesystem is read-only. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Medama server. Both databases at /app/data on the volume. corsAllowedOrigins is the comma-separated origin list allowed to call the API routes, for a dashboard hosted elsewhere; env merges over the defaults PORT, APP_DATABASE_HOST, ANALYTICS_DATABASE_HOST, HOME and LOGGER, so a key you set wins. Probes are connection probes because the dashboard redirects an unauthenticated browser. Compose an exposure onto the HTTP port — a public one, since the tracker is fetched by visitors' browsers.
Import: import 'github.com/metio/kurly/workloads/medama-analytics/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "medama-analytics" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
corsAllowedOrigins | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mediamtx
A MediaMTX server (a real-time media server and proxy: it ingests a live stream over one protocol and republishes it over the others) on the official image; its mediamtx.yml is its only state, rendered as a ConfigMap and passed as the server argument. The HTTP port is HLS (:8888) and the only one an Ingress or HTTPRoute can carry — RTSP :8554 with the RTP/RTCP pair :8000/:8001 UDP, RTMP :1935, WebRTC :8889 with its ICE mux on :8189 UDP and SRT :8890 UDP are published beside it and need a LoadBalancer. WebRTC beyond the cluster also needs webrtcAdditionalHosts set to an address a browser can reach, since the pod IP is not one. The control API serves on :9997 and metrics on :9998; MoQ is off because it would bind two further ports over a certificate it mints for itself. kurly authors no Secret and the default config authenticates nobody: anybody who reaches a port can publish and read streams. Nothing is written to disk, so no volume; streams are per-connection, so a second replica is a second server rather than a bigger one.
server http
The MediaMTX server. config is the whole mediamtx.yml, rendered and passed as the argument; extend it for authentication, named paths or recording. Compose an exposure onto the HLS port and a LoadBalancer for the streaming ports.
Import: import 'github.com/metio/kurly/workloads/mediamtx/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mediamtx" |
image | string | — | — |
replicas | int | — | 1 |
config | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mediawiki
A MediaWiki server (the wiki engine behind Wikipedia) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with uploaded files on a PersistentVolume. Configured by a LocalSettings.php mounted from a Secret (it holds the DB password and secret key). The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The MediaWiki server. Uploaded files at /var/www/html/images on the volume. Mount a LocalSettings.php at /var/www/html/LocalSettings.php from a Secret (DB creds + secret key). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mediawiki/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mediawiki" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
memcached
An in-memory cache sharded by the client, as a StatefulSet whose storage is nothing and whose identity is everything. No replication and no persistence: an upgrade always starts cold, and stable pod names are what bound the loss to 1/N of a client's keyspace.
cache stateful
The memcached shards: a StatefulSet (for stable DNS names, not storage) and the headless Service that names them. Clients consistent-hash keys over memcached-0..N-1; scaling reshuffles that ring, so treat replicas as part of the client configuration. The container memory limit is derived from memoryMB, since -m caps only the item cache.
Import: import 'github.com/metio/kurly/workloads/memcached/cache.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "memcached" |
image | string | — | — |
replicas | int | — | 3 |
memoryMB | int | — | 64 |
maxConnections | int | — | 1024 |
memex
A memEx server (a structured personal knowledge base of notes, contexts and pipelines, inspired by zettelkasten and org-mode) on the official image. A plain composable http workload on the Phoenix release, backed by an external PostgreSQL — all state is in the database, so it claims no volume. A migrator in the supervision tree runs the migrations on start, so a first boot against a fresh database is slower than the ones after it, which is what the startup budget covers. Mail is not optional: the production configuration refuses to start without SMTP_HOST, SMTP_USERNAME and SMTP_PASSWORD, because users are invited and addresses confirmed by email. host is the public domain links and invite mails are built from and defaults to localhost, which boots and is wrong for every real deployment. Registration is invite-only by default. kurly authors no Secret; SECRET_KEY_BASE, DATABASE_URL and the SMTP credentials come from a provided Secret via envFrom. The image runs as the NAME nobody, which kubelet cannot check against runAsNonRoot, so the uid and gid are pinned to 65534, and RELEASE_TMP points at a scratch because an Elixir release writes its boot files into the release tree. Probed by connection, because every page redirects an unauthenticated visitor to the sign-in form. One replica: without libcluster the Phoenix PubSub is per node. Serves on :4000.
server http
The memEx server. All state is in PostgreSQL and the release migrates it on start. secretName holds SECRET_KEY_BASE, which signs the session cookie — a value that changes on every restart signs everybody out — plus DATABASE_URL and the SMTP credentials. host is the public domain the app builds its links and invite mails from; smtpHost/smtpPort/smtpSsl name the relay, whose credentials stay in the Secret. registration chooses between invite and public sign-up. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/memex/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "memex" |
image | string | — | — |
host | hostname | — | "localhost" |
secretName | string | — | "memex" |
smtpHost | hostname | — | "localhost" |
smtpPort | string | — | "587" |
smtpSsl | bool | — | false |
emailFrom | string | — | — |
emailName | string | — | "memEx" |
registration | string | — | "invite" |
locale | string | — | "en_US" |
poolSize | string | — | "10" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
memos
A Memos server (a lightweight, self-hosted notes and micro-blogging service). A plain composable http workload that keeps its notes in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :5230.
server http
The Memos server. Keeps notes in SQLite at /var/opt/memos on the volume. Point MEMOS_DRIVER at external PostgreSQL through env to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/memos/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "memos" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
memtly
A Memtly server (event photo sharing: guests scan a QR code, see the gallery and upload their own photos and videos, for a wedding, a concert or a trip). A plain composable http workload keeping its SQLite database, uploads, thumbnails and branding on four PersistentVolumes; DATABASE_TYPE and DATABASE_CONNECTION_STRING move the data to an external MariaDB, MySQL, PostgreSQL or SQL Server, and the uploads stay on the volume either way. The encryption key, its salt and the admin password all have PUBLISHED defaults in the project's own compose file, so supplying the Secret is the difference between a private gallery and a public one. Memtly redirects http to https itself by default, which loops behind a TLS-terminating ingress, so this workload turns that off and probes by connection. Settings are imported from the environment at the FIRST boot only and administered from the settings tab afterwards. Kestrel is pinned to :5000 by the application itself, whatever the base image's ASPNETCORE_HTTP_PORTS says. Single writer over ReadWriteOnce volumes: one replica, recreated.
server http
The Memtly server. The SQLite database and settings live at /app/config, uploaded originals at /app/uploads, generated thumbnails at /app/thumbnails and operator branding at /app/custom_resources, each on its own volume. baseUrl is the public URL written into the links and QR codes handed to guests and has no default, since a wrong one is worse than none. secretName holds ENCRYPTION_KEY, ENCRYPTION_SALT and ACCOUNT_ADMIN_PASSWORD. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/memtly/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "memtly" |
image | string | — | — |
title | string | — | "Memtly" |
baseUrl | string | — | — |
forceHttps | bool | — | false |
storageSize | quantity | — | "20Gi" |
configStorageSize | quantity | — | "1Gi" |
thumbnailStorageSize | quantity | — | "5Gi" |
resourceStorageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "memtly" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mermaid-live-editor
A Mermaid Live Editor server (a self-hosted, in-browser editor for Mermaid diagrams: flowcharts, sequence diagrams, Gantt charts and more from text) on the official image. A plain composable http workload. Diagrams are rendered client-side and shared via URL, so the server only serves static assets and holds no data — a plain, horizontally scalable Deployment. Serves on :8080.
server http
The Mermaid Live Editor server. Stateless; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mermaid-live-editor/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mermaid-live-editor" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
meshmonitor
A MeshMonitor server (a web front end for a Meshtastic mesh: it connects to a node over TCP, records the messages, telemetry and node list it sees, and draws them on a map and a timeline). A plain composable http workload on the project's own image; everything it knows goes into a SQLite database on a PersistentVolume, so no external database is needed. IT NEEDS A NODE IT CAN REACH OVER IP — Meshtastic radios are usually attached by USB or Bluetooth, neither of which a pod can be given, so nodeIp is the address of a node with the TCP API enabled, and egress to it is the first thing to check when the UI comes up empty. The image's supervisord declares user=root and drops to the node user with su-exec, which normally means a root container; it does not here, because the runtime uid is the node user's own 1000, making that call a change to the uid the process already has, and the entrypoint skips its chown when it is not root because fsGroup has done that job. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3001.
server http
The MeshMonitor server. nodeIp and nodePort are the Meshtastic node it connects to; the database, backups and notification configuration live under /data on the volume. behindProxy marks the session cookies secure and trusts the forwarded client address, which is what a deployment behind an exposure wants. supervisord writes its pidfile to a scratch /var/run, so the root filesystem stays read-only. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/meshmonitor/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "meshmonitor" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
nodeIp | string | — | — |
nodePort | int | — | 4403 |
behindProxy | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
metamcp
A MetaMCP server (an MCP proxy that aggregates several Model Context Protocol servers into one endpoint, groups them into namespaces and applies middleware in front) on the project's own image, backed by an external PostgreSQL. Pairs with a cnpg-cluster named metamcp-db. APP_URL IS REQUIRED AND NOT DECORATION: the application throws and exits without one, and builds its callback URLs from it while validating request origins against it, so a wrong value gives a page that loads and a login that fails — the default is a placeholder that boots and is wrong for every real deployment. Registration stays open until an administrator exists, so bootstrapEmail with BOOTSTRAP_USER_PASSWORD in the Secret creates that account at start rather than leaving the first visitor to claim it. The first start applies migrations with pnpm, which writes a tool cache under HOME — unset it resolves to / and the migration fails before the application starts once — so HOME points at the scratch, and the wait is a startup probe. Stateless: a plain rolling Deployment. Serves on :12008.
server http
The MetaMCP server. dbHost/dbPort/dbName/dbUser point at PostgreSQL, defaulting to a cnpg-cluster named metamcp-db. appUrl is the browser-visible URL callbacks and origin checks are built from. bootstrapEmail/bootstrapName create the administrator at start, on the first run only. secretName holds DATABASE_URL — the whole connection string, which the migration step reads and which a manifest cannot compose from an envFrom password — plus POSTGRES_PASSWORD, BETTER_AUTH_SECRET and BOOTSTRAP_USER_PASSWORD (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/metamcp/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "metamcp" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "metamcp-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "metamcp" |
dbUser | string | — | "metamcp" |
appUrl | string | — | "http://localhost:12008" |
bootstrapEmail | string | — | — |
bootstrapName | string | — | "admin" |
logLevel | string | — | "info" |
secretName | string | — | "metamcp" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
metrics-server
The Kubernetes Metrics Server: it scrapes CPU/memory usage from every node's kubelet and serves it through the aggregated metrics.k8s.io API — what kubectl top and Horizontal Pod Autoscalers read. A plain composable http workload that registers an APIService and carries the aggregation RBAC (ServiceAccount, ClusterRoles/Bindings, the kube-system auth-reader RoleBinding).
server http
The metrics server. namespace MUST match where you deploy — the APIService and cluster RBAC name the ServiceAccount by namespace, which cluster-scoped objects cannot inherit later; kube-system is conventional. kubeletInsecureTLS=true skips verifying the kubelet serving cert (needed on kind and many on-prem clusters, or every scrape fails the TLS handshake).
Import: import 'github.com/metio/kurly/workloads/metrics-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "metrics-server" |
namespace | string | — | "kube-system" |
image | string | — | — |
replicas | int | — | 1 |
kubeletInsecureTLS | bool | — | false |
metricResolution | string | — | "15s" |
resources | object | — | {"limits":{"memory":"400Mi"},"requests":{"cpu":"100m","memory":"200Mi"}} |
labels | object | — | — |
annotations | object | — | — |
metube
A MeTube server (a web UI for yt-dlp: paste a video or playlist URL and it downloads it to a directory) on the official image; downloaded files live on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8081.
server http
The MeTube server. Downloads at /downloads. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/metube/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "metube" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
microbin
A MicroBin server (a tiny, self-contained pastebin and file-sharing service). A plain composable http workload that keeps its pastes and uploaded files on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :8080.
server http
The MicroBin server. Keeps pastes and files at /app/microbin_data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/microbin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "microbin" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mindwendel
A mindwendel server (a shared brainstorming board where a team collects ideas and upvotes them) on the official image. A plain composable http workload on the Phoenix release, backed by an external PostgreSQL — all state is in the database, so it claims no volume. The entrypoint waits for the database, runs the Ecto migrations and only then listens, so the first boot against a fresh database is slower than the ones after it, which is what the startup budget covers. databaseSsl is off by default because an in-namespace PostgreSQL commonly serves plaintext and the app refuses to connect when told to expect TLS that is not there. File uploads are off by default: attachments go to S3-compatible object storage and are encrypted with a vault key, so enabling them needs OBJECT_STORAGE_* and VAULT_ENCRYPTION_KEY_BASE64 as well. kurly authors no Secret; SECRET_KEY_BASE and DATABASE_USER_PASSWORD come from a provided Secret via envFrom. Probed by connection, because every page redirects to the current locale. One replica: without libcluster the Phoenix PubSub is per node, so two pods serve the same board without seeing each other. Serves on :4000.
server http
The mindwendel server. All state is in PostgreSQL and the release migrates it on start. secretName holds SECRET_KEY_BASE, which signs the session cookie — a value that changes on every restart signs everybody out — and DATABASE_USER_PASSWORD. urlHost/urlPort/urlScheme are the public origin board links are built from; the app refuses to start without a host. fileUpload additionally needs OBJECT_STORAGE_* in env and VAULT_ENCRYPTION_KEY_BASE64 in the Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mindwendel/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mindwendel" |
image | string | — | — |
dbHost | hostname | — | "mindwendel-db-rw" |
dbName | string | — | "mindwendel" |
dbUser | string | — | "mindwendel" |
dbPort | string | — | "5432" |
databaseSsl | bool | — | false |
urlHost | hostname | — | "localhost" |
urlPort | string | — | "4000" |
urlScheme | string | — | "http" |
secretName | string | — | "mindwendel" |
defaultLocale | string | — | "en" |
removalAfterDays | string | — | "30" |
fileUpload | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mirumoji
A Mirumoji server (a Japanese immersion toolkit: it tokenises the subtitles of a video, makes every word clickable for a dictionary lookup with a kanji breakdown, transcribes audio and exports flashcards). A plain composable http workload keeping its media, profiles and SQLite database on a PersistentVolume. Upstream ships two images and this is the BACKEND one — the React frontend of the project's compose file is not carried, because it terminates TLS itself with a certificate authority it mints at runtime, which an Ingress or an HTTPRoute already does. The dictionary data is baked into the image (UniDic plus half a gigabyte of Kotobase databases), so it needs no egress at all until the optional LLM breakdowns or the offloaded Modal transcription are switched on. It runs as ROOT deliberately: the server resolves its storage through platformdirs under $HOME and the baked dictionary cache sits in root's home at mode 0700, so an unprivileged account cannot even traverse into it — the root filesystem stays read-only and no capability or escalation is granted. Service links are disabled because every setting it reads is named MIRUMOJI_*, and a Service of this name injects MIRUMOJI_PORT as a tcp:// URL into that same namespace of names. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The Mirumoji backend. Media, profiles, clips and the SQLite database at /root/.local/share/mirumoji on the volume. transcribeBackend picks between local faster-whisper and Modal (auto decides from what is present); secretName holds the optional keys for the features that leave the cluster — OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, MIRUMOJI_LLM_API_KEY, MIRUMOJI_LLM_BASE_URL and the two MODAL_TOKEN_* values — without which subtitles, dictionary and flashcards all still work. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mirumoji/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mirumoji" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
transcribeBackend | string | — | "auto" |
logLevel | string | — | "INFO" |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
misskey
A Misskey server (a self-hosted, feature-rich ActivityPub/Fediverse social platform) on the official image, backed by an external PostgreSQL and Redis, with uploaded files on a PersistentVolume. Misskey reads its whole config (including DB/Redis credentials) from /misskey/.config/default.yml; because that holds secrets, kurly mounts it from an existing Secret you provide (kurly never mints key material). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Misskey server. configSecret is an existing Secret holding default.yml (the config, with DB/Redis creds), mounted at /misskey/.config. Files at /misskey/files. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/misskey/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "misskey" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
configSecret | string | — | "misskey" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mongo-express
A mongo-express server (a lightweight, web-based MongoDB admin UI) on the official image. Stateless: a plain rolling Deployment that connects to the MongoDB you point it at. kurly authors no Secret; ME_CONFIG_MONGODB_URL and the basic-auth credentials come from a provided Secret via envFrom. Pairs with a mongodb-cluster. Serves on :8081.
server http
The mongo-express server. secretName holds ME_CONFIG_MONGODB_URL and the basic-auth credentials (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mongo-express/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mongo-express" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "mongo-express" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mongooseim
A MongooseIM server (an XMPP server built for messaging at scale, with clustering and a GraphQL management API) on the official image. A plain composable http workload keeping its Mnesia database on a PersistentVolume, so no external database is needed. The entrypoint derives the Erlang node from the hostname and puts the database in Mnesia.<node>, so NODE_HOST is pinned — with the pod name as the hostname every replacement pod picks a new directory on the same volume and starts empty. That entrypoint also rewrites the release's own etc/ in place with sed and the release creates var/ and log/ beside it, so the image tree is writable and the pod runs as 1001:1002, the account owning those files — root would fail every one of those writes with EACCES, since ALL capabilities are dropped and none of the tree belongs to it. Non-root, no privilege escalation, dropped capabilities and its own user namespace all stand. /member is the drop-in directory it reads its configuration from (a scratch volume unless configMount is set, because the entrypoint changes into it), and the shipped configuration serves the localhost domain with sample GraphQL admin credentials on :5541 and :5551, neither of them published here. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves XMPP client :5222, s2s :5269, and BOSH/WebSocket over HTTP on :5280.
server http
The MongooseIM server. Keeps its Mnesia database at /var/lib/mongooseim on the volume, under a directory named after the Erlang node — nodeHost is what pins that name, so changing it moves the database. Mount your own mongooseim.toml at /member with kurly.config after setting configMount, since the shipped one serves only the localhost domain. Route :5222 and :5269 as TCP and compose an HTTP exposure onto :5280 for BOSH and WebSocket clients.
Import: import 'github.com/metio/kurly/workloads/mongooseim/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mongooseim" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
nodeHost | string | — | "localhost" |
configMount | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
monica
A Monica server (a self-hosted personal relationship manager) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with uploads on a PersistentVolume. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; DB_PASSWORD and APP_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Monica server. dbHost/dbName/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). appUrl is the public URL. secretName holds DB_PASSWORD and APP_KEY (envFrom). Uploads at /var/www/html/storage. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/monica/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "monica" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "monica-db" |
dbName | string | — | "monica" |
dbUser | string | — | "monica" |
appUrl | string | — | — |
secretName | string | — | "monica" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mosquitto
An Eclipse Mosquitto server (a lightweight, self-hosted MQTT message broker for IoT and home automation). Mosquitto speaks MQTT, not HTTP: it listens on :1883, its mosquitto.conf is mounted as a ConfigMap and passed verbatim, and its persistence database lives on a PersistentVolume. The default allows anonymous clients; a real broker sets up authentication. WebSockets (:9001) need a listener in the config and a second Service. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves MQTT on :1883.
server http
The Mosquitto broker on :1883. config is mosquitto.conf, mounted verbatim. Data at /mosquitto/data. Expose to devices (often a LoadBalancer), not an HTTP ingress.
Import: import 'github.com/metio/kurly/workloads/mosquitto/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mosquitto" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
config | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
multi-scrobbler
A multi-scrobbler server (it watches playback across many sources — Spotify, Plex, Jellyfin, YouTube Music, a desktop player — and forwards each play to the scrobbling services that keep your history, such as Last.fm, ListenBrainz or Maloja) on the upstream image. Its configuration, the credentials each source hands back and the database of plays live on a PersistentVolume at /config. baseUrl is the one value only the operator knows: every OAuth source builds its redirect URI from it, so an unset one sends the browser back to an address that is not this instance and the source never connects. Probes open the port rather than reading the health endpoint, which reports the state of the CONFIGURED sources and answers unhealthy on a fresh instance. The LinuxServer.io base image starts its s6-overlay init as root and drops to the PUID/PGID user, so this runs as root with CHOWN, DAC_OVERRIDE, FOWNER, SETGID and SETUID granted back by name (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9078.
server http
The multi-scrobbler server. baseUrl is the public URL OAuth callbacks return to; puid/pgid own the files on the volume; timezone sets TZ. Configuration and credentials at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/multi-scrobbler/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "multi-scrobbler" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mumble
A Mumble server (Murmur: a self-hosted, low-latency voice-chat server for gaming and communities). Modelled as an HTTP workload for its Deployment/Service plumbing, but Mumble speaks its own voice protocol on :64738 (TCP control, UDP voice), with its SQLite database on a PersistentVolume. The Service publishes the TCP port; UDP voice needs a second Service (usually a LoadBalancer). kurly authors no Secret; MUMBLE_SUPERUSER_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Mumble/Murmur server on :64738 (TCP control + UDP voice). secretName holds MUMBLE_SUPERUSER_PASSWORD (envFrom). Data at /data. Expose to clients (often a LoadBalancer); add a second Service for UDP.
Import: import 'github.com/metio/kurly/workloads/mumble/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mumble" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "mumble" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
myfin-budget
A MyFin API server (the backend of a personal finance platform: accounts, transactions, categorised budgets, investments and forecasts) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one) and claiming no volume of its own. The browser client is a separate static bundle that talks to this API and is not part of this workload. kurly authors no Secret; the database connection comes from a provided Secret via envFrom, as DATABASE_URL or as the DB_* parts the server assembles one from, and the SMTP credentials the one-time-code and password-reset mails need belong in the same Secret. The first start applies the Prisma migration ladder before it listens, so it has a startup probe rather than a longer liveness delay; the migrations take an advisory lock, so several replicas start safely. Stateless: a plain rolling Deployment. Serves the REST API on :3001.
server http
The MyFin API server. Everything is in MySQL — this stage claims no volume. secretName holds DATABASE_URL (or DB_NAME/DB_USER/DB_PW/DB_HOST/DB_PORT, which the server assembles a URL from when DATABASE_URL is absent) and the SMTP_* settings the platform mails with, all via envFrom. Anything else the server reads — ENABLE_USER_SIGNUP, TRUST_PROXY, LOGGING — can be passed through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/myfin-budget/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "myfin-budget" |
image | string | — | — |
replicas | int | — | 1 |
secretName | string | — | "myfin-budget" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mylar3
A Mylar3 server — a self-hosted comic-book (CBR/CBZ) downloader and library manager. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8090.
server http
The A Mylar3 server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/mylar3/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mylar3" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
myspeed
A MySpeed server (runs speed tests on a schedule and charts the history, showing when a connection degraded). A plain composable http workload keeping its database on a PersistentVolume. Run from a cluster it measures the NODE'S uplink rather than a home connection. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the dashboard on :5216.
server http
The MySpeed server. Its database lives at /myspeed/data on the volume. /myspeed/bin is scratch as well, because MySpeed creates data, bin, data/logs and data/servers relative to its working directory at every start and downloads the speedtest CLI into bin — without it the only message is "Could not create the data folder", which names the one directory that was fine. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/myspeed/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "myspeed" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
mysql-cluster
A highly-available MySQL cluster as an Oracle MySQL Operator InnoDBCluster custom resource (Group Replication fronted by MySQL Router). The MySQL counterpart to cnpg-cluster — an app that needs MySQL/MariaDB instead of PostgreSQL points its dbHost at this cluster. Requires the MySQL Operator for Kubernetes; unlike CNPG, you provide the root-credentials Secret (kurly mints none).
cluster mysql
The MySQL InnoDBCluster CR. Adapt with the parameters and render with kurly.list — composed by parameter, not by + feature (it is a custom resource). secretName is a Secret you provide with rootUser/rootHost/rootPassword. instances are Group Replication members (odd count for quorum); routerInstances the routing tier.
Import: import 'github.com/metio/kurly/workloads/mysql-cluster/cluster.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "mysql" |
instances | int | — | 3 |
routerInstances | int | — | 2 |
serverVersion | string | — | "9.7.1" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "mysql-cluster" |
resources | object | — | — |
tlsUseSelfSigned | bool | — | true |
imagePullSecrets | array | — | — |
labels | object | — | — |
annotations | object | — | — |
nats
A NATS server (a fast, lightweight, self-hosted messaging system: pub/sub, request/reply and, with JetStream, persistent streams) on the official image; NATS speaks its own protocol on :4222 with its JetStream store on a PersistentVolume. JetStream is enabled. Single server (a real deployment runs a cluster). The monitoring endpoint (:8222) needs an extra Service. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves clients on :4222.
server http
The NATS server on :4222 (JetStream enabled). Store at /data; monitoring (:8222) needs an extra Service. Usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/nats/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nats" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
navidrome
A Navidrome server (a modern music server and streamer, compatible with Subsonic/Airsonic clients) on the official image. A plain composable http workload that keeps its database on a PersistentVolume and reads a music library from /music on the same volume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4533.
server http
The Navidrome server. Database at /data, music library at /music (read-only), both on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/navidrome/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "navidrome" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
neko
A neko server (a virtual browser running inside the container, its screen and sound streamed to a room over WebRTC with shared mouse and keyboard) on the official Firefox image. A plain composable http workload and a STATELESS one: a room is a browser session, so it claims no PersistentVolume and runs a single replica — a second would be a second, unrelated room behind one address. Deliberately less hardened than most here: supervisord starts as root to bring up X, D-Bus and PulseAudio before dropping to an unprivileged account, and the browser profile lives among the image's own files, so the root filesystem is writable. Serves the room UI and signalling on :8080; the media itself is WebRTC on one multiplexed TCP+UDP port, which must be reachable from the client.
server http
The neko server. secretName holds the two multiuser passwords (envFrom); kurly mints none. webrtcPort is the single port both WebRTC transports are multiplexed onto, and nat1to1 the address clients reach it on — without it the room loads and the screen never arrives, since the pod address WebRTC would otherwise advertise is unroutable from a browser. Firefox and GStreamer share decoded frames through a /dev/shm scratch sized by shmSize. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/neko/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "neko" |
image | string | — | — |
secretName | string | — | "neko" |
webrtcPort | int | — | 59000 |
nat1to1 | string | — | — |
screen | string | — | "1280x720@30" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
shmSize | quantity | — | "1Gi" |
labels | object | — | — |
annotations | object | — | — |
neo4j
A Neo4j graph database on the official Community image. Unlike the other database workloads, Neo4j Community has no operator and does not cluster (clustering is Enterprise), so this is a plain composable http single-instance workload rather than a CR — its graph lives on a PersistentVolume. Community Edition is GPLv3 (fine to run). kurly authors no Secret; NEO4J_AUTH comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves HTTP on :7474 and Bolt on :7687.
server http
The Neo4j server. Graph at /data on the volume. secretName holds NEO4J_AUTH (neo4j/<password>, envFrom). Compose an exposure onto the HTTP port; route Bolt (:7687) as TCP. Clustering/HA needs Neo4j Enterprise, beyond this recipe.
Import: import 'github.com/metio/kurly/workloads/neo4j/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "neo4j" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "neo4j" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
netalertx
A NetAlertX server (scans a network, keeps an inventory of the devices it finds, and notifies when one appears, disappears or changes). A plain composable http workload keeping its configuration and SQLite database under /data on a PersistentVolume. What it can see is the POD network: on a CNI overlay arp-scan reaches the pod subnet and nothing behind it, so a default deployment inventories other pods rather than the LAN — reaching real devices means host networking with NET_RAW/NET_ADMIN, which leaves the restricted posture behind. The entrypoint prepares /data as root and drops to its own account with su-exec, so root, escalation and the default capability set are all required; everything outside /data is written under /tmp, so the root filesystem stays read-only. Service links are off because the injected NETALERTX_PORT collides with the image own NETALERTX_* environment. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :20211.
server http
The NetAlertX server. Configuration and SQLite under /data on the volume; logs, the rendered nginx configuration and the API snapshots under /tmp. Scans whatever network the pod sits on, which on an overlay CNI is the pod subnet. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/netalertx/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "netalertx" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
netbootxyz
A netboot.xyz server — a self-hosted network-boot menu and asset server for PXE-booting installers and tools. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The A netboot.xyz server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/netbootxyz/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "netbootxyz" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
netbox
A NetBox server (the IPAM/DCIM source of truth: IP address management, data-center infrastructure modelling, and a full REST/GraphQL API). Two composable stages on the community image — server (the web front end) and worker (the RQ background task worker) — with PostgreSQL and Redis external. Pairs with the cnpg-cluster and valkey workloads. Single writer over a ReadWriteOnce media volume: the server is one replica, recreated; the worker scales horizontally.
server http
The NetBox web front end, serving the UI and API on :8080. dbHost/dbName/dbUser default to a cnpg-cluster named netbox-db; redisHost to a valkey named netbox-cache (queue on Redis DB 0, cache on DB 1). secretName is the Secret the image reads at /run/secrets — secret_key (Django SECRET_KEY, keep it stable), db_password, and superuser_password on first boot. allowedHosts is Django ALLOWED_HOSTS. kurly authors no Secret. Compose an exposure onto the HTTP port; run a worker alongside.
Import: import 'github.com/metio/kurly/workloads/netbox/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "netbox" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
dbHost | string | — | "netbox-db-rw" |
dbName | string | — | "netbox" |
dbUser | string | — | "netbox" |
redisHost | string | — | "netbox-cache" |
secretName | string | — | "netbox" |
allowedHosts | string | — | "*" |
superuserName | string | — | "admin" |
superuserEmail | string | — | "admin@example.com" |
skipSuperuser | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"300m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The NetBox RQ background worker, draining the high/default/low queues (webhooks, report/script runs, housekeeping). Same image and Secret as the server, no Service. dbHost/dbName/dbUser/redisHost/secretName match the server. Scales horizontally via replicas — workers coordinate through the shared Redis queue. A NetBox deployment needs at least one.
Import: import 'github.com/metio/kurly/workloads/netbox/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "netbox-worker" |
image | string | — | — |
dbHost | string | — | "netbox-db-rw" |
dbName | string | — | "netbox" |
dbUser | string | — | "netbox" |
redisHost | string | — | "netbox-cache" |
secretName | string | — | "netbox" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nextcloud
A Nextcloud server (a self-hosted content-collaboration platform: file sync and share, calendars, contacts, office documents) on the official Apache image; with the default SQLite backend the whole installation lives on a PersistentVolume. kurly authors no Secret; NEXTCLOUD_ADMIN_* come from a provided Secret via envFrom. Point it at an external PostgreSQL/MySQL and S3 primary storage to scale out. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Nextcloud server. trustedDomains sets NEXTCLOUD_TRUSTED_DOMAINS; secretName holds NEXTCLOUD_ADMIN_* (envFrom). Installation at /var/www/html. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/nextcloud/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nextcloud" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
trustedDomains | string | — | — |
secretName | string | — | "nextcloud" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nginx
An nginx server (the HTTP server and reverse proxy) on the official image. Stateless: it serves whatever its web root holds and keeps nothing, so it is a plain Deployment. The upstream server block listens on :80, which an unprivileged process cannot bind, so the stage mounts its own server block into /etc/nginx/conf.d via a subPath ConfigMap mount; serverConfig replaces it verbatim. Content comes from a volume or ConfigMap composed onto the web root. Serves on :8080 behind a Service on :80.
server http
The nginx server. port is the port the mounted server block listens on and the container exposes; root is the web root it serves; serverConfig replaces the generated server block verbatim. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/nginx/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nginx" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 8080 |
root | string | — | "/usr/share/nginx/html" |
serverConfig | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nginx-proxy-manager
An Nginx Proxy Manager server (a self-hosted reverse-proxy with a web UI, free Let's Encrypt certificates, access lists and custom nginx config) on the official image; its SQLite database and config live on a PersistentVolume. The reverse proxy listens on :80/:443, separate ports that need their own Service. It binds the privileged ports so it runs as root with a writable root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. The admin UI serves on :81.
server http
The Nginx Proxy Manager server. Data at /data; the proxy (:80/:443) needs an extra Service. Compose an exposure onto the admin HTTP port.
Import: import 'github.com/metio/kurly/workloads/nginx-proxy-manager/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nginx-proxy-manager" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
certificateStorageSize | quantity | — | "1Gi" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nocobase
A NocoBase server (a self-hosted, open-source no-code/low-code platform for building internal tools, databases and workflows) on the official image, backed by an external PostgreSQL, with its storage on a PersistentVolume. kurly authors no Secret; the DB_* connection and APP_KEY come from a provided Secret via envFrom. Pairs with a cnpg-cluster named nocobase-db. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The NocoBase server. secretName holds the DB_* connection and APP_KEY (envFrom). Storage at /app/nocobase/storage. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/nocobase/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nocobase" |
image | string | — | — |
dbHost | string | — | "nocobase-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "nocobase" |
dbUser | string | — | "nocobase" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "nocobase" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
node-red
A Node-RED server — a flow-based, low-code programming tool for wiring together APIs, devices and online services; flows and settings on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :1880.
server http
The A Node-RED server. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/node-red/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "node-red" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
note-mark
A Note Mark server (a small web-based Markdown notes app; one Go binary serves both the API and the compiled frontend). A plain composable http workload with the notes, the uploaded assets and the SQLite database on a PersistentVolume. PUBLIC_URL is deployment-specific and the app refuses to start without a valid one, so the default is a localhost URL that boots and is wrong everywhere it is really deployed. AUTH_TOKEN__SECRET signs the session tokens and comes from a Secret kurly does not author. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Note Mark server. Notes, uploaded assets and db.sqlite live at /data on the volume. publicUrl must be the URL a browser reaches this instance at, with no trailing slash — the app validates it and will not start otherwise. secretName holds AUTH_TOKEN__SECRET, base64 of at least 32 bytes, signing the session tokens. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/note-mark/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "note-mark" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
publicUrl | string | — | "http://localhost:8080" |
secretName | string | — | "note-mark" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
numa
A numa resolver (an ad-blocking DNS server in a single Rust binary: forwarding or fully recursive with DNSSEC validation, hosts-style blocklists, conditional forwarding, per-client policies and zone records) on the official image; its numa.toml is its only state, rendered as a ConfigMap. It answers DNS on :53 (TCP/UDP) and DNS-over-TLS on :853, separate Service ports to route as the cluster routes DNS, and serves its dashboard, REST control plane and /metrics on :5380. The control plane is authenticated and numa mints a fresh token on every start, so NUMA_API_TOKEN pins one from the Secret. Binding the privileged ports is granted as NET_BIND_SERVICE alone, leaving the restricted posture otherwise intact. DNSSEC validation requires the recursive upstream mode; a forwarder validates nothing itself. The .numa HTTPS proxy is off, since it needs a certificate authority every client device must trust. Stateless, so any replica count is safe; the data directory is a scratch volume, so a restarted pod regenerates its self-signed DoT certificate.
server http
The numa resolver. upstream sets the resolvers queries are forwarded to (mode=recursive resolves from the root servers, which is also what DNSSEC needs); blocking carries the ad-blocking lists; settings merges over the rest of numa.toml table by table. DNS (:53) and DoT (:853) need routing of their own; compose an exposure onto the API port. secretName holds NUMA_API_TOKEN.
Import: import 'github.com/metio/kurly/workloads/numa/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "numa" |
image | string | — | — |
replicas | int | — | 1 |
upstream | object | — | {"address":["9.9.9.9","1.1.1.1"]} |
blocking | object | — | {"enabled":true} |
settings | object | — | — |
secretName | string | — | "numa" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nzbget
An NZBGet server — an efficient, self-hosted Usenet downloader written in C++. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6789.
server http
The An NZBGet server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/nzbget/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nzbget" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
nzbhydra2
An NZBHydra2 server — a meta-search server that aggregates Usenet indexers behind one search API for the *arr apps. On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5076.
server http
The An NZBHydra2 server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/nzbhydra2/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "nzbhydra2" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
oauth2-proxy
An OAuth2 Proxy server (a reverse proxy and forward-auth service that puts an OAuth2/OIDC login in front of your apps, delegating to Keycloak, authentik, Pocket ID, Google, GitHub) on the official image. Stateless (sessions in a signed cookie or shared Redis): a plain rolling Deployment. kurly authors no Secret; the provider settings, client id/secret and cookie secret come from a provided Secret via envFrom (OAUTH2_PROXY_*). Serves on :4180 — front an app or wire as a reverse proxy forward-auth.
server http
The OAuth2 Proxy server. secretName holds the OAUTH2_PROXY_* provider settings and secrets (envFrom). Front an app (OAUTH2_PROXY_UPSTREAMS) or use as forward-auth at /oauth2/auth.
Import: import 'github.com/metio/kurly/workloads/oauth2-proxy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "oauth2-proxy" |
image | string | — | — |
replicas | int | — | 2 |
secretName | string | — | "oauth2-proxy" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ocular
An Ocular server (a budgeting app tracking spending and budgets across months and years, with dashboard analytics, imports from Google Sheets and JSON export) on the official image. A composable http workload. One container runs two processes: the genesis backend on :8080 and a Caddy in front of it that serves the single-page app and proxies /api/* to it, so only :80 is worth a Service. Accounts and budget data live in the backend own database file on a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Ocular server. The backend database and Caddy state live under /data on the volume; Caddy autosaved config goes to a scratch at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ocular/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ocular" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
tokenExpiration | int | — | 720 |
secretName | string | — | "ocular" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
offen
An Offen Fair Web Analytics server (lightweight web analytics where the people being measured can read and erase what was collected about them). A plain composable http workload with accounts and events in a SQLite database on a PersistentVolume, so it needs no external database. It serves on :3000 rather than the image default :80, because the image reaches :80 through a FILE CAPABILITY on the binary, which a container dropping all capabilities never receives. secretName holds OFFEN_SECRET, which signs sessions: without it a random key is minted at startup and every restart logs everybody out. There is no sign-up page — the first account is created with the offen setup command inside the pod. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Offen server: collector, auditorium and account interface. The SQLite database lives at /var/opt/offen on the volume. Serves on :3000 (OFFEN_SERVER_PORT), keeping the restricted posture instead of relaxing capabilities to bind :80; terminate TLS in the exposure you compose on top. secretName holds OFFEN_SECRET, the session-signing key. Create the first account with the offen setup command inside the running pod. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/offen/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "offen" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "offen" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
olivetin
An OliveTin server (a web interface that turns a handful of shell commands into buttons). A plain composable http workload on the project's own image: its whole state is config.yaml, rendered as a ConfigMap, so it claims no volume and needs no database. Every action is a shell command run INSIDE THIS CONTAINER, which is the security boundary worth thinking about before exposing it — anything the container can do, a button can do. The actions list starts EMPTY rather than carrying the upstream demo set, which invites `cat ~/.bash_history` through a web page. Serves on :1337.
server http
The OliveTin server. actions are OliveTin action definitions, passed through verbatim; config merges over the rendered config.yaml for any of its other settings. Nothing is written outside /tmp, so the root filesystem stays read-only. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/olivetin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "olivetin" |
image | string | — | — |
replicas | int | — | 1 |
actions | array | — | — |
logLevel | string | — | "INFO" |
config | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
ollama
An Ollama server (a self-hosted runtime for running large language models locally, with a simple REST API) on the official image; its downloaded models live on a PersistentVolume. Runs on CPU by default; for GPU acceleration schedule it on a GPU node and request the device resource. The backend open-webui and many AI apps talk to. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :11434.
server http
The Ollama server. Models at /models. Usually reached in-cluster (e.g. from open-webui). Compose an exposure onto the HTTP port only if reached from outside.
Import: import 'github.com/metio/kurly/workloads/ollama/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ollama" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"8Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
ombi
An Ombi server — a request-management portal for Plex, Emby and Jellyfin. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3579.
server http
The An Ombi server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/ombi/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "ombi" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
omnitools
An OmniTools server (a self-hosted collection of everyday utilities: image and video conversion, PDF tools, text and JSON formatting, encoders and generators). About as small as a workload here gets — nginx serving a static bundle, no database, no volume and no Secret. Every tool runs in the browser and files are never uploaded, so it holds no user data at any point and can be scaled out freely. Serves on :80.
server http
The OmniTools server. Static assets only: no volume, no Secret, nothing to back up. Runs as the image's own uid 101 with NET_BIND_SERVICE granted back, rather than as root, so that nginx can bind :80 while everything else stays dropped. replicas is a plain knob here because there is no state to share. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/omnitools/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "omnitools" |
image | string | — | — |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
onedev
A OneDev server (Git hosting, code search, pull requests, issues and a CI/CD engine in one self-contained application). A composable http workload on the project's own image: repositories, attachments and — by default — the embedded database live under /opt/onedev on a PersistentVolume, so nothing else is needed to run it. IT INSTALLS ITSELF INTO THE VOLUME, AS ROOT: the entrypoint copies and upgrades the application tree on every start and the Java service wrapper manages its own child process, so root and privilege escalation are relaxed deliberately — the read-only root filesystem stays and nothing else about the hardened posture is given up. Builds run somewhere: the server can execute CI jobs in its own container, so a OneDev used for CI should point at a Kubernetes executor and give jobs their own pods and limits. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI on :6610 and Git-over-SSH on :6611.
server http
The OneDev server. dbUrl and dbUser point at an external database (a JDBC URL); left unset OneDev keeps its embedded HSQLDB inside the volume, which suits a small instance and is not what a busy one wants. secretName holds hibernate_connection_password for the external case, through envFrom. The startup budget is long because the JVM and the first-run setup take a while, and the probes are by connection because the first request lands on a setup wizard. Compose an exposure onto the HTTP port, and route the SSH port separately if clones over SSH are wanted.
Import: import 'github.com/metio/kurly/workloads/onedev/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "onedev" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
dbUrl | string | — | — |
dbUser | string | — | — |
secretName | string | — | "onedev" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
labels | object | — | — |
annotations | object | — | — |
onloc
An Onloc API server (devices report their position to it and the companion application reads the history back) on the official image, backed by an external PostgreSQL, with uploaded avatars on a PersistentVolume. The API only — the web interface is a separate image kurly does not carry. Pairs with a cnpg-cluster named onloc-db. kurly authors no Secret; DATABASE_URL (a Prisma URL with the password in it) and the two token secrets come from a provided Secret via envFrom, and both token secrets fall back to values published in the source when unset. The container applies the Prisma migrations before it serves, so a first start on an empty database is slow and carries a startup probe. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4000.
server http
The Onloc API server. Uploaded avatars at /app/uploads on the volume; everything else is in PostgreSQL. secretName holds DATABASE_URL, ACCESS_TOKEN_SECRET and REFRESH_TOKEN_SECRET (envFrom) — the two token secrets have published defaults upstream, so supplying them is what stops anybody minting a token. PORT is set to 4000 because the server otherwise listens on 3000. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/onloc/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "onloc" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "onloc" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
onlyoffice
An ONLYOFFICE Document Server (a self-hosted online office suite for collaborative editing, embedded by Nextcloud/Seafile and others) on the official image; its data lives on a PersistentVolume. The app that embeds it points its connector at this URL. kurly authors no Secret; JWT_SECRET comes from a provided Secret via envFrom. The image bundles its own PostgreSQL and RabbitMQ, so it runs as root. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The ONLYOFFICE Document Server. secretName holds JWT_SECRET (envFrom). Data at /var/www/onlyoffice/Data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/onlyoffice/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "onlyoffice" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "onlyoffice" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
open-quartermaster
The Open QuarterMaster core API (items, storage blocks, checkouts and labels; the base station web interface and the plugins are separate images and are not carried here) on the official image, backed by an external MongoDB. Pairs with a mongodb-cluster named open-quartermaster-db. kurly authors no Secret; QUARKUS_MONGODB_CONNECTION_STRING carries the credentials and comes from a provided Secret via envFrom. Every write is behind a bearer token verified against an OIDC provider's public keys (Keycloak upstream, with an oqm realm), named by jwtKeyLocation — kurly carries no identity provider, and unset the API starts and refuses every authenticated call. The Quarkus port is set from the declared port rather than left to the image: the project's own compose file publishes :80 while the image's health check asks :8080. Outgoing Kafka events are off unless a broker is provided. Stateless (files and history live in MongoDB): a plain rolling Deployment. Serves on :8080.
server http
The Open QuarterMaster core API. database is the MongoDB database name; jwtKeyLocation is the OIDC provider JWKS URL bearer tokens are verified against; events turns on the outgoing Kafka messaging that is off without a broker. secretName holds QUARKUS_MONGODB_CONNECTION_STRING (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/open-quartermaster/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "open-quartermaster" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 8080 |
database | string | — | "oqm" |
jwtKeyLocation | string | — | — |
events | bool | — | false |
secretName | string | — | "open-quartermaster" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
open-webui
An Open WebUI server (a feature-rich, self-hosted web interface for chatting with local and remote LLMs — Ollama and any OpenAI-compatible API) on the official image; with the default SQLite backend its database and uploads live on a PersistentVolume. kurly authors no Secret; WEBUI_SECRET_KEY comes from a provided Secret via envFrom. Point it at an external PostgreSQL (DATABASE_URL) to scale past SQLite. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Open WebUI server. ollamaBaseUrl points at an Ollama backend; secretName holds WEBUI_SECRET_KEY (envFrom). Data at /app/backend/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/open-webui/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "open-webui" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
ollamaBaseUrl | string | — | — |
secretName | string | — | "open-webui" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
openbao
An OpenBao server (identity-based secrets and encryption management, the community fork of Vault). A plain composable http workload on the project's own image, using the file storage backend on a PersistentVolume, so a single node needs no external database. It STARTS SEALED and seals again on every restart until somebody unseals it, which is why the probes are by connection: an HTTP health check reports 501 uninitialised and 503 sealed, and a liveness probe reading those would restart a server behaving exactly as designed. TLS is disabled on the listener, because a cluster deployment terminates TLS at the exposure in front of it; disable_mlock is set so the process does not need the IPC_LOCK capability the hardened default drops. Single writer over a ReadWriteOnce volume: one replica, recreated — Raft on a stateful set is what more than one node needs. Serves the API and UI on :8200.
server http
The OpenBao server. The file storage backend keeps everything under /openbao/file on the volume. apiAddr is the address clients and other nodes reach this server at, which OpenBao redirects to — a wrong value shows up as a client following a link to nowhere. config merges over the rendered configuration for any of OpenBao's own settings, including a listener or storage backend of your own. Expect to run `bao operator init` once and to unseal after every rollout. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/openbao/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "openbao" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
apiAddr | string | — | — |
config | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
opencloud
An OpenCloud server (file sync and share with a web interface, WebDAV and desktop/mobile clients), run as ONE process hosting every internal service. A plain composable http workload needing nothing external: the generated configuration and the stored files sit on two separate PersistentVolumes, because the configuration holds the service secrets every internal service authenticates with and blobs without them are unreadable. The image already runs unprivileged, so nothing is relaxed. An init step runs `opencloud init` exactly once — guarded on the config file, since a second run mints new secrets — and reads the first administrator password from a Secret. TLS is terminated in front of the pod (PROXY_TLS=false), and url must name the address users reach it at, since OpenCloud hands it to its own web client. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :9200.
server http
The OpenCloud server. File blobs and the users database live at /var/lib/opencloud, the generated configuration at /etc/opencloud, each on its own volume. url is the public address users reach it at and has no sane default — a wrong one serves a UI that cannot talk to its own backend. secretName holds IDM_ADMIN_PASSWORD, read ONCE by the init step that creates the first administrator; leave the key out and init generates one and prints it to its log. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/opencloud/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "opencloud" |
image | string | — | — |
url | string | — | "https://opencloud.example.com" |
storageSize | quantity | — | "50Gi" |
configSize | quantity | — | "1Gi" |
storageClass | string | — | — |
port | int | — | 9200 |
secretName | string | — | "opencloud" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
opencost
OpenCost, the CNCF cost-monitoring model: it reads resource usage from Prometheus, joins it with pricing, and exposes per-workload cost metrics (and an API) on :9003. A plain composable http workload that carries a ServiceAccount + ClusterRole + ClusterRoleBinding, because attributing cost reads cluster-scoped objects and every namespace. Pairs with the prometheus (or thanos) workload as its data source.
server http
The OpenCost cost model. namespace MUST match where you deploy — it names the ServiceAccount in the cluster RoleBinding, which a cluster-scoped object cannot inherit later. prometheusEndpoint points at the prometheus workload (or a Thanos Query); env carries extra pricing/cloud settings. The web UI is a separate image (opencost-ui); this is the model, scraped at :9003.
Import: import 'github.com/metio/kurly/workloads/opencost/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "opencost" |
namespace | string | — | "opencost" |
image | string | — | — |
prometheusEndpoint | string | — | "http://prometheus-operated.monitoring.svc:9090" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
opengist
An Opengist server (a self-hosted pastebin where every snippet is a Git repository). A plain composable http workload that keeps the repositories, a SQLite database and the Bleve search index on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :6157, with built-in Git over SSH on :2222.
server http
The Opengist server. Keeps gist repositories, the SQLite database and the search index at /opengist on the volume — the entrypoint pins that path, so it is not a parameter. Point OG_DB_URI at external PostgreSQL or MySQL through env to move the database off the volume. OG_SECRET_KEY comes from `secretName`: unset, the app generates one per start and every restart logs everyone out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/opengist/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "opengist" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
externalUrl | string | — | — |
secretName | string | — | "opengist" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
openhab
An openHAB server (a vendor-neutral, self-hosted home-automation platform integrating a huge range of devices behind one engine, UI and rule system) on the official image; its three persistent directories — config (/openhab/conf), runtime userdata (/openhab/userdata) and installed add-ons (/openhab/addons) — each get their own PersistentVolume (kurly.store composed three times). USB/serial radios are hardware and not modelled. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :8080.
server http
The openHAB server. confSize/userdataSize/addonsSize size the three PVCs. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/openhab/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "openhab" |
image | string | — | — |
confSize | quantity | — | "2Gi" |
userdataSize | quantity | — | "5Gi" |
addonsSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
openhabittracker
An OpenHabitTracker server (habits, tasks and notes in one place, with time tracking, a calendar view and completion statistics). A plain composable http workload keeping its database on a PersistentVolume. The published image is a Blazor Server app for EXACTLY ONE USER: the account comes from the Secret, there is no registration and there is no second account to add, so an instance per person is the deployment model rather than a limit of this workload. Deliberately less hardened: a request logger writes inside the install tree, so it runs root with a writable root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The OpenHabitTracker server. Its database lives at /app/.OpenHabitTracker on the volume, beside the application code. secretName holds the one account the instance has (APPSETTINGS_USERNAME, APPSETTINGS_EMAIL, APPSETTINGS_PASSWORD) plus APPSETTINGS_JWT_SECRET, which signs its sessions. Its request logger opens a LiteDB file inside the install tree on the FIRST request rather than at startup, so a read-only root filesystem would leave the pod healthy and every request failing — this workload is deliberately less hardened, writable-rooted and root-run. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/openhabittracker/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "openhabittracker" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "openhabittracker" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
openreader
An OpenReader server (a text-to-speech reader for EPUB, PDF, Markdown, plain text and DOCX documents, with the spoken words highlighted as playback follows along). A plain composable http workload with no external dependency: the image carries its own SQLite database, an embedded SeaweedFS blob store and an embedded NATS JetStream, all under one directory on a PersistentVolume. The Service publishes :8333 beside the app on :3003, because uploads and downloads are PRESIGNED and fetched by the browser directly from the blob store — an exposure that forgets it accepts documents nowhere. BASE_URL is what those presigned URLs and the session cookies are minted against, so it has no usable default and must be the address users actually reach. Speech itself is not in the image: point apiBase at a TTS service. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The OpenReader server. The SQLite metadata database, the uploaded documents and the JetStream state all live at /app/docstore on the volume. secretName holds AUTH_SECRET, which signs the sessions, and optionally API_KEY for the TTS provider. apiBase and API_KEY are read ONCE, on the first boot, and administered from the web interface afterwards. Runs as the node account the image ships (uid 1000) with the hardened defaults intact. Compose an exposure onto the HTTP port, and remember the S3 port.
Import: import 'github.com/metio/kurly/workloads/openreader/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "openreader" |
image | string | — | — |
baseUrl | string | — | "https://openreader.example.com" |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
apiBase | string | — | — |
secretName | string | — | "openreader" |
adminEmails | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
opensearch-cluster
A highly-available OpenSearch cluster as an OpenSearch Operator OpenSearchCluster custom resource, with optional OpenSearch Dashboards. OpenSearch is the Apache-2.0 fork of Elasticsearch — no SSPL/Elastic-License restriction on offering it as a service, the right default for a platform that monetizes hosting. Authors the CR directly like cnpg-cluster; composed by parameter, not by + feature. Requires the OpenSearch Operator.
cluster opensearch
The OpenSearchCluster CR. replicas is the default node pool size (each node is cluster_manager+data+ingest; split into dedicated pools via the raw + hatch for large clusters). dashboards runs OpenSearch Dashboards alongside. Render with kurly.list.
Import: import 'github.com/metio/kurly/workloads/opensearch-cluster/cluster.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "opensearch" |
replicas | int | — | 3 |
opensearchVersion | string | — | "3.7.0" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
dashboards | bool | — | true |
dashboardsReplicas | int | — | 1 |
labels | object | — | — |
annotations | object | — | — |
organizr
An Organizr server (a self-hosted HTPC/homelab services dashboard that ties your apps together behind one tabbed interface with authentication) on the official image (pinned by digest; Renovate maintains it); SQLite config on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Organizr server. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/organizr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "organizr" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
otel-collector
An OpenTelemetry Collector as a per-node agent (a DaemonSet), so local workloads send telemetry to a collector on their own node. The collector config — receivers, processors, exporters, and the pipelines wiring them — is passed verbatim (it is the collector's schema, not kurly's); the default is a working OTLP agent that receives, batches, and prints. Fully restricted by default: node-log collection needs a hostPath and is a documented opt-in.
agent daemon
The per-node collector. config is the collector's own document, rendered straight into the mounted config file; the default receives OTLP on 4317/4318, guards memory, batches, and exports to the debug logger, with a health_check extension on 13133 backing the probes. Replace config for real pipelines, and move the probes if it drops health_check.
Import: import 'github.com/metio/kurly/workloads/otel-collector/agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "otel-collector" |
image | string | — | — |
config | object | — | — |
otterwiki
An Otter Wiki server (a small Markdown wiki where every page is a file in a git repository and every edit is a commit). A plain composable http workload with the repository on a PersistentVolume. It starts PUBLICLY READABLE AND WRITABLE and the first account registered becomes the administrator, so an exposed instance hands both to whoever arrives first — set the permissions before publishing it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Otter Wiki server. The git repository and its index live at /app-data on the volume. supervisord runs nginx and the application together and drops privileges to their accounts, which it can only do from root, so this workload is deliberately less hardened. Set anonymous read/write and registration from the wiki's own settings page before exposing it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/otterwiki/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "otterwiki" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
our-shopping-list
An Our Shopping List server (shared shopping and todo boards, synchronised live between everyone looking at one) on the official image, backed by an external MongoDB. The database connection is a host, a port and a database name and nothing else: upstream states MongoDB authentication is not supported yet, so there is no credential to carry and kurly declares no Secret — reach the database over a network it is not otherwise exposed on. Boards update over socket.io, so an exposure that does not upgrade the connection leaves a UI that loads and never moves for anybody but the person typing. baseUrl serves the app under a path instead of the web root, and the health endpoint moves with it. Stateless (every board and item lives in MongoDB, the client bundle is baked into the image): a plain rolling Deployment. Serves on :8080.
server http
The Our Shopping List server. dbHost, dbPort and dbName are the whole database connection — the application supports no MongoDB authentication, so there is nothing to put in a Secret. baseUrl serves the app under a path rather than the web root. Compose an exposure onto the HTTP port, one that upgrades WebSocket connections.
Import: import 'github.com/metio/kurly/workloads/our-shopping-list/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "our-shopping-list" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 8080 |
dbHost | string | — | "mongodb" |
dbPort | int | — | 27017 |
dbName | string | — | "osl" |
baseUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
overleaf
An Overleaf server (the Community Edition of the collaborative LaTeX editor) on the official monolith image, backed by an external MongoDB (a replica set — it uses transactions) and Redis, with projects and compiles on a PersistentVolume. kurly ships no MongoDB recipe; bring your own (Redis can be the valkey workload). The image spawns TeX compiles and writes across the root filesystem, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; OVERLEAF_MONGO_URL and OVERLEAF_INVITE_TOKEN_SECRET come from a provided Secret via envFrom — the container refuses to start without the latter, and regenerating it invalidates every invitation token already issued. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Overleaf server. redisHost defaults to a valkey named overleaf-cache; siteUrl is the public URL; appName the instance name. secretName holds OVERLEAF_MONGO_URL, pointing at a MongoDB replica set you provide, and OVERLEAF_INVITE_TOKEN_SECRET, which must stay stable across restarts (envFrom). Projects and compiles at /var/lib/overleaf. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/overleaf/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "overleaf" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
redisHost | string | — | "overleaf-cache" |
siteUrl | string | — | — |
appName | string | — | "Overleaf" |
secretName | string | — | "overleaf" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
owncast
An Owncast server (a self-hosted live video streaming and chat server, an open alternative to Twitch) on the official image; its data (SQLite config, chat history, stream segments) lives on a PersistentVolume. Streaming IN uses RTMP on :1935, a separate port this HTTP workload does not expose — add a Service for it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web player on :8080.
server http
The Owncast server. Data at /app/data. RTMP ingest (:1935) needs an extra Service composed on. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/owncast/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "owncast" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"250m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
owntracks-recorder
An OwnTracks Recorder server (a self-hosted store and web UI for the location data OwnTracks phone apps publish) on the official image. A plain composable http workload that keeps its location store on a PersistentVolume under /store. Phone apps can publish over HTTP directly or via an MQTT broker the Recorder subscribes to. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8083.
server http
The OwnTracks Recorder server. Store at /store on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/owntracks-recorder/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "owntracks-recorder" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | {"OTR_STORAGEDIR":"/store"} |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
oxicloud
An OxiCloud server (file storage and sharing with a web interface, written in Rust). A plain composable http workload on the project's own image: the files go to a PersistentVolume and the metadata to an external PostgreSQL. baseUrl is what OxiCloud builds share links and OIDC redirects from, so behind a reverse proxy it has to be the address a browser reaches — share links that resolve only inside the cluster are that value left at its default. The image's entrypoint chowns the storage volume and drops privileges with su-exec when it starts as root, and says plainly that an unprivileged start assumes the volume permissions are already right, which is what fsGroup does; the stage runs as the image's own uid 1001 and keeps the hardened posture. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8086.
server http
The OxiCloud server. The blob store lives at /app/storage on the volume. secretName holds OXICLOUD_DB_CONNECTION_STRING through envFrom — the PostgreSQL URL, whose password is why it is a Secret rather than an env value. baseUrl is the URL a browser reaches this at. Everything else OxiCloud reads is OXICLOUD_*, through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/oxicloud/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "oxicloud" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
secretName | string | — | "oxicloud" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pairdrop
A PairDrop server (a self-hosted, AirDrop-style local file-sharing app: transfer files and messages between devices peer-to-peer via WebRTC) on the official image. The server only brokers peer connections and keeps no state. One replica: peers pair through in-memory rooms held by a single server instance. Serves on :3000.
server http
The PairDrop server. Stateless signaling; runs as one replica. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pairdrop/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pairdrop" |
image | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
paisa
A Paisa server (a plain-text, double-entry personal finance manager built on ledger/beancount journals). A plain composable http workload that reads its configuration and journal from a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI on :7500.
server http
The Paisa server, run from /data on the volume where it finds paisa.yaml and the referenced journal (provide them before first use). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/paisa/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "paisa" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
paperless-ngx
A Paperless-ngx server (scan, index, and archive documents with OCR and full-text search) backed by an external PostgreSQL and Redis, with its data/media/consume/export trees on a PersistentVolume. The image runs the web server and Celery workers together. Pairs with a cnpg-cluster named paperless-db and a valkey named paperless-cache. The entrypoint writes to the root filesystem, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. kurly authors no Secret; secrets come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The Paperless-ngx server. dbHost/dbName/dbUser default to a cnpg-cluster named paperless-db; redisHost to a valkey named paperless-cache. url is the public URL; adminUser the first-run admin. secretName holds PAPERLESS_DBPASS, PAPERLESS_SECRET_KEY, and PAPERLESS_ADMIN_PASSWORD (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/paperless-ngx/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "paperless-ngx" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
dbHost | string | — | "paperless-db-rw" |
dbName | string | — | "paperless" |
dbUser | string | — | "paperless" |
redisHost | string | — | "paperless-cache" |
url | string | — | — |
adminUser | string | — | "admin" |
secretName | string | — | "paperless-ngx" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
parse-server
A Parse Server (a backend-as-a-service: a REST and GraphQL API over a document store, with users, sessions, files, push notifications and cloud functions). A plain composable http workload on the community image; all state is in the external database, so it claims no volume and scales horizontally. THE MASTER KEY IS THE WHOLE SECURITY MODEL — a request carrying it bypasses every class-level permission and ACL, so it lives in the Secret secretName names, is never shipped to a client, and cannot be rotated without updating everything that holds it. serverUrl is the address Parse hands to clients and writes into generated links, mount path included; a value right for the cluster and wrong for the internet produces working API calls and broken file downloads. The default file adapter writes into the database, which is why no volume is needed. Serves on :1337.
server http
The Parse Server. appId is the application id clients send and is not a secret; serverUrl is the URL clients reach this at, mountPath included; mountPath is the path the API is served under. secretName holds PARSE_SERVER_MASTER_KEY and PARSE_SERVER_DATABASE_URI through envFrom — the database password is embedded in that URI. Everything else Parse reads is PARSE_SERVER_*, through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/parse-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "parse-server" |
image | string | — | — |
replicas | int | — | 1 |
appId | string | — | "parse" |
serverUrl | string | — | "http://parse-server:1337/parse" |
mountPath | string | — | "/parse" |
secretName | string | — | "parse-server" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
part-db
A Part-DB server (an inventory for electronic components: parts, stock levels, storage locations and the datasheets that belong to them). A plain composable http workload on the official Apache/PHP image, keeping its database, attachments and public media on PersistentVolumes. It defaults to SQLite inside the uploads volume, so it needs nothing external to start; databaseUrl points it at a MySQL or PostgreSQL instead, and since that DSN carries the password it belongs in a Secret read with envFrom rather than in the rendered manifest. Apache and php-fpm start as root, bind :80 and chown the volumes before dropping to www-data, so this workload is deliberately less hardened, with a writable root filesystem for the Symfony cache. The first boot runs the Doctrine migrations before anything answers, which the startup probe budget allows for. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :80.
server http
The Part-DB server. Attachments and, by default, the SQLite database at /var/www/html/uploads; publicly served media at /var/www/html/public/media, each on its own volume. databaseUrl is a Doctrine DSN — leave it unset for SQLite, or set mysql://… or postgresql://… and supply the credentials through a Secret. The first account is created interactively on first visit, so kurly mints no Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/part-db/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "part-db" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
mediaStorageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
databaseUrl | string | — | — |
instanceName | string | — | "Part-DB" |
defaultLanguage | string | — | "en" |
defaultTimezone | string | — | "UTC" |
baseCurrency | string | — | "EUR" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
passwordpusher
A Password Pusher server (share passwords and secrets over self-destructing, expiring links) on the official image, backed by an external PostgreSQL. Stateless — its state lives in the database, so it can run several replicas. kurly authors no Secret; DATABASE_URL and SECRET_KEY_BASE come from a provided Secret via envFrom. Pairs with a cnpg-cluster named passwordpusher-db. Serves on :5100.
server http
The Password Pusher server. secretName is the Secret holding DATABASE_URL (with the embedded DB password) and SECRET_KEY_BASE, pulled in via envFrom; kurly mints none. Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/passwordpusher/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "passwordpusher" |
image | string | — | — |
secretName | string | — | "passwordpusher" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pdfding
A PdfDing server (a PDF manager, viewer and editor with tags, search and reading progress). A plain composable http workload keeping the uploaded PDFs and the SQLite database together under DATA_DIR on a PersistentVolume — no external database. HOST_NAME is Django's ALLOWED_HOSTS, so every Host it is not set to is answered 400 before any view runs; the probes therefore check the listening socket rather than an HTTP path. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The PdfDing server. Uploaded PDFs and the SQLite database live under dataDir on the volume. hostName must name every host the instance answers on. secretName holds SECRET_KEY, which signs sessions and password-reset links. Point DATABASE_TYPE=POSTGRES and the POSTGRES_* variables at an external server through env to move the database off the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pdfding/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pdfding" |
image | string | — | — |
hostName | string | — | "pdfding.example.com" |
dataDir | string | — | "/data" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "pdfding" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
peer-calls
A Peer Calls server (group video calls in the browser over WebRTC, peer-to-peer or through its own SFU). A plain composable http workload on the project's own image: nothing is written down, so it claims no volume and needs no database. Signalling state lives in the process, so two pods answering the same room do not see each other's peers and the call silently splits — one replica unless redisHost names a Redis to share that state through. TLS is not optional in practice: a browser will not hand a page its camera or microphone unless the page came over HTTPS. Serves the web app and the signalling WebSocket on :3000.
server http
The Peer Calls server. redisHost/redisPort switch the signalling store from process memory to Redis, which is what makes a second replica safe. Everything else Peer Calls reads is PEERCALLS_*, through env. Compose an exposure onto the HTTP port, and terminate TLS there.
Import: import 'github.com/metio/kurly/workloads/peer-calls/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "peer-calls" |
image | string | — | — |
replicas | int | — | 1 |
redisHost | string | — | — |
redisPort | int | — | 6379 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
peertube
A PeerTube server (a decentralized, federated video platform) on the official image, backed by an external PostgreSQL and Redis, with videos/uploads/config on a PersistentVolume. Pairs with a cnpg-cluster named peertube-db and a valkey named peertube-cache. kurly authors no Secret; PEERTUBE_DB_PASSWORD, PEERTUBE_SECRET, and the initial root password come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9000.
server http
The PeerTube server. dbHost/dbName/dbUser default to a cnpg-cluster named peertube-db; redisHost to a valkey named peertube-cache. webserverHost is the public hostname (required for federation). secretName holds PEERTUBE_DB_PASSWORD, PEERTUBE_SECRET, PT_INITIAL_ROOT_PASSWORD (envFrom). Videos/uploads at /data, config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/peertube/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "peertube" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
dbHost | string | — | "peertube-db-rw" |
dbName | string | — | "peertube" |
dbUser | string | — | "peertube" |
redisHost | string | — | "peertube-cache" |
webserverHost | string | — | — |
secretName | string | — | "peertube" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
pelican-panel
A Pelican Panel server (a web control panel for running and managing game servers) on the official image. A plain composable http workload that keeps its SQLite database, its .env, uploads and plugins under /pelican-data on a PersistentVolume — no external database by default, though MySQL or PostgreSQL can be pointed at through env. The image runs as its own www-data user and Caddy binds the privileged :80 through a file capability, so NET_BIND_SERVICE is granted back on top of dropped-ALL and privilege escalation is allowed; the root filesystem is writable because the Laravel tree is optimized and cached in place. BEHIND_PROXY keeps Caddy on plain :80 with automatic HTTPS off, which is what an Ingress or HTTPRoute terminating TLS in front of it wants. The entrypoint mints an APP_KEY into the volume on first start, so kurly authors no Secret. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Pelican Panel server. All state — the SQLite database, .env, uploads, plugins — lives at /pelican-data on the volume. appUrl is the public URL the panel builds links and assets against; behindProxy (default) keeps Caddy on plain :80 and leaves TLS to the proxy in front. secretName is optional and only needed to pin APP_KEY or supply external database credentials via envFrom. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pelican-panel/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pelican-panel" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
behindProxy | bool | — | true |
trustedProxies | string | — | "0.0.0.0/0 ::/0" |
dbConnection | string | — | "sqlite" |
databasePath | path | — | "/pelican-data/database/database.sqlite" |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
percona-server
A Percona Server for MySQL instance — a drop-in MySQL replacement with the instrumentation Percona adds: better diagnostics, an audit plugin and XtraDB. A composable stateful workload, because a database wants a stable identity and a volume that follows it. ONE SERVER, NOT A CLUSTER: no replication, no failover and no automatic backup, so losing the node means restoring from whatever somebody else took — compose a backup axis onto it, and reach for an operator when the database matters more than the simplicity. THE ROOT PASSWORD IS READ ONCE: the entrypoint initialises the data directory on first start from the Secret and never reads it again, so changing MYSQL_ROOT_PASSWORD later changes nothing. Probed by connection with a long startup budget, because a first start that builds the data directory takes minutes. Serves MySQL on :3306 and the X protocol on :33060.
server stateful
The Percona Server instance. database and user are created on first start alongside root; secretName holds MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD through envFrom. config is merged into a my.cnf fragment mounted at /etc/my.cnf.d. The data directory is /var/lib/mysql on the volume. One replica: raising it makes several unrelated databases, not a cluster.
Import: import 'github.com/metio/kurly/workloads/percona-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "percona-server" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
database | string | — | — |
user | string | — | — |
secretName | string | — | "percona-server" |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
pgadmin
A pgAdmin 4 server (the web UI for administering PostgreSQL) on the official image; its session and configuration store (SQLite) lives on a PersistentVolume. kurly authors no Secret; PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD come from a provided Secret via envFrom. The image runs as uid 5050. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The pgAdmin server. secretName holds PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD (envFrom). Config at /var/lib/pgadmin. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pgadmin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pgadmin" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "pgadmin" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
photoprism
A PhotoPrism server (an AI-powered, self-hosted photo-management app with face recognition and automatic tagging) on the official image. A plain composable http workload — with the SQLite backend its database, cache, and originals live on a PersistentVolume, no external database. The image runs TensorFlow indexing and writes the rootfs, relaxing read-only-rootfs while keeping non-root and dropped capabilities. kurly authors no Secret; PHOTOPRISM_ADMIN_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :2342.
server http
The PhotoPrism server. Storage at /photoprism/storage, originals at /photoprism/originals, both on the volume. siteUrl is the public URL (keep the trailing /). secretName holds PHOTOPRISM_ADMIN_PASSWORD (envFrom). Point PHOTOPRISM_DATABASE_DRIVER at external MariaDB (mysql-cluster) to scale out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/photoprism/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "photoprism" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
siteUrl | string | — | — |
adminUser | string | — | "admin" |
secretName | string | — | "photoprism" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
photoview
A Photoview server (a self-hosted photo gallery for photographers that scans a media library, builds albums and serves them with face recognition and RAW support) on the official image, backed by an external MySQL/MariaDB or PostgreSQL, with TWO PersistentVolumes — the media library at /photos and a thumbnail cache at /app/cache (kurly.store composed twice). kurly authors no Secret; the database driver and connection come from a provided Secret via envFrom. Pairs with a mysql-cluster named photoview-db. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :80.
server http
The Photoview server. mediaSize/cacheSize size the two PVCs (/photos and /app/cache); secretName holds PHOTOVIEW_DATABASE_DRIVER and the connection URL (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/photoview/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "photoview" |
image | string | — | — |
mediaSize | quantity | — | "100Gi" |
cacheSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "photoview" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
phpmyadmin
A phpMyAdmin server (the classic web UI for administering MySQL and MariaDB) on the official image. Stateless: a plain rolling Deployment that connects to the MySQL host you point it at. Serves on :80.
server http
The phpMyAdmin server. dbHost sets PMA_HOST (the MySQL/MariaDB host). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/phpmyadmin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "phpmyadmin" |
image | string | — | — |
replicas | int | — | 2 |
dbHost | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
picoshare
A PicoShare server (a minimal file-sharing app: upload a file, get a link, optionally with an expiry). A plain composable http workload whose files AND metadata live in one SQLite database on a PersistentVolume — uploaded content is stored as blobs inside the database rather than beside it, so the volume must be sized for everything hosted, not for an index. One shared passphrase grants upload and administration. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4001.
server http
The PicoShare server. Everything lives in SQLite at /data/store.db on the volume, uploaded file contents included — size storageSize for the files themselves. secretName holds PS_SHARED_SECRET, the single passphrase granting upload and administration; PicoShare has one account and this is it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/picoshare/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "picoshare" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "picoshare" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pictshare
A PictShare server (image, video and paste hosting with a resizing URL API: the size, rotation and format are path segments, so a link asks for a variant and the server produces it). A plain composable http workload keeping the uploads on a PersistentVolume. ANYONE WHO CAN REACH IT CAN UPLOAD unless UPLOAD_CODE or ALLOWED_SUBNET says otherwise — that is the design, so the Secret is the difference between a private service and a public file host. Deliberately less hardened: the entrypoint rewrites php.ini, writes its config into the application tree and starts a local redis cache, so the root filesystem is writable and it runs as root, with NET_BIND_SERVICE granted back so Caddy can bind :80 and DAC_OVERRIDE so root may enter the directories the image gives to the redis account. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The PictShare server. Uploads live at /app/public/data on the volume; the redis it starts is a cache inside the pod, rebuilt after a restart and turned off with REDIS_CACHING=false in env — REDIS_SERVER and REDIS_PORT default to localhost:6379, since the image's own socket default makes phpredis resolve a file path as a hostname and every page 500s. secretName holds UPLOAD_CODE, MASTER_DELETE_CODE and ADMIN_PASSWORD — who may upload, who may delete anything, and who reaches the admin page; empty codes mean no code is required. The S3 and FTP backends are configured entirely through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pictshare/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pictshare" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "pictshare" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"768Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pigallery-2
A PiGallery2 server (a directory-first photo gallery that serves the folder tree the photos already live in, with search, faces and a map view) on the official image. Its SQLite database, generated thumbnails and the media library all sit under /app/data on one PersistentVolume (config, db, images, tmp), so it needs no external database. The image creates none of those directories and then runs on defaults it can never save, so each is mounted back out of the same volume through a subPath, which kubelet creates when it is missing. The server binds :80 and nothing moves it — neither PORT nor --Server-port is read — which needs no privilege in Kubernetes, so the hardened default posture stands. kurly authors no Secret and the server needs none. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The PiGallery2 server. storageSize sizes the single volume at /app/data (config, SQLite database, the media library at /app/data/images and the thumbnail cache). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pigallery-2/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pigallery-2" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pihole
A Pi-hole server (a self-hosted, network-wide DNS sinkhole that blocks ads and trackers, with a web admin dashboard) on the official image; its config and query database live on a PersistentVolume. It answers DNS on :53 (TCP/UDP), separate ports to add a Service for. kurly authors no Secret; the admin password (FTLCONF_webserver_api_password) comes from a provided Secret via envFrom. It binds the privileged DNS port so it runs as root with a writable root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the admin dashboard on :80.
server http
The Pi-hole server. timezone sets TZ; secretName holds FTLCONF_webserver_api_password (envFrom). Config at /etc/pihole; DNS (:53) needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pihole/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pihole" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
timezone | string | — | "UTC" |
secretName | string | — | "pihole" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
piler
A piler server (an email archive: messages kept deduplicated and encrypted under a retention policy, searchable full text and exportable for an audit). A composable http workload backed by an external MySQL/MariaDB, an external memcached and an external Manticore Search. TWO volumes, and the second is not a convenience: /etc/piler holds piler.key, generated on first start, and every archived message is encrypted with it — an archive whose configuration volume was thrown away is an archive nothing can read. The Manticore server has to be started against piler's own manticore.conf from that volume, since the index definitions are piler's rather than Manticore's defaults, and that wiring is a deployment decision this workload does not make. The entrypoint stays root and starts cron, php-fpm, nginx and rsyslogd through their init scripts, each dropping to its own account, so this workload is deliberately less hardened. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves the web interface on :80 and takes mail on :25, which is SMTP and no exposure can carry.
server http
The piler server. Archived messages at /var/piler/store, the generated configuration — piler.conf, the nginx site, config-site.php, the TLS material and piler.key — at /etc/piler, both on their own volume. hostname becomes nginx's server_name, the hostid in piler.conf and the base of every link the web interface builds, so it has no useful default. The schema is created on first start into a database that must already exist. secretName holds MYSQL_PASSWORD; ADMIN_USER_PASSWORD_HASH may live there too and resets the built-in administrator on every start, but it is a hash rather than a password and nothing can mint it. memcached is not optional — the entrypoint writes MEMCACHED_ENABLED=1 unconditionally. Compose an exposure onto the HTTP port; :25 needs the mail system to reach the Service directly.
Import: import 'github.com/metio/kurly/workloads/piler/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "piler" |
image | string | — | — |
hostname | string | — | "piler.example.com" |
storageSize | quantity | — | "50Gi" |
configSize | quantity | — | "1Gi" |
storageClass | string | — | — |
dbHost | string | — | "piler-db" |
database | string | — | "piler" |
dbUser | string | — | "piler" |
memcachedHost | string | — | "memcached" |
manticoreHost | string | — | "manticore" |
realtimeIndex | bool | — | false |
secretName | string | — | "piler" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pilos
A PILOS server (an open-source, Laravel-based front-end for BigBlueButton from TH Mittelhessen: room and meeting management with LDAP/OIDC support) on the official all-in-one image (nginx + php-fpm), backed by an external PostgreSQL and Redis, with uploaded assets on a PersistentVolume. It reaches an existing BigBlueButton server over the network; kurly does not run BBB itself. Pairs with a cnpg-cluster named pilos-db and a Redis. kurly authors no Secret; the database/Redis/APP_KEY/BBB settings come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated; the bundled nginx master needs root and a writable rootfs. Serves on :80.
server http
The PILOS server. secretName holds the database/Redis/APP_KEY/BBB settings (envFrom). Uploaded assets at /var/www/html/storage/app. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pilos/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pilos" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "pilos" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pinchflat
A Pinchflat server (an automated YouTube archiver built on yt-dlp: point it at channels or playlists and it downloads new uploads on a schedule, named and tagged for a media server to pick up). A plain composable http workload with two PersistentVolumes — a small one for its SQLite database and a large one for media, which grows without limit. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves the web UI on :8945.
server http
The Pinchflat server. SQLite and configuration at /config, downloaded media at /downloads — sized separately because a few subscribed channels fill tens of gigabytes. It checks every path it uses at startup and exits on the first read-only one, which is why /etc/yt-dlp is scratch as well as /tmp. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pinchflat/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pinchflat" |
image | string | — | — |
mediaSize | quantity | — | "100Gi" |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pinepods
A PinePods server (a podcast manager several people share: subscriptions, play positions and downloads live in one database, so an episode paused on a phone resumes on a laptop). A composable http workload backed by an external PostgreSQL, with downloaded episodes and backups on a PersistentVolume. It also needs a Redis the background workers coordinate through (redisHost/redisPort): the API process exits on a cache it cannot resolve and nginx then answers every request 502, which reads as a slow start rather than a missing dependency. Coordinates are env from dbHost/database/dbUser; a provided Secret holds DB_PASSWORD and the first administrator's PASSWORD (kurly authors none) — left out, the container invents that password and prints it to the log. supervisord runs nginx, the FastAPI process and cron together as root and the entrypoint chowns the mail spool, so root, capabilities and a writable root filesystem are relaxed. The Service shares the workload name, which would inject PINEPODS_PORT as a tcp:// URL over the value the API builds its links from, so service links are off. The first boot waits for the database, creates the schema and the administrator before anything answers, and is startup-probed. Podcast search is resolved through the project's own API over the internet. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8040.
server http
The PinePods server, serving the web app on :8040 — nginx serves the compiled front end and proxies /api to the FastAPI process on :8032, which is never exposed. Downloads, backups and certificates live at /opt/pinepods on the volume; everything else is in PostgreSQL. dbHost/database/dbUser default to a cnpg-cluster named pinepods-db, and the database itself must already exist. serverHost is the host clients reach this instance on, which the API builds the links it hands out from — unset, the container falls back to the pod name. Set proxyProtocol/reverseProxy when an exposure terminates TLS in front of it, or the generated links stay http:// and a browser refuses them. adminFullname/adminUsername/adminEmail describe the first administrator, whose PASSWORD is in the Secret alongside DB_PASSWORD (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pinepods/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pinepods" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
dbHost | string | — | "pinepods-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "pinepods" |
dbUser | string | — | "pinepods" |
redisHost | string | — | "pinepods-cache-headless" |
redisPort | int | — | 6379 |
serverHost | string | — | — |
proxyProtocol | string | — | "http" |
reverseProxy | bool | — | false |
searchApiUrl | string | — | "https://api.pinepods.online/api/search" |
adminFullname | string | — | "Pinepods Admin" |
adminUsername | string | — | "pine-admin" |
adminEmail | string | — | "admin@pinepods.online" |
secretName | string | — | "pinepods" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
piwigo
A Piwigo server — a self-hosted photo gallery with albums, tagging and user management (backed by MySQL/MariaDB). On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Piwigo server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/piwigo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "piwigo" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
plik
A Plik server (temporary file upload and sharing, with a web app, a REST API and a command-line client). A plain composable http workload on the project's own image: uploads land on a PersistentVolume and their metadata in a SQLite database beside them, so no external database is needed. The rendered plikd.cfg names ABSOLUTE paths, because Plik resolves its defaults relative to a working directory inside the read-only image and would have nowhere to write. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Plik server. Uploads and the SQLite database live under /data on the volume. plikDomain is the public URL Plik builds download links against and checks OAuth redirects with — unset, links follow the Host of the request, which is right behind one ingress and wrong behind two. config merges over the rendered plikd.cfg for any of Plik's other settings. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/plik/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "plik" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
plikDomain | string | — | — |
config | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
plone
A Plone backend (a content management system with workflow, versioning and per-object permissions, serving its content and the REST API the Volto frontend talks to). A plain composable http workload with the object database — Data.fs, its blobs and the template cache — on a PersistentVolume. THE IMAGE SHIPS admin/admin: Zope creates its first Manager account from /app/inituser and then keeps it in the database, so adminSecretName mounting a Secret over that file before the first start is the only moment the credential can be chosen. The root filesystem is writable because the entrypoint rewrites the instance's own configuration in /app at every start. Single writer over a ReadWriteOnce volume: one replica, recreated — two Zope processes writing one Data.fs corrupt it, and scaling out is what ZEO and RelStorage are for. Serves on :8080.
server http
The Plone backend. Data.fs, the blob storage, the template cache and the per-client home live at /data on the volume. site creates a Plone site inside the empty Zope root on start-up — without one Zope answers and there is no CMS in it — and creation is skipped when a site of that id already exists; distribution picks volto (headless) or classic (server-rendered pages). adminSecretName names a Secret with an inituser key holding <user>:<password>, read only by the FIRST start of an empty database; kurly mints none. A first start compiles the translation catalogs and creates the site's default content before binding, so it is gated by a startup probe. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/plone/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "plone" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
site | string | — | "Plone" |
distribution | string | — | "volto" |
adminSecretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
plumio
A Plumio server (Markdown notes with a live preview, optional end-to-end encryption, and organisations several people share). A plain composable http workload keeping its SQLite database and the note files themselves on a PersistentVolume, so it needs no external database. The image runs two Node processes — the API on :3001 and the web front end on :3000, which proxies /api/* to the API internally — so only :3000 is published and a browser needs nothing else. secretName holds JWT_SECRET and ENCRYPTION_KEY; the image ships neither, and losing ENCRYPTION_KEY loses the notes it protected. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Plumio server. The SQLite database and the note files live under /data on the volume. secretName holds JWT_SECRET (signs the tokens users hold) and ENCRYPTION_KEY (makes an encrypted note readable again — there is no reset that recovers one without it). appUrl is the URL users reach this instance at, and reaches the app as both APP_URL and ALLOWED_ORIGINS; it has no default, because a default would be wrong everywhere it is really deployed. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/plumio/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "plumio" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "plumio" |
appUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pmm
A PMM server — the server half of Percona Monitoring and Management, collecting metrics and query analytics from agents beside MySQL, PostgreSQL and MongoDB instances and showing them in dashboards built for those databases. IT IS THE SERVER AND THE AGENTS ARE SOMEWHERE ELSE: PMM sees nothing until a pmm-agent is installed beside each database and registered against it, which happens from the database's side, so a PMM with no agents is a working set of empty dashboards. EVERYTHING LIVES IN ONE DIRECTORY, INCLUDING THE DATABASES IT RUNS — the image bundles VictoriaMetrics for metrics, PostgreSQL for its inventory and ClickHouse for query analytics and puts all of them under /srv on one volume, so the volume is the whole deployment and its size is a retention decision. The admin password is read once at first start and lives in PMM's own database from then on. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The PMM server. Metrics, query analytics and PMM's own inventory all live under /srv on the volume. secretName holds PMM_ADMIN_PASSWORD through envFrom, read once at first start; retention is how long metrics are kept. The startup budget is long because several bundled databases are initialised on the first start. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pmm/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pmm" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
secretName | string | — | "pmm" |
retention | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"8Gi"},"requests":{"cpu":"1","memory":"4Gi"}} |
labels | object | — | — |
annotations | object | — | — |
pocket-id
A Pocket ID server (a simple, self-hosted OIDC provider for passkey logins, no passwords) on the official image; with the default SQLite backend its database and keys live on a PersistentVolume. appUrl is the public HTTPS URL passkeys bind to. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :1411.
server http
The Pocket ID server. appUrl is the public HTTPS URL (issuer/callback origin). Data at /app/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pocket-id/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pocket-id" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pocketbase
A PocketBase server (a self-hosted, open-source backend in one file: an embedded SQLite database, auth, file storage and a REST/realtime API, with an admin dashboard) on the official image. A plain composable http workload that keeps its database, uploads and migrations on a PersistentVolume under /pb_data. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the API and admin UI on :8080.
server http
The PocketBase server. Data at /pb_data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pocketbase/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pocketbase" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
podfetch
A Podfetch server (a self-hosted podcast manager that subscribes to feeds, downloads the episodes, and plays them in the browser, with a GPodder endpoint so an existing podcast app keeps syncing against it) on the upstream image. A plain composable http workload backed by SQLite — no external database. It claims TWO volumes, because the two things it keeps grow at different rates: the database at /app/db and the downloaded audio at /app/podcasts, which is the one that fills a disk. Single writer over ReadWriteOnce volumes: one replica, recreated. Probed by connection, because the web app is a JavaScript bundle. Serves on :8000.
server http
The Podfetch server. SQLite database at /app/db, downloaded episodes at /app/podcasts, each on its own volume. pollingInterval is how often, in MINUTES, the subscribed feeds are checked for new episodes — the unit the server prints back on start, not the seconds the name suggests. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/podfetch/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "podfetch" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
databaseSize | quantity | — | "1Gi" |
storageClass | string | — | — |
pollingInterval | int | — | 60 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
polaris
A Polaris server (a music streaming server for a personal collection, with a web client and a documented HTTP API) on the community Alpine image. A plain composable http workload that keeps its database, its cache and the music library on one PersistentVolume — no external database. The entrypoint reads POLARIS_PORT as its listen port, which the Service would otherwise inject as a tcp:// URL, so service links are disabled and the port is set explicitly. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5050.
server http
The Polaris server. Database at /var/lib/polaris, cache at /var/cache/polaris, music library at /music, all on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/polaris/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "polaris" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
portabase
A Portabase server (scheduled backups and on-demand restores for the databases you already run, with a web interface to drive them). A composable http workload on the project's own image: the backup files land on a PersistentVolume and Portabase's own records in a PostgreSQL. GIVE IT A DATABASE OF ITS OWN, OR IT STARTS ONE INSIDE THE POD — without DATABASE_URL the entrypoint initialises and runs a PostgreSQL in the container's own filesystem, which a restart discards along with every schedule and every record of what was backed up. THE BACKUPS ARE ONLY AS SAFE AS THIS VOLUME: one PersistentVolume in the same cluster as the databases it protects is a backup against a dropped table, not against losing the cluster. It holds credentials for everything it backs up, so a NetworkPolicy limiting egress to exactly those is worth composing on. The entrypoint runs tusd, the application server and nginx as root, editing configuration in place first, so four defaults are relaxed deliberately. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Portabase server. Backups are written under /data on the volume. secretName holds DATABASE_URL through envFrom — without it Portabase runs a PostgreSQL inside the pod and loses it on restart. behindTlsProxy tells the resumable-upload endpoint that TLS is terminated in front. Compose an exposure onto the HTTP port, and compose a backup axis onto this workload if the copies have to survive the cluster.
Import: import 'github.com/metio/kurly/workloads/portabase/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "portabase" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
secretName | string | — | — |
behindTlsProxy | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
portainer
A Portainer CE server (a self-hosted management UI for Docker and Kubernetes) on the official image. A plain composable http workload that keeps its database and settings on a PersistentVolume. Single instance over a ReadWriteOnce volume: one replica, recreated. To administer the cluster it runs in, Portainer needs a ServiceAccount bound to a ClusterRole (cluster-admin for full control); kurly authors no RBAC — create the ServiceAccount and binding yourself and pass serviceAccountName. Serves the HTTP UI on :9000.
server http
The Portainer server. Database at /data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/portainer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "portainer" |
image | string | — | — |
serviceAccountName | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
postal
A Postal install (a full outgoing mail platform: applications hand it messages over SMTP or its HTTP API, it delivers them and records the fate of each one) as the three stages the application really is: server (the interface and API on :5000), worker (which actually delivers what is queued) and smtp (which accepts mail on :25). All three run the same official image and differ only in the subcommand. There is NO RabbitMQ — v3 removed it — and no volume: everything Postal keeps is in an external MySQL/MariaDB, and it uses TWO logins, because the message database holds one schema PER MAIL SERVER, created at runtime, so that account needs CREATE DATABASE on the prefix where the main one does not. Configuration is entirely environment: v3 reads every config key from a variable named after it (main_db.host is MAIN_DB_HOST) and the image's postal.yml simply stays absent. The schema is created by `postal initialize`, which the server stage runs as an idempotent initContainer rather than as a manual step. The smtp stage listens on :2525 and publishes :25 on its Service — the image gives ruby the cap_net_bind_service capability, which only a container that keeps capabilities could use. SIGNING_KEY is an RSA private key rather than a password, so it is read from the mounted Secret as a file; all three stages keep the hardened default posture. Deploy server first, then worker and smtp.
server http
The Postal web server: the management interface and the HTTP API applications submit through, on :5000. webHostname is the address this instance really answers on — it ends up in every link Postal mails out. dbHost/database/dbUser name the main MySQL/MariaDB; messageDbUser/messageDbPrefix the account that creates a schema per mail server. secretName holds MAIN_DB_PASSWORD, MESSAGE_DB_PASSWORD, RAILS_SECRET_KEY and SIGNING_KEY, and is read both as environment and as files under /secrets, which is where the signing key is read from. An initContainer runs `postal initialize` before every start, so a fresh database needs no manual step. Stateless — scale it with replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/postal/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postal" |
image | string | — | — |
webHostname | string | — | "postal.example.com" |
webProtocol | string | — | "https" |
smtpHostname | string | — | "postal.example.com" |
dbHost | string | — | "postal-db" |
dbPort | int | — | 3306 |
database | string | — | "postal" |
dbUser | string | — | "postal" |
messageDbUser | string | — | "postal" |
messageDbPrefix | string | — | "postal" |
secretName | string | — | "postal" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
smtp http
The Postal SMTP server: where applications hand their mail in and where bounces arrive. Same image, databases and Secret as the server; deploy it after that stage. The container listens on port (2525) and the Service publishes servicePort (25) — binding :25 in the container would need a capability the hardened default drops. SMTP is not HTTP, so an expose recipe does not apply: publish it with a LoadBalancer Service or a Gateway TCPRoute. Stateless — scale it with replicas.
Import: import 'github.com/metio/kurly/workloads/postal/smtp.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postal-smtp" |
image | string | — | — |
webHostname | string | — | "postal.example.com" |
webProtocol | string | — | "https" |
smtpHostname | string | — | "postal.example.com" |
port | int | — | 2525 |
servicePort | int | — | 25 |
dbHost | string | — | "postal-db" |
dbPort | int | — | 3306 |
database | string | — | "postal" |
dbUser | string | — | "postal" |
messageDbUser | string | — | "postal" |
messageDbPrefix | string | — | "postal" |
secretName | string | — | "postal" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The Postal worker: it delivers the queued mail, retries what bounced, expires held messages and fires the outgoing webhooks. Same image, same databases and same Secret as the server; no Service and no port. An install without one queues mail and sends none. Deploy it after the server, which creates the schema it reads. The workers claim messages through the shared database, so scale it with replicas.
Import: import 'github.com/metio/kurly/workloads/postal/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postal-worker" |
image | string | — | — |
webHostname | string | — | "postal.example.com" |
webProtocol | string | — | "https" |
smtpHostname | string | — | "postal.example.com" |
dbHost | string | — | "postal-db" |
dbPort | int | — | 3306 |
database | string | — | "postal" |
dbUser | string | — | "postal" |
messageDbUser | string | — | "postal" |
messageDbPrefix | string | — | "postal" |
secretName | string | — | "postal" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
postgres
A PostgreSQL server — a powerful, open-source object-relational database. A single-instance server on the official image (not a replicated cluster — use the operator-backed cluster workloads for HA where kurly ships one). Speaks its own protocol on :5432; data on a PersistentVolume. kurly authors no Secret; credentials come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Reached in-cluster on :5432.
server http
The A PostgreSQL server. secretName holds the credentials (envFrom). Data at /var/lib/postgresql/data. Usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/postgres/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "postgres" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "postgres" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
prestashop
A PrestaShop server — a self-hosted, open-source e-commerce platform for building online stores (backed by MySQL). On the official image, backed by an external database, with data on a PersistentVolume (the image populates it on first run). kurly authors no Secret; the DB credentials come from a provided Secret via envFrom. The Apache master runs as root then serves as www-data. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A PrestaShop server. secretName holds the DB credentials (envFrom). Data at /var/www/html. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/prestashop/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "prestashop" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "prestashop" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
privatebin
A PrivateBin server (a minimalist, zero-knowledge pastebin: the server stores only encrypted blobs, encrypted and decrypted in the browser) on the official nginx+php-fpm image; with the default filesystem backend its encrypted pastes live on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The PrivateBin server. Pastes at /srv/data. Point at an external database (conf.php) to scale out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/privatebin/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "privatebin" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
projectsend
A ProjectSend server — a self-hosted, private file-sharing app that assigns uploads to specific clients (backed by MySQL/MariaDB). On the LinuxServer.io image; its config lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A ProjectSend server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/projectsend/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "projectsend" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
prometheus
A Prometheus server as a prometheus-operator `Prometheus` custom resource, with the cluster-scoped RBAC it scrapes with. Authors the CR (like cnpg-cluster) for the operator to reconcile into a StatefulSet and the `prometheus-operated` Service. Requires the prometheus-operator installed. The default is central monitoring: it selects every ServiceMonitor/PodMonitor in every namespace.
server prometheus
The Prometheus server. namespace MUST match where you deploy — it names the ServiceAccount in the cluster RoleBinding, which a cluster-scoped object cannot inherit later. The selectors (verbatim operator schema) default to selecting everything; scope them to narrow what it scrapes. Query it at prometheus-operated.<namespace>.svc:9090.
Import: import 'github.com/metio/kurly/workloads/prometheus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "prometheus" |
namespace | string | — | "monitoring" |
image | string | — | — |
replicas | int | — | 1 |
retention | string | — | "15d" |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
scrapeInterval | string | — | "30s" |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
externalLabels | object | — | — |
serviceMonitorSelector | object | — | — |
podMonitorSelector | object | — | — |
ruleSelector | object | — | — |
probeSelector | object | — | — |
namespaceSelector | object | — | — |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
prowlarr
A Prowlarr server — an indexer manager and proxy that syncs indexers across the *arr suite. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9696.
server http
The A Prowlarr server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/prowlarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "prowlarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
pydio-cells
A Pydio Cells server (file sharing and collaboration: workspaces, share links, versioning and an activity feed). A plain composable http workload on the project's own image; the files and Cells' own configuration live under CELLS_WORKING_DIR on a PersistentVolume, with the metadata in an external MySQL/MariaDB. IT CONFIGURES ITSELF ON FIRST START, THROUGH A WIZARD — the entrypoint turns `cells start` into `cells configure` when it finds no installation, so the first request lands on a setup form and the probes are by connection. TLS terminates in front of it: Cells enables TLS on its own site by default and then wants a certificate before it answers, so CELLS_SITE_NO_TLS is set and the exposure holds the certificate. externalUrl is what Cells puts in share links and OAuth redirects; unset, the links it hands out resolve only from inside the cluster. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Pydio Cells server. The working directory (configuration, data, logs, certificates) is /var/cells on the volume; the database is configured through the first-boot wizard. externalUrl sets CELLS_SITE_EXTERNAL. Everything else Cells reads is CELLS_*, through env. Compose an exposure onto the HTTP port and terminate TLS there.
Import: import 'github.com/metio/kurly/workloads/pydio-cells/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pydio-cells" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
externalUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
pyload-ng
A pyLoad server — a free and open-source download manager for one-click hosters and more. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The A pyLoad server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/pyload-ng/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "pyload-ng" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
qbittorrent
A qBittorrent server — a feature-rich, self-hosted BitTorrent client with a web UI. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The A qBittorrent server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/qbittorrent/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "qbittorrent" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
questdb
A QuestDB server (a time-series database that ingests fast and answers SQL, with a web console, a PostgreSQL-wire endpoint and a line-protocol ingest port). A plain composable http workload on the project's own image; everything it stores is one directory on a PersistentVolume. THREE PORTS, THREE AUDIENCES — :9000 is the console and REST API, :8812 speaks the PostgreSQL wire protocol so existing clients and BI tools connect unchanged, and :9009 takes InfluxDB line protocol for ingest; the console has no authentication of its own in the open-source build, so an exposure without something in front of it publishes the database. The entrypoint chowns its data directory and re-execs through gosu only when it starts as root, so running as the image's own uid 10001 with fsGroup relaxes nothing. Single writer: one process owning one data directory, so one replica, recreated — two would corrupt the tables rather than share them. Probed by connection, because the endpoints that look like health checks are queries.
server http
The QuestDB server. The data directory is /var/lib/questdb on the volume. Settings come through QDB_*/QUESTDB_* in env. The memory limit is what QuestDB may map for its column files rather than a JVM heap cap. Compose an exposure onto whichever of the three ports should be reachable, and put authentication in front of the console.
Import: import 'github.com/metio/kurly/workloads/questdb/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "questdb" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"2Gi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
quickshare
A Quickshare server (simple file sharing between devices: upload, browse and hand out share links from a web interface). A plain composable http workload that keeps the shared files and the SQLite database indexing them together on a PersistentVolume — no external database. secretName holds DEFAULTADMIN and DEFAULTADMINPWD, the administrator created on the FIRST start; without them Quickshare invents a password and prints it to the log once. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :8686.
server http
The Quickshare server. Files and the SQLite index live at /quickshare/root on the volume, the layout its shipped /quickshare/docker.yml configures. Runs as uid/gid 8686, the account the image group-owns its tree by. secretName supplies the first administrator (DEFAULTADMIN, DEFAULTADMINPWD). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/quickshare/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "quickshare" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "quickshare" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
quickwit
A Quickwit server (a search engine for logs, traces and other append-only data, built to index onto object storage) running every Quickwit service in one process. A plain composable http workload on the project's own image. By default the whole index is on the PersistentVolume, which needs no other infrastructure and does not grow past one node; defaultIndexRootUri pointed at an S3 bucket is what Quickwit is built for, leaving the volume to the metastore and local caches, with the bucket credentials coming from secretName. Single writer over a ReadWriteOnce volume: one replica, recreated — a cluster wants a PostgreSQL metastore and separately scaled indexer and searcher roles, which is a different arrangement than this one stage. Serves the REST API, the UI and the Elasticsearch-compatible API on :7280.
server http
The Quickwit server, running all services (`quickwit run`). The data directory is /quickwit/qwdata on the volume. defaultIndexRootUri moves index splits to object storage (s3://bucket/indexes); secretName holds the credentials for it (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) through envFrom — kurly authors no Secret. The gossip and gRPC ports are published so another node could be pointed at this one. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/quickwit/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "quickwit" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
defaultIndexRootUri | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
rabbitmq
A RabbitMQ server (a widely-used, self-hosted message broker implementing AMQP) on the official management image; the broker speaks AMQP on :5672 with data on a PersistentVolume. Single node (not a cluster). The management UI (:15672) needs a separate Service. kurly authors no Secret; RABBITMQ_DEFAULT_USER/PASS come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves AMQP on :5672.
server http
The RabbitMQ server. secretName holds RABBITMQ_DEFAULT_USER/PASS (envFrom). Data at /var/lib/rabbitmq; the management UI (:15672) needs an extra Service. Usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/rabbitmq/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rabbitmq" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "rabbitmq" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
radarr
A Radarr server — a movie collection manager for Usenet and BitTorrent users. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :7878.
server http
The A Radarr server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/radarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "radarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
radicale
A Radicale server (a lightweight CalDAV and CardDAV server for calendars and contacts) on the tomsquest image. A plain composable http workload that keeps its collections on a PersistentVolume — no external database. The image runs as its designated uid 2999 with a writable root filesystem (s6 init). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5232; mount a config + htpasswd for real authentication.
server http
The Radicale server. Keeps collections at /data on the volume. Default config allows anonymous access — mount a Radicale config and htpasswd (kurly.config / kurly.secretMount) for htpasswd auth. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/radicale/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "radicale" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
rallly
A Rallly server (a self-hosted scheduling and group-poll tool for finding the best date to meet) on the official image, backed by an external PostgreSQL. Stateless — its state lives in the database, so it can run several replicas. kurly authors no Secret; DATABASE_URL, SECRET_PASSWORD, and SMTP creds come from a provided Secret via envFrom. Pairs with a cnpg-cluster named rallly-db. Serves on :3000.
server http
The Rallly server. baseUrl is the public URL. secretName holds DATABASE_URL (with the DB password), SECRET_PASSWORD, and SMTP_* (envFrom). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rallly/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rallly" |
image | string | — | — |
baseUrl | string | — | — |
secretName | string | — | "rallly" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
readeck
A Readeck server (a self-hosted read-it-later and web-bookmarking tool that saves clean, readable copies of pages). A plain composable http workload that keeps its bookmarks and saved articles in SQLite on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :8000.
server http
The Readeck server. Keeps its SQLite database and saved pages at /readeck on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/readeck/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "readeck" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
reaparr
A Reaparr server (it connects to the Plex servers an account has access to, browses their libraries and downloads what you pick into your own). A plain composable http workload on the official image; the SQLite database, settings and logs live on a PersistentVolume mounted at /Config. Downloads land in /Downloads and finished media is moved into /Movies and /TvShows — ordinary container directories unless the real library volumes are mounted over them, so anything outside /Config goes with the pod. Plex credentials are entered in the web UI and stored in that database, so kurly authors no Secret. The image ships USER abc and its s6 service execs the application as that account, so it keeps the hardened posture: uid 911 is pinned so the volume is writable, with scratch space for the s6 service tree and the .NET temporary files. Probed by connection, and given a startup budget for the first-run database migration. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :7000.
server http
The Reaparr server. timezone sets TZ and logLevel sets LOG_LEVEL. Config at /Config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/reaparr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "reaparr" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
timezone | string | — | "UTC" |
logLevel | string | — | "INFORMATION" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
receipt-wrangler
A Receipt Wrangler server (uploaded or mail-polled receipts, categorised, with the amounts split between people and reported on) backed by an external PostgreSQL and an external Redis. REDIS IS NOT OPTIONAL: the API runs its asynq worker and scheduler in-process and exits fatally when it cannot reach one, so a deployment with only a database never starts. Pairs with a cnpg-cluster named receipt-wrangler-db and a valkey named receipt-wrangler-cache; secretName holds DB_PASSWORD plus the SECRET_KEY signing users in and the ENCRYPTION_KEY protecting the stored mail credentials, the last two of which are refused as empty at startup. The entrypoint runs the Go API and nginx side by side as root, so root, privilege escalation, capabilities and a writable root filesystem are relaxed, and service links are off because a cache Service called redis injects REDIS_PORT as a tcp:// URL this API parses as an integer. Uploaded images sit on one ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Receipt Wrangler server, serving the web app and its API on :80. dbHost/dbPort/database/dbUser default to a cnpg-cluster named receipt-wrangler-db and redisHost/redisPort to a valkey named receipt-wrangler-cache; secretName holds DB_PASSWORD, SECRET_KEY and ENCRYPTION_KEY (envFrom, kurly authors none). Uploaded receipt images live at /app/receipt-wrangler-api/data on a ReadWriteOnce volume. The first boot migrates the schema and is guarded by a startup probe. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/receipt-wrangler/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "receipt-wrangler" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "receipt-wrangler-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "receipt-wrangler" |
dbUser | string | — | "receipt-wrangler" |
redisHost | string | — | "receipt-wrangler-cache-headless" |
redisPort | int | — | 6379 |
secretName | string | — | "receipt-wrangler" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
recipya
A Recipya server (a recipe manager that imports recipes from a website or a photo, plans meals for a week and turns the plan into a shopping list). A plain composable http workload keeping its SQLite databases, images and videos on a PersistentVolume. The port is not the image EXPOSE, which still says 8080: Recipya listens on whatever RECIPYA_SERVER_PORT names and has no default of its own, so one parameter sets the env, the container port and the Service port together. First boot downloads a 62 MB nutrition database from githubusercontent into the data directory and exits when it cannot, so the pod needs internet egress and a generous startup budget. An empty directory is mounted at /.dockerenv because the app reads its configuration from the environment only when that path exists — without it, it takes the interactive path, accepts the prompt defaults on EOF and binds a random ephemeral port. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8078.
server http
The Recipya server. SQLite, images and videos under /data on the volume. port sets RECIPYA_SERVER_PORT, the container port and the Service port together; baseUrl is the URL browsers reach this instance at, which Recipya builds its shared links and mails from, and is left unset rather than defaulted to something wrong everywhere; noSignups refuses new registrations once the accounts that should exist do. Needs egress on first boot for the nutrition database. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/recipya/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "recipya" |
image | string | — | — |
port | int | — | 8078 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
noSignups | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
redash
A Redash deployment (connect a data source, write a query, save it, and put the result on a dashboard) on the official image, as three composable stages — server (the web UI and API on :5000), worker (the RQ worker that actually runs the queries) and scheduler (the RQ scheduler that enqueues the periodic ones). State lives in an external PostgreSQL and an external Redis, so no stage claims a volume; pairs with the cnpg-cluster and valkey workloads. The server only enqueues, so a deployment without a worker accepts queries and never answers them, and without the scheduler nothing refreshes on its own. The schema is NOT migrated on start: the image carries the migration as its own entrypoint command (create_db on an empty database, manage db upgrade after an upgrade), run once before these stages are useful. secretName holds REDASH_DATABASE_URL, REDASH_REDIS_URL, REDASH_COOKIE_SECRET (it signs the session cookie) and REDASH_SECRET_KEY (it encrypts the data source credentials in the database, so changing it leaves every configured source unreadable); kurly authors no Secret. Service links are off, because every Redash setting is REDASH_-prefixed and a Service named after the workload injects REDASH_PORT. Runs as an unprivileged uid on a read-only root filesystem, probed by connection since the root path redirects to the login form. The scheduler is one replica by construction; workers scale horizontally.
scheduler worker
The Redash RQ scheduler: it enqueues the periodic work, so it is what makes a query with a refresh interval refresh and a schema get re-read. Same image, database, Redis and Secret as the server, with no Service and no port. Exactly one replica by construction and no replicas parameter — every instance would enqueue the same periodic jobs, doubling the load on the data sources while changing nothing a user sees. The schedule lives in Redis, so it claims no volume.
Import: import 'github.com/metio/kurly/workloads/redash/scheduler.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redash-scheduler" |
image | string | — | — |
secretName | string | — | "redash" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
server http
The Redash web server, serving the UI and API on :5000 (gunicorn). It enqueues every query rather than running it, so run the worker and scheduler stages alongside it against the same database, Redis and Secret. host is the public URL links and mails are built from and defaults to a value that boots rather than one that is right. secretName holds REDASH_DATABASE_URL, REDASH_REDIS_URL, REDASH_SECRET_KEY and REDASH_COOKIE_SECRET, read via envFrom; kurly mints none. Claims no volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/redash/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redash" |
image | string | — | — |
host | string | — | "http://localhost:5000" |
secretName | string | — | "redash" |
webWorkers | string | — | "4" |
passwordLoginEnabled | bool | — | true |
inviteOnly | bool | — | true |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The Redash RQ worker: what actually runs a query against a data source, refreshes a schema and sends a mail. Same image, database, Redis and Secret as the server, with no Service and no port. queues picks which RQ queues this deployment drains, so a second worker stage with queues=queries and a bigger resource tier keeps ad hoc queries off the pods running the scheduled ones. workersCount is the processes inside one pod; replicas scales horizontally, the workers coordinating through the shared Redis queues. A Redash deployment needs at least one.
Import: import 'github.com/metio/kurly/workloads/redash/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redash-worker" |
image | string | — | — |
secretName | string | — | "redash" |
queues | string | — | "queries,scheduled_queries,schemas,default,periodic" |
workersCount | string | — | "2" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
redaxo
A REDAXO server (a content management system built around a structured editor, where an article is composed from typed modules rather than typed into a single rich-text field) on the project's own Apache/PHP image, backed by an external MySQL/MariaDB. The entrypoint installs only into an EMPTY document root: it copies the application tree in, writes the database connection and runs the installer, so a reachable server holding an already-created, empty database is a prerequisite of the FIRST boot and not only of serving, and the volume is the installation rather than a cache. Every optional REDAXO_* variable is rendered even when empty, because the entrypoint runs under set -u and reads them all unconditionally — an unset one aborts setup before Apache starts. Setup copies the tree, installs and chowns the writable directories to www-data before Apache drops privileges, so root, capabilities and a writable image tree are all relaxed deliberately. Every variable it reads is REDAXO_*, so service links are disabled. kurly authors no Secret; REDAXO_DB_PASSWORD and REDAXO_ADMIN_PASSWORD come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume holding the tree, the addons and the uploaded media: one replica, recreated. Serves on :80.
server http
The REDAXO server. dbHost/database/dbUser point at a MySQL/MariaDB that must already exist — setup runs with --db-createdb=no — and secretName holds REDAXO_DB_PASSWORD and REDAXO_ADMIN_PASSWORD (envFrom). server is the public URL REDAXO builds its links against and serverName the site name beside it; left unset the installer keeps its own default and those links point elsewhere. lang, timezone and dbCharset are recorded at install time, adminUser is the account setup creates. The application tree, the installed addons and the uploaded media all live at /var/www/html, and the entrypoint installs only into an empty one. Probes are by connection, because the document root redirects into the backend login. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/redaxo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redaxo" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "redaxo-db" |
database | string | — | "redaxo" |
dbUser | string | — | "redaxo" |
dbCharset | string | — | "utf8mb4" |
server | string | — | — |
serverName | string | — | — |
errorEmail | string | — | — |
lang | string | — | "en_gb" |
timezone | string | — | "UTC" |
adminUser | string | — | "admin" |
secretName | string | — | "redaxo" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
redis-commander
A Redis Commander server (a self-hosted web UI for managing Redis) on the official image (pinned by digest; Renovate maintains it). Stateless: a plain rolling Deployment that connects to the Redis you point it at. Serves on :8081.
server http
The Redis Commander server. redisHosts sets REDIS_HOSTS (e.g. local:redis:6379). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/redis-commander/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redis-commander" |
image | string | — | — |
replicas | int | — | 2 |
redisHosts | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
redmine
A Redmine server (a mature, self-hosted project-management web app: issue tracking, wikis, forums, Gantt charts and time tracking) on the official image, backed by an external MySQL/MariaDB or PostgreSQL, with uploaded files on a PersistentVolume. kurly authors no Secret; the database connection and REDMINE_SECRET_KEY_BASE come from a provided Secret via envFrom. Pairs with a mysql-cluster named redmine-db. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The Redmine server. secretName holds the database connection (REDMINE_DB_* / DATABASE_URL) and REDMINE_SECRET_KEY_BASE (envFrom). Files at /usr/src/redmine/files. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/redmine/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "redmine" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "redmine" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
registry
A Docker Registry server (the reference OCI registry: a self-hosted store and distribution point for container images) on the official image; its stored images live on a PersistentVolume. Usually reached in-cluster; the docker-registry-ui workload gives it a web interface. The bare registry is unauthenticated and plaintext — front it with TLS and auth or keep it in-cluster. Single writer over a ReadWriteOnce volume: one replica, recreated (back it with S3 for a scaled registry). Serves on :5000.
server http
The Docker Registry server. Images at /var/lib/registry. Usually reached in-cluster. Compose an exposure onto the HTTP port only if reached from outside (with TLS/auth in front).
Import: import 'github.com/metio/kurly/workloads/registry/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "registry" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
remark42
A Remark42 server (a lightweight comment engine for static sites and blogs: a script tag on the page, and readers comment with a social login or anonymously). A plain composable http workload keeping comments in an embedded BoltDB on a PersistentVolume — no external database. Runs unprivileged: its entrypoint drops privileges only when started as root, so naming the image's own uid takes the other path. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the API and widget on :8080.
server http
The Remark42 server. Comments live in BoltDB at /srv/var on the volume. siteUrl is required — it is baked into the widget script and every OAuth callback, so a wrong value loads comments nowhere and returns logins to the wrong host. secretName holds SECRET, which signs reader JWTs; remark42 refuses to start without it. Auth providers are configured through env (REMARK_AUTH_*) and none is set by default, which the log reports as "no auth providers defined". Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/remark42/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "remark42" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
siteUrl | string | yes | — |
site | string | — | "remark" |
secretName | string | — | "remark42" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
restreamer
A datarhei Restreamer server (a complete streaming server with a web UI: ingest a camera or RTMP feed, transcode it with FFmpeg, publish it as HLS or push it on to YouTube and Twitch). A plain composable http workload with two PersistentVolumes, one for configuration and one for recordings. Transcoding is CPU work and the resource limit is what stops one busy stream starving its neighbours. Serves the UI and HLS on :8080, with RTMP on 1935, RTMPS on 1936 and SRT on 6000/UDP.
server http
The Restreamer server. Configuration at /core/config and recordings at /core/data, on separate volumes. secretName holds CORE_API_AUTH_USERNAME and CORE_API_AUTH_PASSWORD — without them the first visitor to the web UI is invited to create the administrator, which on an exposed instance is whoever finds it first. Compose an exposure onto the HTTP port; the ingest ports are not HTTP and need their own routes.
Import: import 'github.com/metio/kurly/workloads/restreamer/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "restreamer" |
image | string | — | — |
configSize | quantity | — | "1Gi" |
dataSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "restreamer" |
env | object | — | — |
resources | object | — | {"limits":{"cpu":"2","memory":"2Gi"},"requests":{"cpu":"500m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
rocketchat
A Rocket.Chat server (a self-hosted, open-source team chat platform) on the official image, backed by an external MongoDB replica set (Rocket.Chat requires the oplog). Pairs with a mongodb-cluster named rocketchat-db. kurly authors no Secret; MONGO_URL and MONGO_OPLOG_URL come from a provided Secret via envFrom. Stateless (uploads live in MongoDB GridFS): a plain rolling Deployment. Serves on :3000.
server http
The Rocket.Chat server. rootUrl is the public URL. secretName holds MONGO_URL and MONGO_OPLOG_URL (envFrom); MongoDB MUST be a replica set. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rocketchat/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rocketchat" |
image | string | — | — |
replicas | int | — | 2 |
rootUrl | string | — | — |
secretName | string | — | "rocketchat" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
romm
A RomM server (scans a ROM library, enriches it with metadata from the games databases, and browses or plays it in the browser). A composable http workload backed by an external MariaDB/MySQL, with the library, scraped resources and uploaded assets on a PersistentVolume. The image bundles its own Valkey and runs nginx plus the Python backend under s6, which starts as root and drops privileges, so the hardened defaults are relaxed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The RomM server. The ROM library, the scraped resources, the uploaded assets and the config file all live under /romm on the volume; everything indexed is in MariaDB/MySQL. secretName holds DB_PASSWD and ROMM_AUTH_SECRET_KEY, which signs sessions, alongside whichever metadata provider credentials are used (IGDB, MobyGames, SteamGridDB) — without those the library still imports, it simply arrives without cover art. s6 starts as root and drops privileges and the entrypoint chowns the volume, so this workload is deliberately less hardened. Probed by connection, because the application redirects to a login page. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/romm/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "romm" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
dbHost | string | — | "romm-db" |
dbPort | int | — | 3306 |
database | string | — | "romm" |
dbUser | string | — | "romm" |
secretName | string | — | "romm" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
roundcube
A Roundcube server (a browser-based IMAP webmail client) on the official image. A plain composable http workload that connects to an external IMAP/SMTP mail server (e.g. the mailu workload) and keeps its own state in SQLite on a PersistentVolume — no external database. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Roundcube server. imapHost/smtpHost point at the mail server (e.g. ssl://mail.example.com:993). Keeps contacts/preferences in SQLite at /var/roundcube/db. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/roundcube/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "roundcube" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
imapHost | string | — | — |
smtpHost | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
roundup-issue-tracker
A Roundup issue tracker (issues over the web, by email and from a command line, on a schema the tracker owner edits). A plain composable http workload with the tracker home — configuration, page templates and the SQLite database — on a PersistentVolume. The image's own start script installs a tracker by ASKING which template and which backend on a terminal, so the init container does both steps non-interactively instead and the server starts unattended. secretName holds ADMIN_PASSWORD, read once when the database is initialised. webUrl is written into every link and every mail the tracker sends and is read on the FIRST RUN ONLY. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080, with the tracker under /<tracker>/.
server http
The Roundup server. The tracker home lives at /usr/src/app/tracker on the volume: config.ini, the page templates and the hyperdb. The init container runs the two steps the image would otherwise prompt for — `install` with the template, the backend and the web URL, then `initialise` with the administrator's password — and only ever acts on what is missing, so a tracker already holding issues survives every restart. backend selects the hyperdb: only the file-backed anydbm and sqlite are self-contained, since postgres and mysql want a server and a database URL this workload does not supply. secretName holds ADMIN_PASSWORD for the tracker's `admin` account. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/roundup-issue-tracker/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "roundup-issue-tracker" |
image | string | — | — |
tracker | string | — | "issues" |
template | string | — | "classic" |
backend | string | — | "sqlite" |
webUrl | string | — | "http://localhost:8080/issues/" |
secretName | string | — | "roundup-issue-tracker" |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
routr
A Routr server on the all-in-one image: a SIP proxy, registrar and location server in one container, listening on :5060 (TCP and UDP), :5061 (TLS), :5062 (WS) and :5063 (WSS), with the gRPC management API on :51908. SIP ports want a LoadBalancer or a Gateway TCPRoute/UDPRoute, never an HTTP ingress, and SIP is address-sensitive: behind NAT the proxy needs EXTERNAL_ADDR or it advertises the pod address and media goes nowhere. NO VOLUME, deliberately — the image carries an already initialized PostgreSQL and has had the tooling that created it removed, so a volume mounted over the data directory hides a cluster nothing can rebuild; agents, domains and trunks therefore live as long as the pod, unless databaseUrl points at a PostgreSQL you keep. Registrations are held in memory, so one replica, recreated. The entrypoint starts PostgreSQL and su-execs down, so this workload runs deliberately less hardened.
server http
The Routr server. databaseUrl defaults to the PostgreSQL inside the image, which dies with the pod — point it at an external one to keep agents, domains, trunks and numbers across restarts. externalAddr is the address other SIP endpoints reach this proxy at; without it Routr advertises the pod address. The gRPC API on :51908 has no authentication of its own here, so keep it inside the cluster. Route the SIP ports as TCP/UDP rather than composing an HTTP exposure.
Import: import 'github.com/metio/kurly/workloads/routr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "routr" |
image | string | — | — |
databaseUrl | string | — | "postgres://postgres:postgres@localhost:5432/routr" |
externalAddr | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
rss-bridge
An RSS-Bridge server (generates RSS/Atom feeds for sites that do not publish their own, from a large library of community bridges) on the official image. It holds no persistent state — feeds are produced on request — so it is a plain stateless Deployment. The Apache master runs as root then serves as www-data. Serves on :80.
server http
The RSS-Bridge server. Stateless; mount a whitelist.txt over /app/whitelist.txt to restrict enabled bridges. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rss-bridge/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rss-bridge" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
rssbox
An RSS Box server (it turns sites that stopped publishing feeds back into RSS: YouTube channels, Twitch streams, SoundCloud, Vimeo, Instagram and a dozen more) on the project's own image. MOST SERVICES NEED AN API KEY REGISTERED IN YOUR NAME — YouTube, Vimeo, SoundCloud, Twitch and Imgur — supplied through a Secret, while Instagram, Mixcloud, Speedrun and Dailymotion need none; a missing key fails that service's feeds rather than the server, so it can be deployed with no Secret and grown as credentials arrive. Redis is optional and caches URL resolution only. It fetches the public internet on every request, which is the function: a cluster with default-deny egress has nothing to convert, and every feed polled spends somebody else's quota in your name. Bundler insists on a writable cache inside the application tree and the image's user has /nonexistent as its home, so /app/tmp is an emptyDir and HOME points at the scratch. Stateless: a plain rolling Deployment. Serves on :3000.
server http
The RSS Box server. secretName carries the per-service API keys (envFrom) and none of them are required to start. redisUrl wires in a cache for URL resolution, which is the only feature that uses one. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rssbox/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rssbox" |
image | string | — | — |
replicas | int | — | 1 |
redisUrl | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
rundeck
A Rundeck server (a self-hosted runbook-automation and operations platform: jobs, workflows, access control and scheduling) on the official image; with the default embedded storage its data lives on a PersistentVolume. kurly authors no Secret; the admin credentials come from a provided Secret via envFrom. Point it at an external MySQL/PostgreSQL to scale past the embedded database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4440.
server http
The Rundeck server. grailsUrl is the public URL (RUNDECK_GRAILS_URL); secretName holds the admin credentials (envFrom). Data at /home/rundeck/server/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rundeck/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rundeck" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
grailsUrl | string | — | — |
secretName | string | — | "rundeck" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
rustfs
A RustFS server (an S3-compatible object store written in Rust: buckets and objects on a PersistentVolume, spoken to by anything that already speaks S3) on the project's own image. THE DEFAULT CREDENTIALS ARE PUBLISHED IN UPSTREAM'S OWN DOCUMENTATION — without a Secret it starts as rustfsadmin/rustfsadmin, the same pair on every deployment anyone has run — so secretName carries RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY and must be set before the instance is reachable. IT IS A RELEASE CANDIDATE and upstream marks distributed mode, lifecycle rules and KMS as under test; what this renders is the single-node shape they call ready. The image writes logs to a directory rather than stdout, so that directory is pointed at the scratch volume. :9000 is the S3 API and :9001 the console. Single writer over one ReadWriteOnce volume holding every bucket: one replica, recreated. Serves on :9000.
server http
The RustFS server. secretName carries RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY (envFrom) and replaces the published defaults. storageSize sizes the volume every bucket lives on. logLevel sets the observability logger. The console is published as the extra port console. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/rustfs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "rustfs" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
secretName | string | — | — |
logLevel | string | — | "warn" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sablier
A Sablier server (it scales workloads to zero and starts them again on the first request, showing a waiting page while they come up). A plain composable http workload on the project's own image: it watches and scales other workloads through the Kubernetes API and keeps nothing of its own. It does NOT proxy traffic — a middleware in Traefik, Caddy, Nginx, Envoy, Istio or APISIX asks it whether the workload is up, holds the request while it starts, and only then forwards. The grant is the interesting part: reading and changing the replica count is namespace-wide on deployments and statefulsets, since RBAC resourceNames cannot express 'whichever carry the sablier label'; apiServerClient declares that Role and the egress to the apiserver together, so a consumer's own rbac() or networkPolicy() composes with it rather than firewalling the scaler off from what it scales. Sessions are in-process, so a restart forgets which workloads are awake — a slow first request rather than an error, and the reason for one replica. Serves its API on :10000.
server http
The Sablier server. sessionDuration is how long a workload stays awake after the last request reaches it. kurly renders the ServiceAccount, Role and RoleBinding the Kubernetes provider needs. The reverse proxy in front of the scaled workloads calls this Service; expose it only if that proxy is outside the cluster.
Import: import 'github.com/metio/kurly/workloads/sablier/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sablier" |
image | string | — | — |
sessionDuration | string | — | "5m" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sabnzbd
A SABnzbd server — a self-hosted, web-based binary newsreader (Usenet downloader). On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The A SABnzbd server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sabnzbd/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sabnzbd" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
safebucket
A Safebucket server (file sharing where uploads and downloads are presigned and executed by the browser straight against S3-compatible storage, leaving the API with metadata and access control only). A composable http workload, stateless: metadata in an external PostgreSQL, objects in an external bucket. The two S3 endpoints are deliberately separate — the pod reaches the store at a cluster address while the presigned URLs must be signed for the address a browser reaches — and the bucket must exist and allow CORS from the web URL, since the server verifies it on start. The cache and event bus default to their in-process implementations, which is what makes a single replica self-sufficient; more replicas want a Redis and a NATS. Serves on :8080.
server http
The Safebucket API and bundled web app on :8080. apiUrl, webUrl and allowedOrigins are the URLs a BROWSER reaches; s3Endpoint is host:port as the server reaches the bucket and s3ExternalEndpoint the absolute URL the browser reaches the same bucket at. The filesystem notifier and activity log write to /app/data, an emptyDir, so both go with the pod — point ACTIVITY__TYPE at a Loki and NOTIFIER__TYPE at an SMTP server to keep either. There is no health endpoint, so the probes are by connection. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/safebucket/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "safebucket" |
image | string | — | — |
replicas | int | — | 1 |
apiUrl | string | — | "https://safebucket.example.com" |
webUrl | string | — | "https://safebucket.example.com" |
allowedOrigins | string | — | "https://safebucket.example.com" |
adminEmail | string | — | "admin@example.com" |
dbHost | string | — | "safebucket-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "safebucket" |
dbUser | string | — | "safebucket" |
dbSslMode | string | — | "require" |
s3Endpoint | string | — | "seaweedfs-s3:8333" |
s3ExternalEndpoint | string | — | "https://s3.example.com" |
s3Bucket | string | — | "safebucket" |
s3Region | string | — | "us-east-1" |
s3UseTls | bool | — | false |
secretName | string | — | "safebucket" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sama
A SAMA server (the backend of an end-to-end encrypted chat: WebSocket and HTTP APIs for conversations, messages, devices and attachments, with the SAMA client applications talking to it) on the official image, backed by an external MongoDB and an external Redis. Pairs with a mongodb-cluster named sama-db. kurly authors no Secret; MONGODB_URL, REDIS_URL, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, COOKIE_SECRET and HTTP_ADMIN_API_KEY come from a provided Secret via envFrom, and rotating either JWT secret signs everyone out. Every replica opens a second socket on :9002 and registers its pod address in Redis, so replicas forward messages to each other DIRECTLY, pod to pod, on a port no Service carries — a NetworkPolicy allowing only the API port delivers messages within a replica and drops them between replicas. Attachments go to S3-compatible object storage against a presigned URL the client uploads to, so the bucket must be reachable from the browser and not only from the cluster; unconfigured, everything but file transfer works. The built-in REPL evaluates JavaScript inside the server process and is left off. Schema migrations are the image's own migrate-mongo command run as a job, not something startup does. Stateless: a plain rolling Deployment. Serves WebSocket and HTTP on :9001, so the exposure has to allow upgrades.
server http
The SAMA server. port carries both the WebSocket and the HTTP API; clusterPort is the pod-to-pod socket replicas find each other on through Redis and stays off the Service. clusterSyncInterval and socketPingInterval are milliseconds and must be set — unset they become NaN and the timers they drive fire continuously. corsOrigin is the origin a browser client is served from; left unset the server echoes back whatever origin asks. s3Endpoint/s3Bucket/s3Region point attachments at object storage, with the keys in the same Secret; the S3 client addresses buckets virtual-host style and that is not configurable. secretName holds MONGODB_URL, REDIS_URL, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, COOKIE_SECRET and HTTP_ADMIN_API_KEY (envFrom). Compose an exposure that allows WebSocket upgrades onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sama/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sama" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 9001 |
clusterPort | int | — | 9002 |
clusterSyncInterval | int | — | 60000 |
socketPingInterval | int | — | 60000 |
corsOrigin | string | — | — |
s3Endpoint | string | — | — |
s3Bucket | string | — | "sama" |
s3Region | string | — | "us-east-1" |
secretName | string | — | "sama" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
scm-manager
An SCM-Manager server (shares and manages Git, Mercurial and Subversion repositories over HTTP, with users, groups and permissions in one place). A plain composable http workload with every repository, plugin and setting in SCM_HOME on a PersistentVolume. The web UI, the REST API and all three repository protocols share :8080, so one exposure carries everything. The FIRST START leaves a setup screen open until somebody creates the administrator, so finish it before the instance is reachable. A first boot links the bundled plugins for minutes, hence a long startup probe and probes by connection. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The SCM-Manager server. Repositories, plugins and configuration live at /var/lib/scm on the volume; the plugin cache and JVM temporary files sit on scratch volumes. Runs as uid 1000 in the root group, as the image ships it. Probes check the connection, because every HTTP path redirects into the setup screen or answers 401 until an administrator exists. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/scm-manager/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "scm-manager" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"768Mi"}} |
labels | object | — | — |
annotations | object | — | — |
scraparr
A Scraparr exporter (Prometheus metrics for the *arr media stack: Sonarr, Radarr, Prowlarr, Bazarr, Jellyseerr, Jellyfin, Komga and the rest). A plain composable http workload on the project's own image: it polls each service's API and serves the result as metrics, keeping nothing, so it claims no volume. API keys do NOT belong in the ConfigMap — Scraparr substitutes ${VAR} in its config.yaml from the environment, so a service's api_key is written as a placeholder and the value comes from the Secret secretName names; inline it would be readable by anything that can read ConfigMaps in the namespace. One replica: more than one is legal and each polls every configured service on its own, multiplying the load on them for no gain. Serves /metrics on :7100.
server http
The Scraparr exporter. services is one entry per scraped service, keyed by connector name, merged into the rendered config.yaml; interval is the poll period in seconds. secretName holds the API keys the config refers to as ${VAR}, read through envFrom. Compose a kurly.serviceMonitor onto it, or an exposure if something outside the cluster scrapes it.
Import: import 'github.com/metio/kurly/workloads/scraparr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "scraparr" |
image | string | — | — |
services | object | — | — |
interval | int | — | 30 |
secretName | string | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
searxng
A SearXNG server (a privacy-respecting, self-hosted metasearch engine that aggregates results from many search services without tracking you) on the official image. Its behaviour is its settings.yml, mounted as a ConfigMap and passed verbatim; it keeps no persistent state of its own. kurly authors no Secret; SEARXNG_SECRET (overriding server.secret_key) comes from a provided Secret via envFrom. A busy instance also wants a Valkey/Redis for the limiter. Stateless: scale freely. Serves on :8080.
server http
The SearXNG server. baseUrl is the public URL; settings is SearXNG's own settings.yml, mounted verbatim; secretName holds SEARXNG_SECRET (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/searxng/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "searxng" |
image | string | — | — |
baseUrl | string | — | — |
settings | object | — | — |
secretName | string | — | "searxng" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
seatsurfing
A Seatsurfing server (desk and meeting-room booking / hot-desking) on the official image, backed by an external PostgreSQL. Stateless — its state lives in the database, so it can run several replicas. kurly authors no Secret; POSTGRES_URL and JWT_SIGNING_KEY come from a provided Secret via envFrom. Pairs with a cnpg-cluster named seatsurfing-db. Serves on :8080.
server http
The Seatsurfing server. secretName is the Secret holding POSTGRES_URL (with the embedded DB password) and JWT_SIGNING_KEY, pulled in via envFrom. env carries non-sensitive settings (PUBLIC_URL, FRONTEND_URL). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/seatsurfing/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "seatsurfing" |
image | string | — | — |
secretName | string | — | "seatsurfing" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
seaweedfs
SeaweedFS as an all-in-one object store: a StatefulSet with a per-pod PVC and a headless Service running `weed server -s3`, so one process is master, volume, filer, and an S3 gateway. It gives a cluster an S3 API on 8333 backed by a PersistentVolume — an in-cluster target for anything that speaks S3, such as a cnpg-cluster's backups.
filer stateful
The access tier of a SPLIT SeaweedFS: `weed filer` puts a filesystem and (s3=true) an S3 gateway on 8333 over the volume servers, keeping its own metadata. Point it at the master with masterEndpoint.
Import: import 'github.com/metio/kurly/workloads/seaweedfs/filer.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "seaweedfs-filer" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
masterEndpoint | string | — | "seaweedfs-master-0.seaweedfs-master-headless:9333" |
s3 | bool | — | true |
master stateful
The coordinator of a SPLIT SeaweedFS: `weed master` holds the topology, assigns file IDs, and directs clients to volume servers. defaultReplication is the cluster-wide policy it owns ('000' keeps one copy). Deploy it, then point the volume and filer stages at it.
Import: import 'github.com/metio/kurly/workloads/seaweedfs/master.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "seaweedfs-master" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
defaultReplication | string | — | "000" |
server stateful
The all-in-one server. Serves S3 on 8333 over the data volume at /data; the master/volume/filer ports serve the cluster itself. The default allows anonymous access, fine inside a trusted namespace. Splitting the roles into dedicated tiers is a different topology, not more replicas, so it would be its own stage.
Import: import 'github.com/metio/kurly/workloads/seaweedfs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "seaweedfs" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
volume stateful
The data tier of a SPLIT SeaweedFS: `weed volume` stores file content and registers with the master, advertising its pod IP so reads reach it. Scale by replicas for capacity, each a pod with its own PVC. Point it at the master with masterEndpoint.
Import: import 'github.com/metio/kurly/workloads/seaweedfs/volume.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "seaweedfs-volume" |
image | string | — | — |
replicas | int | — | 2 |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
masterEndpoint | string | — | "seaweedfs-master-0.seaweedfs-master-headless:9333" |
maxVolumes | int | — | 100 |
semaphore-ui
A Semaphore UI server (a web interface for running Ansible playbooks, Terraform plans and shell scripts, with projects, schedules and an audit trail). A plain composable http workload on the project's own image; the default database is BoltDB in a file on a PersistentVolume, so a single instance needs no external database. secretName carries SEMAPHORE_ACCESS_KEY_ENCRYPTION, the key every stored SSH key and cloud credential is encrypted with — changing it makes them unreadable, so it belongs in a Secret from the first boot. Playbooks execute as child processes IN THIS CONTAINER: the image ships Ansible, and anything else a playbook calls has to be there too. Single writer over a ReadWriteOnce volume: one replica, recreated — dbDialect pointed at PostgreSQL or MySQL is what more than one needs. Serves on :3000.
server http
The Semaphore UI server. dbDialect is postgres, mysql, or bolt; dbHost/dbName/dbUser point at the server, defaulting to a cnpg-cluster named semaphore-ui-db. NOT bolt by default, though the file database on the volume would be the simpler shape: this image rejects it with "Unknown database dialect: bolt", so a bolt default would be a workload that cannot start. secretName holds SEMAPHORE_ACCESS_KEY_ENCRYPTION, SEMAPHORE_ADMIN_PASSWORD and, for an external database, SEMAPHORE_DB_PASS, through envFrom. adminName/adminEmail seed the first user. publicUrl is the URL Semaphore builds its links from. The generated configuration and the repository checkouts go to scratch volumes, so the root filesystem stays read-only. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/semaphore-ui/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "semaphore-ui" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbDialect | string | — | "postgres" |
dbHost | string | — | "semaphore-ui-db-rw" |
dbName | string | — | "semaphore" |
dbUser | string | — | "semaphore" |
secretName | string | — | "semaphore-ui" |
adminName | string | — | "admin" |
adminEmail | string | — | "admin@example.com" |
publicUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sentryshot
A SentryShot server (a network video recorder for IP cameras). A plain composable http workload: cameras, accounts and the recording index live on one PersistentVolume at /app/configs and the recordings on another at /app/storage, so it needs no external database. Two ReadWriteOnce volumes: one replica, recreated. Serves the web UI and API on :2020, with the live view at /live. sentryshot.toml is generated by the recipe and mounts read-only over the config volume; cameras are added in the UI. auth_basic starts with no accounts and nothing seeds one, so bootstrap once with auth=none, create the admin account, then switch back.
server http
The SentryShot server. Keeps accounts, monitors and the recording index at /app/configs and the recordings at /app/storage (two ReadWriteOnce volumes, one replica, recreated). `auth` picks the authentication plugin (`basic` or `none`); `maxDiskUsageGb` is the limit recordings are pruned against and belongs below `storageSize`. Pass `config` to supply the whole sentryshot.toml and enable the motion, object-detection, thumbnail or MQTT plugins. Probes by connection: every path answers 401 under basic auth. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sentryshot/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sentryshot" |
image | string | — | — |
auth | string | — | "basic" |
maxDiskUsageGb | int | — | 100 |
config | string | — | — |
configSize | quantity | — | "1Gi" |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sglang
An SGLang server (a serving runtime that loads one large language model onto the GPUs of its node and answers an OpenAI-compatible API in front of it) on the project's own image. IT DOES NOT RUN WITHOUT AN NVIDIA GPU: the image is built on CUDA with NVIDIA's entrypoint, gpus becomes an nvidia.com/gpu request and limit, and a node without the device plugin leaves the pod Pending — there is no CPU fallback worth offering. The model is a Hugging Face repository fetched at boot, tens of gigabytes for a mid-sized one, so HF_HOME points at the volume and a pod without it downloads everything again on each cold start; a gated repository needs HF_TOKEN from a Secret or the server exits. A cold start downloads and then compiles kernels for the device it found, so the wait is a startup probe. Single writer over a ReadWriteOnce cache: one replica, recreated, and more traffic means more of these behind something that spreads requests. Serves on :30000.
server http
The SGLang server. model names the Hugging Face repository to serve and gpus how many devices to request. storageSize sizes the model cache at /cache, which HF_HOME points at. secretName carries HF_TOKEN for a gated repository. extraArgs is appended to the launcher verbatim. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sglang/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sglang" |
image | string | — | — |
model | string | — | "Qwen/Qwen2.5-0.5B-Instruct" |
gpus | int | — | 1 |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
secretName | string | — | — |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"64Gi"},"requests":{"cpu":"2","memory":"16Gi"}} |
labels | object | — | — |
annotations | object | — | — |
shaarli
A Shaarli server (a self-hosted, database-free bookmarking and link-sharing app) on the official image; because Shaarli is flat-file, its data lives on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Shaarli server. Data at /var/www/shaarli/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/shaarli/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "shaarli" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
shiori
A Shiori server (a simple, self-hosted bookmarks manager with web-page archiving). A plain composable http workload that keeps its bookmarks and archived pages in SQLite on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and API on :8080.
server http
The Shiori server (runs `shiori serve`). Keeps its SQLite database and archives at /shiori on the volume. Point SHIORI_DATABASE_URL at external PostgreSQL/MySQL through env to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/shiori/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "shiori" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
shipshipship
A ShipShipShip server (a public changelog and roadmap page for telling customers what shipped, what is being worked on and what is planned, edited from a Kanban board behind an admin login). A plain composable http workload keeping its SQLite database, uploaded images and any theme installed through the admin interface on a PersistentVolume. The whole surface is public by design and only /admin asks for credentials, so putting the exposure behind an authenticating proxy wholesale defeats the point. The image ships a published JWT_SECRET signing the admin session, which is why the Secret is a prerequisite rather than hardening. The root path serves the public page once a theme is installed and the admin interface until then, so the probes ask for a connection rather than a path. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The ShipShipShip server. SQLite database, uploads and installed themes at /app/data on the volume. baseUrl is the address the site is reached at and only the newsletter uses it, to build the links in the mail it sends — unset by default, since no default is right anywhere. secretName holds ADMIN_USERNAME, ADMIN_PASSWORD and JWT_SECRET. env merges over the defaults GIN_MODE=release, PORT=8080 and DB_PATH, so a key you set wins. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/shipshipship/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "shipshipship" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
secretName | string | — | "shipshipship" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
shkeeper
A SHKeeper gateway (a self-hosted cryptocurrency payment processor: it watches for payments, credits invoices and calls the shop back, with nobody between the merchant and the chain) on the project's own image. IT TAKES NO PAYMENTS UNTIL A NODE ANSWERS IT — every currency needs its own backend daemon with its own chain data, hundreds of gigabytes and days of initial sync, and this recipe carries the gateway and none of them, so with none configured it serves its interface and accepts nothing. Whatever holds the instance's credentials can move funds, which makes the Secret the whole security boundary and an unauthenticated exposure an open till. Single writer over a ReadWriteOnce volume holding the database and the wallet state: one replica, recreated. Serves on :5000.
server http
The SHKeeper gateway. secretName carries the instance credentials and API keys (envFrom); kurly authors none. storageSize sizes the volume holding the database and wallet state. The per-currency backends are separate deployments this does not render. Compose an exposure onto the HTTP port, with authentication in front.
Import: import 'github.com/metio/kurly/workloads/shkeeper/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "shkeeper" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
shlink
A Shlink server (a self-hosted URL shortener with a REST API and rich analytics) on the official image, backed by an external PostgreSQL. Stateless — its state lives in the database, so it can run several replicas. Pairs with a cnpg-cluster named shlink-db. kurly authors no Secret; DB_PASSWORD (and optionally the GeoLite key) come from a provided Secret via envFrom. Serves on :8080.
server http
The Shlink server. dbHost/dbName/dbUser default to a cnpg-cluster named shlink-db. defaultDomain is the short-URL domain. secretName holds DB_PASSWORD (envFrom). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/shlink/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "shlink" |
image | string | — | — |
dbHost | string | — | "shlink-db-rw" |
dbName | string | — | "shlink" |
dbUser | string | — | "shlink" |
defaultDomain | string | — | — |
secretName | string | — | "shlink" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
silverbullet
A SilverBullet server (an extensible, self-hosted markdown notebook / personal knowledge base) on the official image. A plain composable http workload — your notes are plain markdown files on a PersistentVolume, no external database. kurly authors no Secret; SB_USER comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3000.
server http
The SilverBullet server. Your markdown space lives at /space on the volume. secretName holds SB_USER (user:password, envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/silverbullet/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "silverbullet" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "silverbullet" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
siyuan
A SiYuan server (a privacy-first, self-hosted personal knowledge-management and note-taking app with block-level editing and a local-first workspace) on the official image; its workspace (notes, assets and the database) lives on a PersistentVolume. Web access is gated by an access-auth code set via SIYUAN_ACCESS_AUTH_CODE (kurly authors no Secret). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :6806.
server http
The SiYuan server. Set SIYUAN_ACCESS_AUTH_CODE via env/envFromSecret to gate web access. Workspace at /siyuan/workspace. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/siyuan/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "siyuan" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
slink
A Slink server (an image sharing platform: uploads, albums, and shares that can expire or ask for a password). A plain composable http workload on SQLite, so it needs no external database. Two volumes, since the uploaded images and the databases grow at different rates; the volume holding the databases also holds the JWT keypair generated on first start, so losing it signs everyone out for good. Set `origin` to the URL people visit — SvelteKit refuses every upload and login whose Origin header does not match it. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves the client on :3000.
server http
The Slink server. Uploaded images live at /app/slink/images on the first volume, the SQLite databases and the generated JWT keypair at /app/var/data on the second. The client on :3000 proxies /api and /image to the PHP API on :8080 inside the pod, so :3000 is the port to expose. The entrypoint generates the keypair, hands the storage tree to its own account and drops privileges through s6, all of which it can only do from root, so this workload is deliberately less hardened. First start generates a 4096-bit key and migrates two databases, which is what the startup probe budgets for. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/slink/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "slink" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
dataSize | quantity | — | "5Gi" |
dataStorageClass | string | — | — |
origin | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
slskd
An slskd server (a web-based client for the Soulseek file sharing network). A plain composable http workload keeping its configuration, SQLite databases and downloads on a PersistentVolume. The Soulseek peer listen port rides onto the Service beside the web port: peers connect INWARD on it, and without a route from the internet transfers with firewalled peers never start, which looks like slow queues rather than a networking problem. It is a client of a public network, so the pod needs internet egress. Its own TLS listener is off (the certificate it mints is self-signed); terminate at the exposure. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5030.
server http
The slskd server. Configuration, SQLite and downloads at /app on the volume. secretName holds the Soulseek account (SLSKD_SLSK_USERNAME/SLSKD_SLSK_PASSWORD — an account on the public network, which kurly cannot mint), the local web login (SLSKD_USERNAME/SLSKD_PASSWORD) and SLSKD_JWT_KEY, which signs the tokens the web UI holds; slskd generates one at startup when unset, so every restart logs everybody out. https turns its own self-signed TLS listener back on. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/slskd/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "slskd" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "slskd" |
https | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
smtp4dev
An smtp4dev server (a self-hosted fake SMTP server for development: it receives the mail your apps send and shows it in a web UI, without delivering anything onward) on the official image. A plain composable http workload listening on TWO ports — the web UI on :80 and the SMTP sink on :25 (via kurly.extraPort). Its message database lives on a PersistentVolume under /smtp4dev. Single writer over a ReadWriteOnce volume: one replica, recreated. Point your apps at the Service on port 25 for SMTP.
server http
The smtp4dev server. Database at /smtp4dev on the volume; SMTP on port 25, web on 80. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/smtp4dev/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "smtp4dev" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | {"ServerOptions__Database":"/smtp4dev/database.db"} |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
snappymail
A SnappyMail server (a fast, modern, self-hosted webmail client that connects to your existing IMAP/SMTP servers) on the official image (pinned by digest; Renovate maintains it); config and per-account data on a PersistentVolume. SnappyMail is a client and does not run a mail server itself. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8888.
server http
The SnappyMail server. Configure IMAP/SMTP in the admin panel. Data at /var/lib/snappymail. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/snappymail/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "snappymail" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
snipe-it
A Snipe-IT server (a free IT asset and license management system) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with uploads on a PersistentVolume. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; DB_PASSWORD and APP_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Snipe-IT server. dbHost/dbName/dbUser point at a MySQL/MariaDB (e.g. mysql-cluster). appUrl is the public URL. secretName holds DB_PASSWORD and APP_KEY (envFrom). Uploads at /var/lib/snipeit. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/snipe-it/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "snipe-it" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "snipe-it-db" |
dbName | string | — | "snipeit" |
dbUser | string | — | "snipeit" |
appUrl | string | — | — |
secretName | string | — | "snipe-it" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
snypy
A SnyPy backend (the REST API and Django admin behind SnyPy's snippet library: snippets, labels, revisions and per-team sharing). A plain composable http workload on the project's own image, backed by an external PostgreSQL — all state is in the database, so it claims no volume and can run several replicas. This is the API HALF: the web interface ships as a separate image that calls this one FROM THE BROWSER, which is why frontendUrl is a URL a user's browser resolves rather than an in-cluster address, and why it also keys the CORS and CSRF allow-lists. The entrypoint migrates the database and runs collectstatic on every start, so the assets go to a scratch at /static and first boot gets a startup probe. Probed by connection: every route requires authentication and nothing answers at /, so any path a probe could name returns 401, 404 or a redirect. Serves on :8000.
server http
The SnyPy backend. All state is in PostgreSQL. secretName holds DATABASE_URL and SECRET_KEY — the database password is embedded in the URL, and SECRET_KEY signs sessions as well as the registration and password-reset tokens (upstream's published example value is `changeme!`). frontendUrl is the public URL of the SnyPy web interface: the verification and reset mails carry links built from it, and it seeds corsOrigins and csrfTrustedOrigins, so the interface cannot reach the API until it matches. allowedHosts accepts any Host by default, because a pod is reached by pod IP, Service name and whatever the exposure calls it, and a Host Django rejects answers 400. workers sets the gunicorn process count, each one a full copy of Django. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/snypy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "snypy" |
image | string | — | — |
frontendUrl | string | — | "http://localhost:4200" |
allowedHosts | array | — | ["*"] |
corsOrigins | array | — | — |
csrfTrustedOrigins | array | — | — |
secretName | string | — | "snypy" |
replicas | int | — | 1 |
workers | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
solectrus
A SOLECTRUS dashboard (a photovoltaic dashboard: what the panels produce, what the house consumes, what goes to and comes from the grid, and what that is worth) on the official image. This is the DASHBOARD only — the measurements it draws are written to InfluxDB by collectors that run beside a PV system and are not carried here, so a fresh instance with nothing feeding it renders empty rather than broken. It needs all three of an InfluxDB 2 (the measurements), a PostgreSQL (its own records, in a database that must be named solectrus_production, which the application hard-codes) and a Redis (cache and ActionCable); the entrypoint waits for each in turn and refuses to start without DB_HOST, INFLUX_HOST and REDIS_URL, then runs the Rails migrations, which is why the stage carries a startup probe. Pairs with a cnpg-cluster named solectrus-db. kurly authors no Secret; DB_PASSWORD, REDIS_URL, INFLUX_TOKEN, SECRET_KEY_BASE and ADMIN_PASSWORD come from a provided Secret via envFrom. Probed by connection, because Rails checks the Host header against APP_HOST and answers 403 to anything else. Stateless: a plain rolling Deployment with no volume. Serves on :3000.
server http
The SOLECTRUS dashboard. appHost is the host a browser reaches it at and Rails checks the request against it; forceSsl turns on the redirect to https, for where the pod is reached directly over TLS. installationDate is the day the system first produced anything, which every all-time figure is counted from. influxOrg/influxBucket must match what the collectors write into, and INFLUX_TOKEN only has to READ that bucket — the admin token gives a browser-facing app write access to the whole measurement history. secretName holds DB_PASSWORD, REDIS_URL, INFLUX_TOKEN, SECRET_KEY_BASE and ADMIN_PASSWORD (envFrom); SECRET_KEY_BASE signs the session cookies, so a value that changes on every restart signs everybody out. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/solectrus/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "solectrus" |
image | string | — | — |
replicas | int | — | 1 |
secretName | string | — | "solectrus" |
dbHost | string | — | "solectrus-db-rw" |
dbPort | int | — | 5432 |
dbUser | string | — | "solectrus" |
influxHost | string | — | "influxdb" |
influxPort | int | — | 8086 |
influxScheme | string | — | "http" |
influxOrg | string | — | "solectrus" |
influxBucket | string | — | "solectrus" |
appHost | string | — | "solectrus.example.com" |
forceSsl | bool | — | false |
installationDate | string | — | "2025-01-01" |
timezone | string | — | "Europe/Berlin" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sonarr
A Sonarr server — a PVR for Usenet and BitTorrent users to monitor, download and organise TV series. On the LinuxServer.io image; its application config (SQLite) lives on a PersistentVolume. Mount your media/download directories and point it at them in its settings. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8989.
server http
The A Sonarr server server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sonarr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sonarr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sosse
A Sosse server (Selenium Open Source Search Engine: it crawls sites with a real headless browser, keeps its own copies of the pages as screenshots and HTML snapshots, and searches that archive offline). A composable http workload backed by an external PostgreSQL, with the archive on a PersistentVolume. The image ships its own PostgreSQL and starts it by default; the command is overridden to skip it, because that database would live inside the image's tree where nothing in a cluster can back it up or fail it over. That override also writes a sudoers drop-in keeping SOSSE_DB_* across the one start-up step the entrypoint runs under sudo — without it that step alone falls back to 127.0.0.1, fails, and leaves the crawler's MIME handlers unloaded while the pod still goes Ready. Sosse reads every SOSSE_-prefixed environment variable as a configuration option, so service links are switched off — a Service named after the workload would otherwise inject SOSSE_PORT into its configuration. The entrypoint writes its configuration file, chowns its directories and drops privileges from root, so this workload is deliberately less hardened. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Sosse server: uwsgi, nginx and one crawler in the same container. Screenshots, HTML snapshots, collected static files and crawler scripts live at /var/lib/sosse on the volume; the index is in PostgreSQL. secretName holds SOSSE_DB_PASS — Sosse's own default is the published literal `sosse`, written into the configuration file the entrypoint generates on first start, and the environment variable overrides it. env is merged over the database coordinates and takes any sosse.conf option as SOSSE_<option>. The first account is admin/admin. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sosse/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sosse" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
dbHost | string | — | "sosse-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "sosse" |
dbUser | string | — | "sosse" |
secretName | string | — | "sosse" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
speedtest-tracker
A Speedtest Tracker server (runs internet speed tests on a schedule and keeps the history, with charts and alerting). A plain composable http workload keeping its SQLite database on a PersistentVolume — no external database. An init container creates the database file, which neither Laravel nor the image does. Run from a cluster it measures the NODE'S uplink, not a home connection. s6-overlay image, so it is deliberately less hardened. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Speedtest Tracker server. SQLite at /config/database.sqlite on the volume, created by an init container because Laravel opens that file and does not create it, and neither does the image — on a fresh volume the container otherwise starts, migrates nothing, and is torn down by its own supervisor with the real reason well above the line that stops it. appUrl is the public URL; secretName holds APP_KEY, which must be stable or stored values become unreadable. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/speedtest-tracker/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "speedtest-tracker" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
appUrl | string | — | — |
secretName | string | — | "speedtest-tracker" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
spegel
Spegel — a stateless, cluster-local OCI registry mirror (spegel.dev). A DaemonSet that serves image layers already present in each node's containerd content store to its peers over a peer-to-peer router, so a pull satisfied by any node never leaves the cluster; an init container writes containerd's registry-mirror config to pull through the local mirror first. Genuinely node-level infrastructure, so it authors its manifests directly (DaemonSet + init container + hostPath containerd socket/content, a NodePort the kubelet reaches the mirror on, and a headless Service peers bootstrap against via DNS) rather than composing a base kind. namespace is load-bearing: the bootstrap DNS name embeds it, so it must match where you deploy. Runs as root with hostPath mounts (the socket is root-owned); the posture is hardened as far as that allows. kurly features do not apply.
mirror daemon
The Spegel DaemonSet and its Services. namespace MUST match the deploy namespace (the peer-bootstrap DNS name embeds it). containerdSock/containerdContentPath/containerdRegistryConfigPath point at the node's containerd; the kubelet reaches the local mirror at both registryHostPort (straight to the local pod) and registryNodePort (through kube-proxy), so set registryHostPort=null where host ports are forbidden. dataDir persists routing state (null to disable).
Import: import 'github.com/metio/kurly/workloads/spegel/mirror.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "spegel" |
namespace | string | — | "spegel" |
image | string | — | — |
containerdSock | string | — | "/run/containerd/containerd.sock" |
containerdContentPath | string | — | "/var/lib/containerd/io.containerd.content.v1.content" |
containerdRegistryConfigPath | string | — | "/etc/containerd/certs.d" |
containerdNamespace | string | — | "k8s.io" |
registryPort | int | — | 5000 |
registryHostPort | int | — | 30020 |
registryNodePort | int | — | 30021 |
routerPort | int | — | 5001 |
metricsPort | int | — | 9090 |
dataDir | string | — | "/var/lib/spegel" |
logLevel | string | — | "INFO" |
resolveTags | bool | — | true |
mirrorResolveRetries | int | — | 3 |
mirrorResolveTimeout | string | — | "20ms" |
debugWeb | bool | — | false |
clusterDomain | string | — | "cluster.local" |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
tolerations | array | — | [{"operator":"Exists"}] |
nodeSelector | object | — | {"kubernetes.io/os":"linux"} |
affinity | object | — | — |
priorityClassName | string | — | — |
labels | object | — | — |
annotations | object | — | — |
spoolman
A Spoolman server (keeps track of 3D-printing filament: which spools you own, what is left on each, and what got used by which print). A plain composable http workload keeping its SQLite database on a PersistentVolume — no external database. It has no authentication of its own, which is what makes the printer integration simple and what makes exposing it a decision. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI and REST API on :8000.
server http
The Spoolman server. SPOOLMAN_DIR_DATA points the SQLite database at /data on the volume, rather than the image default under the app account's home directory — a sensible place for a desktop install and an awkward one to mount a volume over. Printers talk to the REST API with no credential, so keep it on the network the printers are on or authenticate in front of it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/spoolman/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "spoolman" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sqlpage
A SQLPage server (each .sql file is a page, and its result rows render as tables, forms and charts). A plain composable http workload where the SITE IS THE CONFIGURATION: the .sql files are delivered as a ConfigMap, and with none it serves only its own welcome page. Queries run with whatever rights DATABASE_URL grants and SQLPage has no user model, so authorisation is something the SQL must do. Single writer over a ReadWriteOnce volume for the default SQLite: one replica, recreated. Serves on :8080.
server http
The SQLPage server. site is the application — .sql files keyed by filename, mounted individually into the web root so the image's own assets survive. DATABASE_URL points at SQLite on the volume by default rather than the image default, which would put the database in the web root and serve it alongside the pages; point it at PostgreSQL or MySQL through env instead. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sqlpage/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sqlpage" |
image | string | — | — |
site | object | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
srs
An SRS server (Simple Realtime Server: a live streaming server that ingests RTMP, SRT or WebRTC and delivers HLS, HTTP-FLV and WebRTC). A plain composable http workload needing nothing external, with recorded segments on a PersistentVolume. Ships a starter configuration, because three of its lines are what make SRS runnable as a container at all — the shipped one daemonises and the process the container was started for exits. Serves HLS and HTTP-FLV on :8080, with RTMP on 1935, the API on 1985, WebRTC on 8000/UDP and SRT on 10080/UDP. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The SRS server. Segments are written to /usr/local/srs/objs/nginx/html on the volume. config replaces the starter srs.conf, which is mounted as a single file over the shipped one; keep its daemon-off, console-log and /tmp pid settings or the container will not stay up. candidate is the address WebRTC players are told to connect to — unset resolves to the pod address, which is right in-cluster and wrong outside it. Compose an exposure onto the HTTP port; the ingest ports are not HTTP and need their own routes.
Import: import 'github.com/metio/kurly/workloads/srs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "srs" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
config | string | — | — |
candidate | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
stash
A Stash server (an organiser and player for a personal video library: it indexes the files you point it at, scrapes metadata for them and serves a web player over the result). A plain composable http workload keeping its SQLite database, configuration and generated artefacts on a PersistentVolume. The image defaults every path into /root/.stash and so expects to run as root; each of those paths is an environment variable, so this stage points them all at the volume and runs unprivileged. Media is scanned from /data/media on that same volume, which is why the default size is large. Both probes ask for a connection, because a fresh server answers / with a setup wizard and a configured one redirects to a login. Scraping reaches the internet, so a NetworkPolicy that forgets egress leaves every scraper empty-handed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9999.
server http
The Stash server. SQLite, configuration, generated artefacts and the scanned media all live under /data on the volume (STASH_CONFIG_FILE, STASH_STASH, STASH_GENERATED, STASH_METADATA, STASH_CACHE, STASH_BLOBS), so size storageSize for the library. Needs egress for metadata scraping. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/stash/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "stash" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
static-web-server
A Static Web Server (a small asynchronous web server for static files, written in Rust and shipped as a single binary on a scratch image) on the official image. Stateless: a plain rolling Deployment. It ships a placeholder page at /public, which the defaults serve and which a consumer mounts their own content over — the root path is a parameter. The image binds :80, which an unprivileged user cannot, so the stage sets SERVER_PORT and declares the port it actually listens on; every other knob is a SERVER_* environment variable passed through env. Service links are off because a Service named server would have Kubernetes inject SERVER_PORT as a tcp:// URL over the listen port. Both probes check the connection rather than a path: the content is the consumer's, and a directory without an index answers 404. Serves on :8080.
server http
The Static Web Server. Serves the directory at root (the image placeholder page by default) on the HTTP port. Stateless. Compose an exposure onto the HTTP port, and mount the content to serve at root.
Import: import 'github.com/metio/kurly/workloads/static-web-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "static-web-server" |
image | string | — | — |
replicas | int | — | 2 |
root | string | — | "/public" |
port | int | — | 8080 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"32Mi"}} |
labels | object | — | — |
annotations | object | — | — |
status-responder
A tiny HTTP service that answers every request with one fixed status code and message. Deploy it once, globally, and route protected paths to it from a Gateway API HTTPRoute (kurly.expose.guard) to take them off the public internet — the portable substitute for the fixed-response filter Gateway API lacks.
responder http
One fixed-status responder (hashicorp/http-echo). Pair with kurly.expose.guard on the protected workload and kurly.expose.referenceGrant here for cross-namespace routing.
Import: import 'github.com/metio/kurly/workloads/status-responder/responder.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "forbidden" |
statusCode | int | — | 403 |
message | string | — | "forbidden" |
labels | object | — | — |
annotations | object | — | — |
stirling-pdf
A Stirling-PDF server (a locally-hosted web toolkit for splitting, merging, converting, and editing PDFs) on the official image. A plain composable http workload — it processes files in memory and keeps configuration on a PersistentVolume, no external database. The image runs LibreOffice and writes the root filesystem, so read-only-rootfs is relaxed while non-root and dropped capabilities stay. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Stirling-PDF server. Keeps configuration and custom files at /configs on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/stirling-pdf/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "stirling-pdf" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
superset
An Apache Superset deployment (a business-intelligence web application for exploring databases, building charts and assembling dashboards): a server stage and a worker stage on the project's own image, with PostgreSQL for metadata and Redis for the query cache and async results, so neither stage claims a volume. THE SECRET KEY ENCRYPTS EVERY STORED DATABASE PASSWORD — Superset keeps the credentials of the databases it queries encrypted with SECRET_KEY, and a key that changes makes all of them unreadable, so it comes from a Secret and stays put. The server migrates itself before it serves: an init container runs `superset db upgrade` and `superset init`, both idempotent, which is what lets a fresh deployment come up without a manual step, and the startup budget is long because the first run on an empty database takes minutes. Without a worker the asynchronous features fail silently — queries queue forever, alerts never fire, reports never arrive, and nothing in the server's log says so. Serves on :8088.
server http
The Superset web server (gunicorn). dbHost/dbPort/dbName/dbUser point at the metadata PostgreSQL and redisHost/redisPort at the cache; the rendered superset_config.py reads the password from the environment rather than embedding it, so the credential never lands in a ConfigMap. secretName holds SUPERSET_SECRET_KEY and DB_PASS. webWorkers is the number of gunicorn workers inside the pod, each a full Superset process. extraConfig is appended to the configuration file verbatim. Compose an exposure onto the HTTP port, and deploy the worker stage beside it.
Import: import 'github.com/metio/kurly/workloads/superset/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "superset" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "superset-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "superset" |
dbUser | string | — | "superset" |
redisHost | string | — | "superset-cache" |
redisPort | int | — | 6379 |
secretName | string | — | "superset" |
webWorkers | int | — | 4 |
extraConfig | string | — | "" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
worker worker
The Celery worker behind Superset: asynchronous SQL Lab queries, alerts, reports and thumbnails. Same image, same Secret and the same rendered configuration as the server — a worker with a different SECRET_KEY cannot decrypt a single stored database password and every task fails. beat runs the celery scheduler instead of a worker, and EXACTLY ONE may run, because two schedulers double-fire every scheduled task. concurrency is how many tasks one worker runs at a time. No Service.
Import: import 'github.com/metio/kurly/workloads/superset/worker.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "superset-worker" |
image | string | — | — |
replicas | int | — | 1 |
dbHost | string | — | "superset-db-rw" |
dbPort | int | — | 5432 |
dbName | string | — | "superset" |
dbUser | string | — | "superset" |
redisHost | string | — | "superset-cache" |
redisPort | int | — | 6379 |
secretName | string | — | "superset" |
beat | bool | — | false |
concurrency | int | — | 4 |
extraConfig | string | — | "" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"3Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
synapse
A Synapse server (the reference Matrix homeserver from the Matrix.org Foundation) on the official image; its configuration, signing keys and (with the default SQLite backend) database live on a PersistentVolume, generated on first start from SYNAPSE_SERVER_NAME. The server name is baked into every id and cannot be changed. Beyond a small instance, edit the generated homeserver.yaml to point at an external PostgreSQL. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8008.
server http
The Synapse server. serverName is the permanent Matrix server name; reportStats toggles anonymous stats. Data at /data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/synapse/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "synapse" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
serverName | string | — | — |
reportStats | string | — | "no" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
sync-in
A Sync-in server (file storage, syncing and sharing with real-time collaboration and per-space permissions) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one) with the files on a PersistentVolume. It is configured entirely through SYNCIN_-prefixed environment variables, which the server overlays onto the configuration model it ships, so no configuration document has to be authored; the credentials among them come from a provided Secret via envFrom and kurly authors no Secret. The entrypoint chowns the volume and drops to the image account with su-exec, so it starts as root with capabilities kept and privilege escalation allowed, and the root filesystem is writable because the completed first run is recorded beside the application's own code. That first run waits for the database and migrates the schema, so it has a startup probe, and it probes by connection because every HTTP path redirects to the login page or answers 401. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Sync-in server. Personal and shared spaces live at /app/data on the volume; accounts, permissions and metadata are in MySQL. secretName holds SYNCIN_MYSQL_URL, SYNCIN_AUTH_ENCRYPTIONKEY, SYNCIN_AUTH_TOKEN_ACCESS_SECRET and SYNCIN_AUTH_TOKEN_REFRESH_SECRET (envFrom), and optionally INIT_ADMIN, INIT_ADMIN_LOGIN and INIT_ADMIN_PASSWORD, which the first start uses to create the administrator account. Anything else the server understands can be passed through env as a SYNCIN_ variable. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/sync-in/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "sync-in" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
secretName | string | — | "sync-in" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
syncthing
A Syncthing server — a continuous, self-hosted file-synchronization tool that syncs folders between devices peer-to-peer (sync on :22000). On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8384.
server http
The A Syncthing server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/syncthing/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "syncthing" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tachidesk
A Suwayomi-Server (formerly Tachidesk): a self-hosted manga reader and library server, on the official image; its library, downloads and settings live on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4567.
server http
The Suwayomi/Tachidesk server. Library at /home/suwayomi/.local/share/Tachidesk. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tachidesk/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tachidesk" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tandoor
A Tandoor Recipes server (a self-hosted recipe manager and meal planner with a smart shopping list) on the official image, backed by an external PostgreSQL; uploaded media on a PersistentVolume under /opt/recipes/mediafiles. A plain composable http workload. kurly authors no Secret; SECRET_KEY and the PostgreSQL settings come from a provided Secret via envFrom. Pairs with a cnpg-cluster named tandoor-db. Single writer over a ReadWriteOnce media volume: one replica, recreated. Serves on :8080.
server http
The Tandoor server. Media at /opt/recipes/mediafiles on the volume; provide the Secret. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tandoor/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tandoor" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | "tandoor" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tautulli
A Tautulli server — a monitoring and tracking tool for Plex Media Server: history, statistics and notifications. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8181.
server http
The A Tautulli server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tautulli/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tautulli" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
technitium
A Technitium DNS Server (a self-hosted, privacy-focused DNS server with ad-blocking, DNS-over-HTTPS/TLS and a full web console) on the official image; its configuration and zones live on a PersistentVolume. It answers DNS on :53 (TCP/UDP), separate ports to add a Service for. kurly authors no Secret; DNS_SERVER_ADMIN_PASSWORD comes from a provided Secret via envFrom. It binds the privileged DNS port so it runs as root with a writable root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. The web console serves on :5380.
server http
The Technitium DNS server. secretName holds DNS_SERVER_ADMIN_PASSWORD (envFrom). Config at /etc/dns; DNS (:53) needs an extra Service. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/technitium/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "technitium" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "technitium" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tempo
Grafana Tempo as a tempo-operator `TempoStack` custom resource: one CR reconciles the whole distributed tracing backend (distributor, ingester, querier, query-frontend, compactor) over object storage. Authors the CR (the same shape as loki) for the operator to own the components, config, and Services. Requires the tempo-operator and an object-storage Secret. Pairs with the seaweedfs workload for S3 and the otel-collector workload for span ingestion.
server tempo
The TempoStack. storageSecret names the object-storage Secret you create (keys bucket/endpoint/access_key_id/access_key_secret) — point it at the seaweedfs workload's S3. storageSize is the per-component PVC. The operator chooses the Tempo image, so there is none to pin. Send spans to tempo-<name>-distributor (OTLP :4317/:4318) and read them from Grafana via tempo-<name>-query-frontend:3200.
Import: import 'github.com/metio/kurly/workloads/tempo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tempo" |
storageSecret | string | — | "tempo-storage" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
teslamate
A TeslaMate server (logs a Tesla's drives, charges and state of charge into PostgreSQL and reports efficiency, cost and mileage from it). A plain composable http workload backed by an external PostgreSQL, claiming no volume of its own. THE WEB APP AUTHENTICATES NOBODY: whoever reaches it can read where the car has been and change its sleep settings, so an exposure that leaves the cluster belongs behind an authenticating proxy. The Grafana dashboards the project publishes are separate software reading the same database. Serves on :4000.
server http
The TeslaMate server. Everything it records lives in the external PostgreSQL, so the pod is stateless. secretName holds DATABASE_PASS and ENCRYPTION_KEY — ENCRYPTION_KEY encrypts the stored Tesla API tokens, so changing it means signing the car in again. MQTT is off unless mqttHost is set. The root filesystem is writable because the Erlang release generates its start-up files and caches elevation data inside its own install tree. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/teslamate/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "teslamate" |
image | string | — | — |
dbHost | string | — | "teslamate-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "teslamate" |
dbUser | string | — | "teslamate" |
secretName | string | — | "teslamate" |
mqttHost | string | — | — |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
thanos
The Thanos components as separate, independently-scaled stages under one workload: query (the stateless Querier fanning out to StoreAPIs for a deduplicated global view), query-frontend (an optional splitting/caching layer in front of it), and ruler (recording/alerting rules evaluated against Query). query and query-frontend are plain composable http workloads; ruler authors a prometheus-operator ThanosRuler custom resource and needs that operator installed.
compact http
The Thanos Compactor (a `thanos compact --wait` Deployment): it compacts raw blocks in object storage, builds the 5m/1h downsampled resolutions, and applies retention. A SINGLETON — a second compactor over the same bucket corrupts the data, so replicas is pinned to 1 (asserted) and it rolls with Recreate; shard a large bucket with --selector.relabel-config across separate compactors. Reads the same objstoreSecret as store. retentionRaw/5m/1h bound each resolution (0d = keep forever).
Import: import 'github.com/metio/kurly/workloads/thanos/compact.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-compact" |
image | string | — | — |
objstoreSecret | string | — | "thanos-objstore" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
retentionRaw | string | — | "0d" |
retention5m | string | — | "0d" |
retention1h | string | — | "0d" |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
extraArgs | array | — | — |
query http
The Thanos Querier (a plain `thanos query` Deployment + Service). endpoints are the StoreAPI targets it fans out to over gRPC (dnssrv+ resolves every replica); queryReplicaLabels deduplicate HA replicas. Serves the Prometheus API on :10902 (gRPC StoreAPI on :10901 for federation). Point a Grafana datasource or the query-frontend at it.
Import: import 'github.com/metio/kurly/workloads/thanos/query.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-query" |
image | string | — | — |
replicas | int | — | 2 |
endpoints | array | — | — |
queryReplicaLabels | array | — | ["prometheus_replica","replica"] |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
extraArgs | array | — | — |
query-frontend http
The Thanos Query Frontend (a plain `thanos query-frontend` Deployment + Service): an optional layer that splits long-range queries, caches results (in-memory by default), and forwards to a downstream Querier. downstreamUrl defaults to a thanos-query Service on :10902 in the same namespace. For a shared cache, pass --query-range.response-cache-config-file via extraArgs and back it with the memcached or valkey workload.
Import: import 'github.com/metio/kurly/workloads/thanos/query-frontend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-query-frontend" |
image | string | — | — |
replicas | int | — | 2 |
downstreamUrl | string | — | "http://thanos-query:10902" |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
extraArgs | array | — | — |
receive stateful
The Thanos Receiver (a `thanos receive` StatefulSet + headless Service): the push-based ingestion path — Prometheus remote-writes to it (:19291) instead of running a sidecar. It holds recent data in a local TSDB, serves it to the Querier over the StoreAPI (:10901), and uploads blocks to object storage. Receivers form a hashring generated from the replica count; replicationFactor copies each series across pods, each tagged with a receive_replica label the Querier deduplicates. Reads the same objstoreSecret as store.
Import: import 'github.com/metio/kurly/workloads/thanos/receive.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-receive" |
image | string | — | — |
replicas | int | — | 1 |
replicationFactor | int | — | 1 |
objstoreSecret | string | — | "thanos-objstore" |
tsdbRetention | string | — | "15d" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
extraArgs | array | — | — |
ruler thanos-ruler
The Thanos Ruler as a prometheus-operator ThanosRuler custom resource. queryEndpoints (verbatim operator schema, dnssrv+ resolves every Query replica) are what it evaluates rules against; ruleSelector/ruleNamespaceSelector decide which PrometheusRule objects it loads ({} selects everything, none selects nothing). alertmanagersUrl lists plain Alertmanager targets; for authenticated ones reference your own Secret through spec.alertmanagersConfig. Reach it at thanos-ruler-operated.<namespace>.svc:10902.
Import: import 'github.com/metio/kurly/workloads/thanos/ruler.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-ruler" |
image | string | — | — |
replicas | int | — | 1 |
queryEndpoints | array | — | — |
alertmanagersUrl | array | — | — |
ruleSelector | object | — | — |
ruleNamespaceSelector | object | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
spec | object | — | — |
store stateful
The Thanos Store Gateway (a `thanos store` StatefulSet + headless Service): it serves historical blocks from object storage over the StoreAPI so the Querier reaches data older than the sidecars hold. Stateful — a per-pod PVC caches block index headers. objstoreSecret names a Secret you provide (key objstore.yaml, fillable with kurly.externalSecret) pointing at the bucket; it pairs with the seaweedfs S3 workload. Add it to the Querier with dnssrv+_grpc._tcp.<name>-headless…
Import: import 'github.com/metio/kurly/workloads/thanos/store.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thanos-store" |
image | string | — | — |
replicas | int | — | 1 |
objstoreSecret | string | — | "thanos-objstore" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
extraArgs | array | — | — |
thelounge
A The Lounge server (a modern, self-hosted web IRC client: always-connected, multi-user, accessible from any browser) on the official image; its configuration and per-user data live on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9000.
server http
The The Lounge server. Data at /var/opt/thelounge. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/thelounge/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thelounge" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
thumbor
A thumbor server (an on-demand image service that crops, resizes, filters and re-encodes images fetched from a URL) on the official image. Runs `thumbor --use-environment`, so every configuration key is an environment variable of the same name; kurly authors no Secret, and SECURITY_KEY comes from a provided one. The published default key plus ALLOW_UNSAFE_URL let anyone have any URL fetched and re-served, so set both. Stateless: the file-storage cache lives in a per-replica scratch under /tmp, so a plain rolling Deployment scales horizontally. Serves on :8888.
server http
The thumbor server. Configuration keys are environment variables (--use-environment). Set SECURITY_KEY and ALLOW_UNSAFE_URL=False, and restrict ALLOWED_SOURCES. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/thumbor/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "thumbor" |
image | string | — | — |
replicas | int | — | 2 |
cacheSize | quantity | — | "2Gi" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tik
A lightweight ticket board and release supervisor. One process serves a read-only board and runs the store's writers over a shared append-only event store.
backend http
The tik backend supervisor: a single-writer http app over a ReadWriteOnce store (one replica, recreated to avoid deadlocking on the volume). Compose an exposure recipe to serve the board.
Import: import 'github.com/metio/kurly/workloads/tik/backend.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tik" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
tika
An Apache Tika server (a content-analysis toolkit that detects and extracts text and metadata from over a thousand file types) on the official image. Stateless: a plain rolling Deployment. The text-extraction companion apps like paperless-ngx expect. Serves on :9998.
server http
The Apache Tika server. Stateless; usually reached in-cluster.
Import: import 'github.com/metio/kurly/workloads/tika/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tika" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tiny-tiny-rss
A Tiny Tiny RSS server (a web-based news feed reader and aggregator for RSS and Atom), backed by an external PostgreSQL, with its working copy on a PersistentVolume. The official image is PHP-FPM ALONE — it speaks FastCGI on :9000 and serves nothing a browser can talk to — so this runs THREE processes in one pod: the FPM app as the workload's container, upstream's nginx and the feed-fetching updater daemon as SIDECARS over the shared volume. nginx serves the app on :80 UNDER /tt-rss, which is where the app installs its working copy. TTRSS_SELF_URL_PATH must be the URL a browser actually reaches it at or the app refuses to work, so selfUrl is a parameter with no default. kurly authors no Secret; TTRSS_DB_PASS comes from a provided Secret via envFrom, and the database user must be allowed to create the pg_trgm extension. Pairs with a cnpg-cluster named tiny-tiny-rss-db. The startup script runs as root (adduser, chown, sudo), so the hardened defaults are relaxed. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Tiny Tiny RSS FPM app plus its nginx and updater sidecars. webImage sets the nginx sidecar image; selfUrl is the public URL including the /tt-rss suffix; secretName holds TTRSS_DB_PASS (envFrom). Settings passed through `env` reach the updater too, unlike a composed kurly.env(). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tiny-tiny-rss/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tiny-tiny-rss" |
image | string | — | — |
webImage | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "tiny-tiny-rss-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "tinytinyrss" |
dbUser | string | — | "tinytinyrss" |
selfUrl | string | — | — |
secretName | string | — | "tiny-tiny-rss" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tinyfeed
A tinyfeed generator (a CLI that reads a list of RSS, Atom and JSON feeds and writes ONE static HTML page aggregating them) on the official image, running the CLI in its daemon mode so the page is rewritten on an interval, onto a PersistentVolume. There is no server here and that is the whole shape of the workload: tinyfeed binds no port and answers no request, so this is a worker with no Service and nothing to compose an exposure onto — serving the page is a second workload, an HTTP server (kurly's caddy is one) mounting the same claim read-only, which needs a ReadWriteMany class or both pods on one node, hence accessModes being a parameter. The feed list is the workload: feeds is rendered to the input file the CLI reads and mounted as a ConfigMap, so changing which feeds are aggregated is a re-render rather than an exec into a pod. The image is FROM scratch around one static Go binary with a non-root USER — no shell, no entrypoint dropping privileges, nothing written outside the volume — so it keeps the fully restricted posture unrelaxed. No external database, cache or broker, and no Secret: every feed it reads is a public URL. Single writer over one file: one replica, recreated.
generator worker
The tinyfeed generator, in daemon mode: it fetches every feed, writes the page to outputPath on the volume, and sleeps interval minutes before doing it again. feeds is the list of feed URLs, rendered to /etc/tinyfeed/feeds.txt in a ConfigMap and read with --input, so changing the list is a re-render. title and description are the page's heading and the line under it; stylesheet and script are links the generated page carries, and template is a path INSIDE the container, so a custom layout comes from a ConfigMap composed on with kurly.config. limit and limitPerFeed cap the articles shown; anything else the CLI accepts (--order-by, --requests, --timeout, --quiet) goes through extraArgs verbatim. accessModes exists because the HTTP server serving the page mounts the same claim: ReadWriteMany lets it run on another node. No exposure — this stage has no Service.
Import: import 'github.com/metio/kurly/workloads/tinyfeed/generator.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tinyfeed" |
image | string | — | — |
feeds | array | — | ["https://feed.lovergne.dev/releases.atom"] |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
outputPath | string | — | "/output/index.html" |
interval | int | — | 1440 |
title | string | — | "Feed" |
description | string | — | — |
stylesheet | string | — | — |
script | string | — | — |
template | string | — | — |
limit | int | — | 256 |
limitPerFeed | int | — | 256 |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
traccar
A Traccar server (a self-hosted GPS tracking platform: it ingests position reports from many GPS devices and phone apps and shows them live on a map) on the official image. Its settings are a traccar.xml mounted as a ConfigMap; with the default embedded H2 database its data lives on a PersistentVolume. It listens for device protocols on extra ports (5000-5150), separate ports to add Services for. Point it at an external PostgreSQL/MySQL (the database.* keys) to scale past the embedded writer. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8082.
server http
The Traccar server. configXml is traccar.xml, mounted verbatim; the default uses embedded H2 on the data volume. Device protocol ports (5000-5150) need extra Services. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/traccar/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "traccar" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
configXml | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
traduora
An ever-traduora server (a translation management platform: teams edit their locales in a web UI, and an import/export API moves the same strings in and out of a build) on the official image, backed by an external PostgreSQL and holding no state of its own. kurly authors no Secret; the database login and TR_SECRET come from a provided Secret via envFrom, and TR_SECRET signs every access token while shipping upstream as the literal string `secret`. The schema is migrated at startup (TR_DB_AUTOMIGRATE), so the first boot takes a while — the startup probe covers it. The image sets no USER, so this runs explicitly as uid 1000 to keep the hardened default. PostgreSQL is the default engine because an early traduora migration alters a column another table's foreign key depends on, which MariaDB refuses. Pairs with a cnpg-cluster named traduora-db. Stateless: a plain rolling Deployment. Serves on :8080.
server http
The traduora server. dbType is traduora's own vocabulary (postgres or mysql); the credentials live in the Secret. secretName holds TR_DB_USER, TR_DB_PASSWORD and TR_SECRET (envFrom). virtualHost is the URL invitation and password-reset mails link to, so the default points at localhost and must be set. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/traduora/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "traduora" |
image | string | — | — |
dbType | string | — | "postgres" |
dbHost | string | — | "traduora-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "traduora" |
virtualHost | string | — | "http://localhost:8080" |
secretName | string | — | "traduora" |
signupsEnabled | bool | — | true |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
traefik
A Traefik edge router that discovers its own configuration from the cluster: it watches Ingress, IngressRoute and Gateway API objects and routes traffic to the Services behind them, obtaining certificates as it goes. UNPRIVILEGED PORTS, DELIBERATELY — Traefik's image listens on :80 and :443, which a container without NET_BIND_SERVICE cannot bind, so the entry points are 8000 and 8443 and the Service maps 80 and 443 onto them; a LoadBalancer in front makes that invisible to clients and the hardened posture stands. The cluster-wide grant covers the Kubernetes Ingress provider and the Gateway API; Traefik's OWN CRDs are deliberately absent, because a cluster that has not installed them would be granted permissions on kinds that do not exist — extraRules is where they go. The dashboard is not exposed and api.insecure is off: it has no authentication of its own, so publishing it publishes the routing table of the whole cluster. acmeEmail turns on a Let's Encrypt resolver storing its account on the volume, and pins the deployment to one replica, because two routers racing for one account produce rate-limit failures rather than certificates.
ingress http
The Traefik router. namespace is where its ServiceAccount lives and is required. acmeEmail enables the Let's Encrypt resolver (and forces a single replica); storageSize sizes the certificate store. extraRules adds to the cluster-wide grant — Traefik's own CRDs belong there on a cluster that has them. extraArgs is appended to its flags. Compose an exposure onto the entry points, usually a LoadBalancer Service.
Import: import 'github.com/metio/kurly/workloads/traefik/ingress.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "traefik" |
image | string | — | — |
namespace | string | — | "traefik" |
replicas | int | — | 2 |
acmeEmail | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
extraRules | array | — | — |
extraArgs | array | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
traggo
A Traggo server (tag-based time tracking with a web interface). A plain composable http workload that keeps its SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app and GraphQL API on :3030.
server http
The Traggo server. Keeps its SQLite database at /data on the volume (TRAGGO_DATABASE_CONNECTION), so it needs nothing external. Reads the first account password from the Secret; compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/traggo/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "traggo" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "traggo" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
trailbase
A TrailBase server (a single-executable application backend: a SQLite database with type-safe REST and realtime APIs, authentication, a WebAssembly runtime and an admin UI, in one process). A plain composable http workload keeping its whole state — database, configuration, auth keys, uploaded files — in one data directory on a PersistentVolume, mounted at /app/traildepot the way upstream mounts it, which masks the WebAssembly components the image ships underneath. It initialises a fresh data directory on first start and prints the administrator credentials to the log, so no Secret is minted here. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :4000.
server http
The TrailBase server. Everything it keeps — the SQLite database, the configuration, the auth keys and uploaded files — lives under /app/traildepot on the volume. The administrator account is created on first start and its credentials printed to the log; the admin UI is at /_/admin. Service links are off because a Service named after the workload injects TRAILBASE_PORT as a tcp:// URL into the environment the process reads. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/trailbase/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "trailbase" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
transmission
A Transmission server — a fast, lightweight, self-hosted BitTorrent client with a web UI. On the LinuxServer.io image; its config (SQLite) lives on a PersistentVolume. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :9091.
server http
The A Transmission server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/transmission/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "transmission" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
transmute
A Transmute server (a self-hosted file converter and compressor driving ffmpeg, ImageMagick and friends from a web UI) on the official image; uploads, results and state live on a PersistentVolume. appUrl is its own public address and has no sensible default. A conversion is CPU- and memory-hungry and writes the file twice, once under /tmp and once on the store, so the defaults suit small files only. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3313.
server http
The Transmute server. appUrl is the public URL, with the scheme a browser sees. Data at /app/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/transmute/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "transmute" |
image | string | — | — |
appUrl | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
trilium
A TriliumNext Notes server (a hierarchical note-taking application for building personal knowledge bases) on the official image. A plain composable http workload that keeps its notes in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web app and sync API on :8080.
server http
The TriliumNext server. Keeps notes in SQLite at /home/node/trilium-data on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/trilium/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "trilium" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
trip
A TRIP server (a minimalist map tracker and trip planner for points of interest and multi-day itineraries). A plain composable http workload keeping its SQLite database, its configuration file, uploaded images and attachments under /app/storage on a PersistentVolume, so it needs no external database. Every setting is a pydantic field read from storage/config.env and overridable per key by an environment variable, so anything the settings screen writes can be handed in through env instead — where the environment wins over the file. SECRET_KEY signs the tokens users hold and is minted into the configuration file on first start when the environment carries none, so a Secret is optional; supplying one keeps sessions alive across a rebuilt volume. Map tiles are fetched by the browser rather than the pod, so blocked egress does not blank the map — server-side OIDC discovery is the one thing that needs it. Runs as 1000 under the hardened default with a read-only root filesystem. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8000.
server http
The TRIP server. SQLite database, config.env, assets, attachments and backups at /app/storage on the volume. secretName is optional and holds SECRET_KEY plus any OIDC credentials via envFrom. env is merged into the container environment, where a key beats the same key in the configuration file the settings screen writes. /api/info answers unauthenticated and is what the probes read. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/trip/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "trip" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
triton-inference-server
An NVIDIA Triton Inference Server (models served from a directory over HTTP and gRPC, with TensorRT, ONNX, PyTorch, TensorFlow and Python backends) on NVIDIA's own image. AN EMPTY MODEL REPOSITORY IS FATAL: Triton loads everything under the repository at start and exits when it can load none, which is what a freshly provisioned volume gives it — so the stage renders --model-control-mode=explicit and the server comes up holding nothing, with models loaded through its API afterwards. GPUs are optional and the CUDA image is not: gpus=0 requests no device and serves on the CPU, real for the ONNX and Python backends and slow for the rest, while the image is eight gigabytes and runs NVIDIA's entrypoint either way. :8000 is HTTP, :8001 gRPC and :8002 Prometheus metrics. Single writer over a ReadWriteOnce repository: one replica, recreated, while a ReadWriteMany volume lets several servers share one set of models. Serves on :8000.
server http
The Triton server. gpus requests devices and zero serves on the CPU. modelControlMode defaults to explicit so an empty repository still boots; poll and none load at start and none exits when there is nothing. storageSize and accessModes size and share the model repository at /models. extraArgs is appended verbatim. Compose an exposure onto the HTTP port; gRPC clients need a route that speaks it.
Import: import 'github.com/metio/kurly/workloads/triton-inference-server/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "triton-inference-server" |
image | string | — | — |
gpus | int | — | 0 |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
modelControlMode | string | — | "explicit" |
extraArgs | array | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"16Gi"},"requests":{"cpu":"1","memory":"4Gi"}} |
labels | object | — | — |
annotations | object | — | — |
tube-archivist
A Tube Archivist server (subscribe to YouTube channels and playlists, download what they publish and serve the result as a searchable media library with metadata, artwork and playback progress). A composable http workload with two PersistentVolumes — artwork and yt-dlp state at /cache, the videos at /youtube — and two external dependencies it cannot supply itself: everything indexed lives in Elasticsearch 8, every download and scan runs through a Redis task queue. The opensearch-cluster workload does NOT substitute, since the official Elasticsearch client refuses a server that does not identify as Elasticsearch. taHost is the public origin Django validates every request against and answers a mismatch with a bare 400; it is left unset rather than defaulted to a placeholder that would be wrong everywhere. The image selects no account and its nginx is configured to run as root, so it runs as root with a writable root filesystem, though nothing escalates. Probed by connection, because every path redirects or answers 403 unauthenticated. Needs egress to YouTube, which a NetworkPolicy written from the manifest silently blocks. Single writer over ReadWriteOnce volumes: one replica, recreated, which also keeps two schedulers from downloading the same videos twice. Serves on :8000.
server http
The Tube Archivist server: nginx and the Django backend behind it, the celery workers and the scheduler, in one pod. taHost is the public origin, protocol included — without it the application refuses to start, and with the wrong one every request is a bare 400. esUrl points at an Elasticsearch 8 and redisCon at a Redis; neither is rendered here. Artwork and yt-dlp state at /cache, the downloaded videos at /youtube, sized separately since the media half grows without limit. secretName holds TA_USERNAME and TA_PASSWORD (the first administrator, read once when the account is created) and ELASTIC_PASSWORD, which must match the Elasticsearch it is pointed at. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tube-archivist/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tube-archivist" |
image | string | — | — |
taHost | string | — | — |
mediaSize | quantity | — | "500Gi" |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
esUrl | string | — | "http://tube-archivist-es:9200" |
redisCon | string | — | "redis://tube-archivist-cache-headless:6379" |
timezone | string | — | "UTC" |
secretName | string | — | "tube-archivist" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
tubesync
A TubeSync server (subscribe to YouTube channels and playlists and it downloads new uploads on a schedule, named and tagged so Plex or Jellyfin picks them up as a library). A plain composable http workload with two PersistentVolumes — a small one for the database and configuration, a large one for media. An s6-overlay image, so deliberately less hardened. Single writer over ReadWriteOnce volumes: one replica, recreated, which also keeps two schedulers from fetching the same videos twice. Serves on :4848.
server http
The TubeSync server. Database and configuration at /config, media at /downloads — sized separately, since the media half grows without limit. puid/pgid are the ownership the media is written with, so a media server sharing the volume can read it. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tubesync/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tubesync" |
image | string | — | — |
mediaSize | quantity | — | "200Gi" |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tuwunel
A Tuwunel server (a Matrix homeserver written in Rust, the successor to conduwuit) on the official image; its embedded RocksDB lives on a PersistentVolume. Settings come from the environment under the TUWUNEL_ prefix, with the ancestors CONDUWUIT_ and CONDUIT_ names still accepted, and no config file is named because a path that does not exist is refused rather than ignored. The serverName is baked into every user and room id at first start and cannot be changed. Service links are off: the injected TUWUNEL_PORT is a tcp:// URL landing on the binary own listen-port setting. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8008.
server http
The Tuwunel Matrix homeserver. serverName is permanent (baked into ids); allowRegistration toggles open sign-up. Data at /var/lib/tuwunel. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tuwunel/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tuwunel" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
serverName | string | — | — |
allowRegistration | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
tvheadend
A Tvheadend server (a self-hosted TV streaming server and DVR: DVB, IPTV, SAT>IP, with a web UI) on the LinuxServer.io image (pinned by digest; Renovate maintains it); config on a PersistentVolume. HTSP streaming (:9982) needs its own Service; tuners are hardware and not modelled. The s6-overlay init runs as root and drops to the PUID/PGID user. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web UI on :9981.
server http
The Tvheadend server. puid/pgid own the mounted files; timezone sets TZ. Config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/tvheadend/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "tvheadend" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
uptime-kuma
An Uptime Kuma monitoring server (self-hosted uptime monitoring and status pages). A plain composable http workload that keeps its checks, history, and settings in a SQLite database on a PersistentVolume — no external database. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the dashboard and status pages on :3001.
server http
The Uptime Kuma server. Keeps everything in SQLite at /app/data on the volume, so it needs nothing external. env carries extra settings (UPTIME_KUMA_* overrides). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/uptime-kuma/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "uptime-kuma" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
upvote-rss
An Upvote RSS server (turns a subreddit, a Hacker News front page or a Lemmy community into a full-text RSS feed, filtered by score, so a reader shows only what the community voted up). A plain composable http workload keeping its response cache on a PersistentVolume. It fetches every article from the site that published it, so the pod needs egress — a NetworkPolicy that forgets this leaves the feeds empty — and it has no authentication at all, so anyone who reaches it can make it fetch a URL on whatever network the pod sits on. The entrypoint chowns its directories and drops to an unprivileged account with su-exec, which it can only do from root, so it runs as root with escalation allowed and capabilities kept; the root filesystem is writable with it, since the image ships no such account and the entrypoint creates it at every start. Service links are disabled because PHP publishes the environment as $_SERVER and the application reads REDIS_HOST and REDIS_PORT from it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Upvote RSS server. Response cache at /app/cache on the volume. secretName holds the optional credentials the feeds are built with — Reddit client id and secret, and an API key for whichever summarisation service is configured; without them Reddit is read anonymously and articles are not summarised. Needs internet egress. Compose an exposure onto the HTTP port only behind authentication.
Import: import 'github.com/metio/kurly/workloads/upvote-rss/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "upvote-rss" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
valkey
A persistent Valkey server (the BSD Redis fork) on the official upstream image, as a kurly.stateful workload with a per-pod PVC and a headless Service. Single-instance stage; a Redis-compatible alternative runs by overriding the image.
cache worker
An in-memory Valkey cache that upgrades its version with zero downtime and no data loss, on the stock image and no orchestrator — the replication hand-off lives entirely in the pod manifests (headless Service, maxSurge, an initContainer that replicates the running peer, and a preStop failover).
Import: import 'github.com/metio/kurly/workloads/valkey/cache.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "valkey" |
image | string | — | — |
maxMemory | string | — | "256mb" |
kubectlImage | string | — | — |
instance stateful
The single-instance Valkey server: a StatefulSet with append-only persistence into a volumeClaimTemplate. Compose + features as usual (it is a composable kurly.stateful app). `image` also accepts a Redis build — Valkey is its BSD fork and takes the same configuration — so name the workload for its role rather than its engine, and a consumer holding an endpoint never learns which it got.
Import: import 'github.com/metio/kurly/workloads/valkey/instance.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "valkey" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
maxMemory | string | — | — |
vanilla-cookbook
A Vanilla Cookbook server (a recipe manager that imports recipes from other cookbook applications and from the web). A plain composable http workload keeping its SQLite database on one PersistentVolume and its uploaded images and import files on another — two volumes, because a single mount would leave one of the two data directories on the container filesystem. ORIGIN is the one setting it cannot infer: SvelteKit checks it against the request when a form is submitted, so a wrong value turns every login and every save into a rejected cross-site request. It runs as root with a writable root filesystem and its capabilities kept, because the entrypoint creates the data directories, aligns the application user with PUID/PGID, chowns both volumes before handing over with gosu, and starts a cron daemon for the scheduled database backup. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :3000.
server http
The Vanilla Cookbook server. SQLite at /app/prisma/db and uploads at /app/uploads, each on its own volume. Set origin to the public URL the application is reached at. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vanilla-cookbook/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vanilla-cookbook" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
uploadsSize | quantity | — | "5Gi" |
storageClass | string | — | — |
origin | string | — | "http://localhost:3000" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
varnish
A Varnish Cache instance (an HTTP cache in front of something slower: responses held in memory and served without waking the backend, with the policy written in VCL) on the official image. THE BACKEND IS RESOLVED WHEN THE VCL IS COMPILED, NOT WHEN A REQUEST ARRIVES: Varnish refuses to compile a configuration naming a host that does not resolve and the pod never listens, so backendHost must point at a Service that already exists and a cache whose backend is deleted fails its next restart rather than its next request. THE CACHE IS MEMORY AND MEMORY IS A LIMIT: size is what Varnish may use for objects and the pod's memory limit must be comfortably larger, since a cache sized at the limit is a pod the kernel kills under load rather than one that evicts. The rendered VCL only names the backend; anything beyond caching what the backend calls cacheable means supplying the whole file, because there is no half-way merge of somebody else's policy. Nothing is persisted by design, so a rollout is a cold cache and a burst straight to the backend, and replicas each fill their own. Stateless: a plain rolling Deployment. Serves on :80.
cache http
The Varnish cache. backendHost/backendPort name the Service it caches for, and it must resolve at start because Varnish resolves it while compiling the VCL. size is VARNISH_SIZE, the object storage it may use — keep the memory limit above it. vcl replaces the rendered configuration entirely. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/varnish/cache.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "varnish" |
image | string | — | — |
replicas | int | — | 1 |
backendHost | string | — | "backend" |
backendPort | int | — | 80 |
size | string | — | "100M" |
vcl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
vaultwarden
A Vaultwarden server (a lightweight, Bitwarden-compatible password manager in Rust). A plain composable http workload that keeps its vault, attachments, and JWT signing key in a SQLite database on a PersistentVolume — no external database needed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the web vault and API on :8080.
server http
The Vaultwarden server. domain is the public URL — WebAuthn/passkeys, attachment links, and email all need it. signupsAllowed is off by default (turn on to bootstrap, then off). env carries extra settings (ADMIN_TOKEN, SMTP_*, or DATABASE_URL to move to external Postgres) — the admin token and any DB password should come from a Secret, kurly mints none. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vaultwarden/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vaultwarden" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
domain | string | — | — |
signupsAllowed | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
vector
A Vector agent (a pipeline for logs, metrics and traces: it collects them, reshapes them with a transform language, and sends them wherever they are meant to go). A composable daemon workload, because collecting a node's container logs means reading them off that node. THE DEFAULT SINK PRINTS TO STDOUT, WHICH IS NOT A PIPELINE — where the data goes is the decision only the deployment can make and there is no honest default, so the agent starts with what it collects visibly going nowhere useful, which is a better failure than buffering into a void; an empty sinks map is refused outright. It reads every container's logs on the node from /var/log, mounted READ-ONLY because an agent that could write them could rewrite the record it exists to ship, and takes a cluster-wide read grant on pods and namespaces to label the lines. State is the checkpoint file: on the pod it survives a container restart but not a reschedule, and dataDir moves it to the node when that matters.
agent daemon
The Vector agent, one pod per node. sinks is where the collected data goes (the default prints to stdout); sources adds to the kubernetes_logs source this configures; transforms are Vector Remap Language stages between them. namespace is where the ServiceAccount lives and is required. dataDir puts the read checkpoints on the node so they survive a reschedule, at the cost of writing to it.
Import: import 'github.com/metio/kurly/workloads/vector/agent.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vector" |
image | string | — | — |
namespace | string | — | "vector" |
sinks | object | — | {"console":{"encoding":{"codec":"json"},"inputs":["kubernetes_logs"],"type":"console"}} |
sources | object | — | — |
transforms | object | — | — |
dataDir | string | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
env | object | — | — |
labels | object | — | — |
annotations | object | — | — |
versity-s3-gateway
A Versity S3 Gateway (an S3 API in front of an ordinary filesystem, so tools that speak S3 can read and write a PersistentVolume). A plain composable http workload on the project's own image: the gateway is stateless and every object is a file on the volume it serves. THE VOLUME IS THE BUCKET NAMESPACE — a top-level directory is a bucket and the objects in it are files, which is what makes data written through S3 stay readable as ordinary files, and is also the constraint: object keys must be legal paths and the filesystem's limits become the gateway's. secretName carries ROOT_ACCESS_KEY and ROOT_SECRET_KEY, the account that can do anything through the gateway. S3 clients using virtual-host addressing need a wildcard hostname; path-style addressing is what works behind a single name. One replica over a ReadWriteOnce volume, recreated; a ReadWriteMany volume allows several, and whether concurrent writers are safe is then a question about that filesystem. Serves on :7070.
gateway http
The Versity S3 Gateway over the posix backend, serving /data on the volume. accessModes is the claim's access mode, region the region reported to clients. secretName holds ROOT_ACCESS_KEY and ROOT_SECRET_KEY through envFrom; everything else the gateway reads is VGW_*, through env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/versity-s3-gateway/gateway.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "versity-s3-gateway" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
secretName | string | — | "versity-s3-gateway" |
region | string | — | "us-east-1" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
vikunja
A Vikunja server (a self-hosted to-do and project-management app) on the official all-in-one image. A plain composable http workload that keeps its data in SQLite and file attachments on a PersistentVolume by default — no external database. kurly authors no Secret; VIKUNJA_SERVICE_JWTSECRET comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3456.
server http
The Vikunja server. Database at /db, attachments at /files, both on the volume. publicUrl is the public URL; secretName holds VIKUNJA_SERVICE_JWTSECRET (envFrom, keep it stable). Point VIKUNJA_DATABASE_TYPE at external Postgres/MySQL via env to scale past SQLite. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vikunja/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vikunja" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
publicUrl | string | — | — |
secretName | string | — | "vikunja" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
vince
A Vince server (privacy-friendly web analytics: a script on a site posts events here and a dashboard reports them, without cookies and without cross-site tracking). A plain composable http workload — one static Go binary with an embedded Pebble database on a PersistentVolume, needing no database, cache or object storage beside it. url is the address browsers reach the instance at and the tracking snippet the dashboard hands out is built from it, so a wrong value produces a snippet posting events nowhere; there is no default that is right anywhere. Automatic TLS is deliberately left off, since a certificate belongs to the exposure composed in front of it. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The Vince server. The Pebble database holding every recorded event lives at /data on the volume. url must name the address browsers reach this instance at, because the tracking snippet is built from it. domains creates those sites on startup, so a fresh instance is usable without clicking through the dashboard first. secretName holds VINCE_ADMIN_NAME and VINCE_ADMIN_PASSWORD, from which the administrator account is created on startup — a self-hosted Vince has no sign-up, so without them nobody can log in. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vince/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vince" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
url | string | — | "http://localhost:8080" |
domains | array | — | — |
secretName | string | — | "vince" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
vinyl-cache
A caching HTTP reverse proxy on the official image: it fronts an application, keeps the responses it is allowed to keep in memory, and answers the next request for them itself. A plain composable http workload. Without backendUrl it is not a proxy — the shipped configuration answers every request from a static page baked into the image, so a deployment that forgets it comes up healthy and caches nothing anybody asked for; the URL must carry a scheme, since the configuration refuses to load without one. vcl is the Varnish Configuration Language, mounted verbatim at /etc/varnish-vcl/default.vcl and named through VARNISH_VCL_FILE, beside the image's own /etc/varnish rather than over it so the shipped includes stay on the default path. The cache is memory: cacheSize and the container memory limit have to agree, or the cache OOM-kills the pod under the load it was added for. The listener is moved off :80, which an unprivileged uid cannot rely on, and the working directory is a scratch. Stateless, cold on start: scale freely, each replica holding its own cache. Serves on :8080 behind a Service on :80.
server http
The caching reverse proxy. backendUrl is the origin it fronts and must carry a scheme; cacheSize is the in-memory store handed to the server, which the container memory limit has to accommodate; vcl replaces the shipped configuration verbatim; port is the port it listens on and the container exposes. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vinyl-cache/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vinyl-cache" |
image | string | — | — |
replicas | int | — | 2 |
port | int | — | 8080 |
backendUrl | string | — | — |
cacheSize | string | — | "128M" |
vcl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
viseron
A Viseron server (records network cameras and runs object detection over the footage, keeping the events worth keeping). A plain composable http workload on the official image, with its configuration, recorded segments, snapshots, thumbnails and event clips all on one PersistentVolume — recorded video is what fills that volume, continuously, so size it for the retention the cameras ask for rather than for the application. Cameras are configured entirely from /config/config.yaml, which is written empty on the first start, so nothing is recorded until it is edited. The s6-overlay init runs as root and drops to the PUID/PGID account, so this runs as root with the transition capabilities granted back by name and a writable root filesystem — s6 supervises nginx, PostgreSQL, go2rtc and Viseron itself from the image tree (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8888.
server http
The Viseron server. Configuration at /config on the volume, footage at /segments and clips, snapshots and thumbnails beside it on the same volume. puid/pgid own those files; timezone sets TZ. Probes are connection probes, because the single-page app moves its paths between releases. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/viseron/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "viseron" |
image | string | — | — |
storageSize | quantity | — | "100Gi" |
storageClass | string | — | — |
puid | int | — | 911 |
pgid | int | — | 911 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"4Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
volsync
Copies a PersistentVolume's contents off the cluster and back, as VolSync ReplicationSource and ReplicationDestination custom resources — restic underneath, one resource per claim, so a volume states its own backup policy next to itself. Requires the VolSync operator, and a CSI driver with a VolumeSnapshotClass for the default Snapshot copy method (Direct copies the live volume, which is only safe for a volume nobody writes during the window). Authors the CRs directly like cnpg-cluster; composed by parameter, not by + feature. kurly authors no Secret: the restic repository URL, its password and the object-storage credentials come from a provided one.
backup volsync
A ReplicationSource: what to copy, where to, how often, and how much to keep. Restic fields kurly does not model pass through verbatim via `restic`, which is merged last.
Import: import 'github.com/metio/kurly/workloads/volsync/backup.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "backup" |
sourcePVC | string | — | "data" |
repository | string | — | "restic-repository" |
schedule | string | — | "0 2 * * *" |
manual | string | — | — |
copyMethod | string | — | "Snapshot" |
retain | object | — | {"daily":7,"monthly":6,"weekly":4} |
pruneIntervalDays | int | — | 14 |
cacheCapacity | quantity | — | — |
cacheStorageClassName | string | — | — |
storageClassName | string | — | — |
volumeSnapshotClassName | string | — | — |
accessModes | array | — | — |
moverSecurityContext | object | — | — |
restic | object | — | — |
labels | object | — | — |
annotations | object | — | — |
restore volsync
A ReplicationDestination: the half that proves the other half. Restores the latest snapshot once, into a volume VolSync provisions (capacity) or one that already exists (destinationPVC); restoreAsOf and previous walk back past a corruption that was itself backed up.
Import: import 'github.com/metio/kurly/workloads/volsync/restore.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "restore" |
repository | string | — | "restic-repository" |
capacity | quantity | — | "10Gi" |
manual | string | — | "restore-once" |
schedule | string | — | — |
copyMethod | string | — | "Snapshot" |
destinationPVC | string | — | — |
accessModes | array | — | ["ReadWriteOnce"] |
storageClassName | string | — | — |
volumeSnapshotClassName | string | — | — |
cacheCapacity | quantity | — | — |
restoreAsOf | string | — | — |
previous | int | — | — |
cleanupCachePVC | bool | — | true |
cleanupTempPVC | bool | — | true |
moverSecurityContext | object | — | — |
restic | object | — | — |
labels | object | — | — |
annotations | object | — | — |
vvveb-cms
A Vvveb CMS server (a content management system whose pages are edited with a drag-and-drop builder) on the project's own nginx + php-fpm image. The image ships only the runtime: the CMS itself is downloaded as a zip from DOWNLOAD_URL and unpacked into /var/www/html the first time that directory is empty, so the first boot needs egress to the download host and takes minutes rather than seconds (a ten-minute startup budget covers it), and the volume is mounted at the application root because code, configuration, themes, uploads and the page cache all live there together; downloadUrl points it at a mirror where there is no egress. It needs a MySQL/MariaDB or PostgreSQL, but the engine and its credentials are entered in the web installer on first visit and written to the volume rather than read from env, so kurly passes no database coordinates and authors no Secret. supervisord runs nginx and php-fpm and drops both to www-data, which it can only do from root, so non-root and read-only-rootfs are relaxed while the probes stay connection probes, since every path redirects to the installer until it has been walked through. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Vvveb CMS server. The whole application tree lives at /var/www/html on the volume, fetched on first boot from downloadUrl (the image default is the project download endpoint). The database is chosen and credentialed in the web installer, not here. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/vvveb-cms/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "vvveb-cms" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
downloadUrl | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wallabag
A wallabag server (a self-hosted read-it-later app that saves clean, readable copies of web pages) on the official image, backed by an external PostgreSQL, with saved images on a PersistentVolume. Pairs with a cnpg-cluster named wallabag-db. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; the DB password and app secret come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The wallabag server. dbHost/dbName/dbUser default to a cnpg-cluster named wallabag-db (SYMFONY__ENV__ prefix). domain is the public URL. secretName holds SYMFONY__ENV__DATABASE_PASSWORD and SYMFONY__ENV__SECRET (envFrom). Saved images on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wallabag/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wallabag" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
dbHost | string | — | "wallabag-db-rw" |
dbName | string | — | "wallabag" |
dbUser | string | — | "wallabag" |
domain | string | — | — |
secretName | string | — | "wallabag" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wallos
A Wallos server — a self-hosted subscription tracker and budgeting tool. On the official image; with the default SQLite backend its data lives on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The A Wallos server. Data on the config volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wallos/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wallos" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
warpgate
A Warpgate server (a smart SSH, HTTPS and database bastion: users connect with an ordinary client, authenticate once, and it proxies them to the targets they are allowed while recording the session). A plain composable http workload whose configuration, SQLite database, SSH host keys and recordings live on one PersistentVolume. An init container runs the unattended setup exactly once, because rerunning it would mint new host keys and break every client that already trusts the old ones. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the admin UI and HTTPS proxy on :8888, SSH on :2222.
server http
The Warpgate server. Configuration, database, host keys and recordings live at /data on the volume. secretName holds WARPGATE_ADMIN_PASSWORD, read ONCE by the setup step — changing it later does not change the password, which by then lives hashed in Warpgate's own database. recordSessions writes the contents of every proxied session to the volume and is off by default, being a storage and a privacy decision rather than kurly's. Probes run the image's own healthcheck subcommand. Compose an exposure onto the HTTP port; the SSH port is not HTTP and needs its own route.
Import: import 'github.com/metio/kurly/workloads/warpgate/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "warpgate" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
httpPort | int | — | 8888 |
sshPort | int | — | 2222 |
secretName | string | — | "warpgate" |
recordSessions | bool | — | false |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
warracker
A Warracker server (tracks product warranties: what you bought, when the cover expires, and the receipts and manuals that go with it, with reminders before each lapses). A composable http workload backed by an external PostgreSQL, with uploaded documents on a PersistentVolume. Every credential it needs has a PUBLISHED default in the project's own compose file, the session-signing key included, so supplying the Secret is the difference between having accounts and not. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Warracker server. Uploaded receipts and manuals live at /data/uploads on the volume; everything else is in PostgreSQL. secretName holds DB_PASSWORD, DB_ADMIN_PASSWORD and SECRET_KEY — all three ship with published defaults upstream, and SECRET_KEY signs sessions. supervisord runs nginx and gunicorn together and drops privileges from root, so this workload is deliberately less hardened. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/warracker/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "warracker" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "warracker-db-rw" |
dbPort | int | — | 5432 |
database | string | — | "warracker" |
dbUser | string | — | "warracker" |
secretName | string | — | "warracker" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
watcharr
A Watcharr server (a watchlist for films and television: what you have seen, what you are part-way through and what you mean to watch, with ratings and progress). A plain composable http workload keeping its SQLite database on a PersistentVolume. It looks metadata up from TMDB at runtime, so the pod needs internet egress even though nothing else about it does — a NetworkPolicy that forgets this leaves the catalogue simply empty. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3080.
server http
The Watcharr server. SQLite at /data on the volume. secretName holds JWT_SECRET, which signs the tokens users hold — Watcharr generates one into its data directory when unset, so it survives a restart here but not a move to a fresh volume; supplying it makes sessions outlive the volume. Needs egress to TMDB for metadata. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/watcharr/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "watcharr" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
secretName | string | — | "watcharr" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wavelog
A Wavelog server (a web logbook for radio amateurs) on the official image, backed by an external MySQL/MariaDB (the mysql-cluster workload provides one), with the installer-written configuration, uploads and user data on PersistentVolumes. It takes no database coordinates from the environment: the web installer at /install asks once and writes them into application/config/docker, so that directory is a volume of its own and no Secret is read. The Apache + PHP entrypoint runs as root and binds :80, relaxing non-root and read-only-rootfs. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :80.
server http
The Wavelog server. Needs an external MySQL/MariaDB, configured once through the web installer at /install, which writes into the config volume. Uploads at /var/www/html/uploads, user data at /var/www/html/userdata. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wavelog/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wavelog" |
image | string | — | — |
configSize | quantity | — | "1Gi" |
uploadSize | quantity | — | "5Gi" |
userdataSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
webtrees
A webtrees server (a self-hosted, collaborative online genealogy application) on the community image, backed by an external MySQL/MariaDB, with its data on a PersistentVolume. kurly authors no Secret; the DB_* credentials come from a provided Secret via envFrom. Pairs with a mysql-cluster named webtrees-db. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The webtrees server. baseUrl is the public URL; secretName holds the DB_* credentials (envFrom). Data at /var/www/webtrees/data. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/webtrees/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "webtrees" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
baseUrl | string | — | — |
secretName | string | — | "webtrees" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wekan
A Wekan server (a self-hosted, open-source kanban board à la Trello) on the official image, backed by an external MongoDB. Pairs with a mongodb-cluster named wekan-db. kurly authors no Secret; MONGO_URL comes from a provided Secret via envFrom. Stateless (attachments live in MongoDB GridFS): a plain rolling Deployment. Serves on :8080.
server http
The Wekan server. rootUrl is the public URL. secretName holds MONGO_URL (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wekan/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wekan" |
image | string | — | — |
replicas | int | — | 2 |
rootUrl | string | — | — |
secretName | string | — | "wekan" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wetty
A WeTTY server (a terminal in the browser: it opens an SSH connection to a host you name and renders it as a web page). A plain composable http workload and a stateless one — it stores nothing and claims no volume, so replicas is an ordinary knob. It performs NO AUTHENTICATION of its own: anyone who reaches the page gets the SSH login prompt of sshHost, so it belongs behind an authenticating proxy or on an internal route. Serves on :3000.
server http
The WeTTY server. sshHost is required — unset it defaults to localhost, which inside a container is the WeTTY pod itself, a terminal connecting to a machine with no sshd. sshUser left unset makes the visitor type their own name, which is usually right for a shared bastion terminal. base is the path it is served under. Stateless, so nothing to back up. Compose an exposure onto the HTTP port, and read the summary about authentication first.
Import: import 'github.com/metio/kurly/workloads/wetty/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wetty" |
image | string | — | — |
sshHost | string | yes | — |
sshPort | int | — | 22 |
sshUser | string | — | — |
base | string | — | "/wetty/" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wger
A wger server (a self-hosted workout, nutrition, and body-weight manager) on the official all-in-one image, backed by an external PostgreSQL and Redis, with uploaded media on a PersistentVolume. Pairs with a cnpg-cluster named wger-db and a valkey named wger-cache. The image runs nginx + uWSGI + Celery and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; DJANGO_DB_PASSWORD and SECRET_KEY come from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The wger server. dbHost/dbName/dbUser default to a cnpg-cluster named wger-db; redisHost to a valkey named wger-cache. siteUrl is the public URL. secretName holds DJANGO_DB_PASSWORD and SECRET_KEY (envFrom). Media at /home/wger/media on the volume. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wger/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wger" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
dbHost | string | — | "wger-db-rw" |
dbName | string | — | "wger" |
dbUser | string | — | "wger" |
redisHost | string | — | "wger-cache" |
siteUrl | string | — | — |
secretName | string | — | "wger" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
whoogle
A Whoogle Search server (a self-hosted, privacy-respecting metasearch proxy for Google results: no ads, no tracking, no JavaScript required) on the official image. Stateless: a plain rolling Deployment. Configure through WHOOGLE_CONFIG_* env. Serves on :5000.
server http
The Whoogle server. Stateless; configure via WHOOGLE_CONFIG_* env. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/whoogle/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "whoogle" |
image | string | — | — |
replicas | int | — | 2 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wiki-go
A Wiki-Go server (a flat-file wiki written in Go: every page is a Markdown file, there is no database at all). A plain composable http workload whose pages, uploads and generated config.yaml share one PersistentVolume. Ships a default administrator account that must be changed before the wiki is exposed. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves the wiki on :8080.
server http
The Wiki-Go server. Keeps pages, uploads and its own config.yaml at /wiki/data on the volume, so it needs nothing external. Runs as uid 1000 with a matching fsGroup. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wiki-go/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wiki-go" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wikidocs
A Wiki|Docs server (a flat-file Markdown wiki engine: pages, revisions, attachments and the configuration are all files under datasets/, and there is no database at all). A plain composable http workload with datasets/ on a PersistentVolume, seeded from the image by an init container because an empty volume would hide the documents/ and trash/ directories the image ships. It is UNCONFIGURED until somebody finishes setup.php: with no datasets/config.inc.php the instance answers with its wizard, and whoever reaches it first names the wiki and picks the edit code that authorises every later edit. The entrypoint renumbers the apache account, chowns the document root and starts httpd on :80 from root, so this workload is deliberately less hardened. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The Wiki|Docs server. Documents, revisions, trash, attachments and config.inc.php live at /var/www/localhost/htdocs/datasets on the volume, seeded from the image on first boot. Run the setup wizard once before exposing it, because it picks the edit code every later edit is authorised with. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wikidocs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wikidocs" |
image | string | — | — |
storageSize | quantity | — | "5Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wikijs
A Wiki.js server (a modern, open-source wiki) on the official image, backed by an external PostgreSQL. Its content and configuration live in the database, so it is stateless and can run several replicas. Pairs with a cnpg-cluster named wikijs-db. kurly authors no Secret; DB_PASS comes from a provided Secret via envFrom. Serves on :3000.
server http
The Wiki.js server. dbHost/dbName/dbUser default to a cnpg-cluster named wikijs-db. secretName holds DB_PASS (envFrom). Scales horizontally via replicas. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wikijs/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wikijs" |
image | string | — | — |
dbHost | string | — | "wikijs-db-rw" |
dbName | string | — | "wikijs" |
dbUser | string | — | "wikijs" |
secretName | string | — | "wikijs" |
replicas | int | — | 1 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wildduck
A WildDuck server (an IMAP and POP3 mail server) on the official image, backed by an external MongoDB and Redis. Pairs with a mongodb-cluster named wildduck-db and a Redis-compatible cache. Configuration comes from the TOML files shipped in the image, each value overridable by an APPCONF_<section>_<key> environment variable; the stage binds the API to 0.0.0.0, because the shipped default binds 127.0.0.1 and neither a probe nor the Service reaches that. kurly authors no Secret: the two connection strings and the API access token come from a provided Secret via envFrom. Stateless (messages and attachments live in MongoDB GridFS, session state in Redis): a plain rolling Deployment. Serves the REST API on :8080; IMAPS (:9993) and POP3S (:9995) are not HTTP and need TCP routing.
server http
The WildDuck server. secretName holds APPCONF_dbs_mongo, APPCONF_dbs_redis and APPCONF_api_accessToken (envFrom) — both connection strings carry a password. hostname is the default domain part for usernames that are not email addresses. Probed by connection, because every API path answers 401 without the access token. Compose an exposure onto the HTTP port, and route the IMAPS and POP3S ports as TCP.
Import: import 'github.com/metio/kurly/workloads/wildduck/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wildduck" |
image | string | — | — |
replicas | int | — | 1 |
secretName | string | — | "wildduck" |
hostname | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
wordpress
A WordPress server (the popular CMS and blogging platform) on the official image, backed by an external MySQL/MariaDB, with content (themes, plugins, uploads) on a PersistentVolume. kurly ships no MySQL recipe — bring your own. The Apache + PHP image starts as root and binds :80, relaxing non-root and read-only-rootfs while keeping dropped capabilities. kurly authors no Secret; WORDPRESS_DB_PASSWORD comes from a provided Secret via envFrom. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The WordPress server. dbHost/dbName/dbUser point at a MySQL/MariaDB you provide. secretName holds WORDPRESS_DB_PASSWORD (envFrom). Content at /var/www/html. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/wordpress/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "wordpress" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
dbHost | string | — | "wordpress-db" |
dbName | string | — | "wordpress" |
dbUser | string | — | "wordpress" |
secretName | string | — | "wordpress" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
xbackbone
An XBackBone server (a lightweight file and screenshot host with ShareX support) on the maintained LinuxServer image. Uploads, the SQLite database and the generated configuration all live under /config on a PersistentVolume, so it needs no external database. The s6-overlay init runs as root and drops to the PUID/PGID user, so this runs as root with a writable root filesystem (kurly keeps the rest of the hardening). Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :80.
server http
The XBackBone server. puid/pgid own the mounted files; timezone sets TZ. Uploads, database and config at /config. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/xbackbone/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "xbackbone" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
puid | int | — | 1000 |
pgid | int | — | 1000 |
timezone | string | — | "UTC" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
xwiki
An XWiki server (a powerful, self-hosted enterprise wiki and application-development platform: structured pages, macros, apps, fine-grained rights) on the official PostgreSQL/Tomcat image, backed by an external PostgreSQL, with its permanent directory on a PersistentVolume. kurly authors no Secret; the DB_* connection comes from a provided Secret via envFrom. Pairs with a cnpg-cluster named xwiki-db. The bundled Tomcat runs as root. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8080.
server http
The XWiki server. secretName holds the DB_* connection (envFrom). Data at /usr/local/xwiki. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/xwiki/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "xwiki" |
image | string | — | — |
storageSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "xwiki" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
yacy
A YaCy search server (a peer-to-peer web search engine that crawls and indexes on its own and, by default, joins the public network of peers). A plain composable http workload with the crawler queues, the Solr index and the peer identity in one DATA directory on a PersistentVolume. The admin account ships with an EMPTY password, protected only by a restriction to local access that a pod does not provide — set it before composing an exposure. The image's yacy account is a named system user kubelet cannot check against runAsNonRoot, so uid 100 is pinned numerically. A JVM on a fresh volume starts slowly, hence a startup probe rather than a lenient liveness probe. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :8090.
server http
The YaCy search server. All state — index, crawl queues, peer identity, the settings written from the web interface — lives at /opt/yacy_search_server/DATA on the volume, which only grows. The administration interface is unauthenticated until you set a password from /ConfigAccounts_p.html, and the same page decides whether the peer joins the public network or stays private. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/yacy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "yacy" |
image | string | — | — |
storageSize | quantity | — | "20Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}} |
labels | object | — | — |
annotations | object | — | — |
yopass
A Yopass server (share a secret through a one-time, self-destructing encrypted link; the browser encrypts and decrypts, so the server only ever holds ciphertext) on the official image, backed by an external Redis. Pairs with a valkey named yopass-cache. The binary reads no environment at all — every setting is a flag — so the stage renders the backend selection into args. kurly authors no Secret and the workload needs none. Stateless: a plain rolling Deployment. Serves on :1337.
server http
The Yopass server. redisHost defaults to a valkey named yopass-cache; maxLength caps the encrypted payload a client may store. Nothing is written to disk — secrets live in the cache until they are read once or expire — so the pod is stateless. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/yopass/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "yopass" |
image | string | — | — |
replicas | int | — | 2 |
redisHost | string | — | "yopass-cache" |
redisPort | int | — | 6379 |
redisDatabase | int | — | 0 |
maxLength | int | — | 10000 |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
your-spotify
A Your Spotify server (a self-hosted dashboard of your own Spotify listening history and statistics) on the official server image, backed by an external MongoDB. kurly authors no Secret; MONGO_ENDPOINT, SPOTIFY_PUBLIC and SPOTIFY_SECRET come from a provided Secret via envFrom. apiEndpoint and clientEndpoint are public URLs a browser resolves — the Spotify OAuth redirect is built from apiEndpoint and must match the redirect URI registered on the Spotify application. The web client is a separate image and is not carried here. Stateless (every scrobble lands in MongoDB): a plain rolling Deployment. Serves the API on :8080.
server http
The Your Spotify server. apiEndpoint is this server's public URL and clientEndpoint the web client's; secretName holds MONGO_ENDPOINT, SPOTIFY_PUBLIC and SPOTIFY_SECRET (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/your-spotify/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "your-spotify" |
image | string | — | — |
replicas | int | — | 2 |
apiEndpoint | string | — | — |
clientEndpoint | string | — | — |
secretName | string | — | "your-spotify" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
yourls
A YOURLS server (Your Own URL Shortener: a small, self-hosted PHP app for short links with stats and plugins) on the official image, backed by an external MySQL/MariaDB. kurly authors no Secret; the YOURLS_DB_* and admin YOURLS_USER/YOURLS_PASS come from a provided Secret via envFrom. Pairs with a mysql-cluster named yourls-db. Stateless (links live in MySQL): a plain rolling Deployment. Serves on :80.
server http
The YOURLS server. site sets YOURLS_SITE (the public URL); secretName holds YOURLS_DB_* and the admin credentials (envFrom). Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/yourls/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "yourls" |
image | string | — | — |
replicas | int | — | 2 |
site | string | — | — |
secretName | string | — | "yourls" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"256Mi"},"requests":{"cpu":"50m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
youtube-dl-nas
A youtube-dl-nas server (a password-protected web queue that hands URLs to yt-dlp and keeps the resulting video, audio and subtitle files, with a download history). A plain composable http workload keeping the downloads and its queue/history state on two PersistentVolumes. The entrypoint runs as root to substitute the credentials into Auth.json inside its own install tree and chown the volumes before dropping to PUID:PGID with gosu, so it runs as root with a writable root filesystem, privilege escalation allowed and capabilities kept — set PUID/PGID through env to have the files owned by an unprivileged account. Both auto-updaters are off by default: yt-dlp's would pip-install into the image on every start and write a log under /var/log. It downloads from the public internet, so the pod needs egress even though nothing else about the manifest says so. Single writer over ReadWriteOnce volumes: one replica, recreated. Serves on :8080.
server http
The youtube-dl-nas server. Downloads at /downfolder, queue, history and sessions at /usr/src/app/metadata, each on its own volume. secretName holds MY_ID and MY_PW, the single account that may reach the queue — there is no other authentication and the server refuses to start without them. env is merged over the defaults APP_PORT=8080, YTDLP_AUTO_UPDATE=false and NLPTUTTI_AUTO_UPDATE=false, so a key you set wins. Compose an exposure onto the HTTP port, and allow egress or every job fails.
Import: import 'github.com/metio/kurly/workloads/youtube-dl-nas/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "youtube-dl-nas" |
image | string | — | — |
downloadSize | quantity | — | "50Gi" |
stateSize | quantity | — | "1Gi" |
storageClass | string | — | — |
secretName | string | — | "youtube-dl-nas" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
yt-dlp-web-ui
A yt-dlp Web UI server (a browser front end for yt-dlp: paste a URL, pick a format, watch progress, fetch the file). A plain composable http workload with downloads on a PersistentVolume. It downloads whatever it is asked to, from wherever the pod can reach, and has NO authentication unless one is configured — so an exposed instance is an open downloader inside your network, and a NetworkPolicy composed onto it is the other half of that decision. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :3033.
server http
The yt-dlp Web UI server. Downloads land at /downloads on the volume, which is where the image's own entrypoint already points, so no argument is overridden. Size storageSize for what will be kept. Put an authenticating proxy in front of it and consider what egress the pod has. Compose an exposure onto the HTTP port.
Import: import 'github.com/metio/kurly/workloads/yt-dlp-web-ui/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "yt-dlp-web-ui" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
zenko-cloudserver
A Zenko CloudServer node (an S3-compatible object storage server that keeps objects on local disk, or forwards them to a cloud bucket) on the official image, as a plain composable http workload serving S3 on :8000. THE ENDPOINT IS PART OF THE CONTRACT: CloudServer answers only for host names it was configured with, and a request whose Host header is not a known REST endpoint is refused whatever the credentials say — so `endpoint` must be the name clients really address it by, the Service name in-cluster or the public name behind an exposure. The image's entrypoint accepts exactly one, so several endpoints mean mounting a config.json over the image's. It rewrites that config.json IN PLACE with jq on every start, in its own install tree beside node_modules and as root, and fails the container if it cannot — which is why this workload runs as root on a writable root filesystem and neither relaxation can be composed away. kurly authors no Secret; SCALITY_ACCESS_KEY_ID and SCALITY_SECRET_ACCESS_KEY come from a provided Secret via envFrom and are the S3 account a client signs with, not a login. Objects and their index are SEPARATE ReadWriteOnce claims (/usr/src/app/localData and /usr/src/app/localMetadata), because the image lays them out that way and an index deserves faster disk than the blocks. Remote management is disabled, so the pod does not register with Scality's hosted service. Every S3 path validates Host and answers 403 unsigned while /_/healthcheck is served only to loopback, so probes are by connection. Single writer with the metadata daemon inside the pod: one replica, recreated.
server http
A CloudServer node. endpoint is the ONE host name it answers for and must match what clients address; region is what a client has to sign for. backend='file' keeps objects and index on the volumes, 'mem' throws both away on restart. dataSize and metadataSize size the two claims independently. secretName holds SCALITY_ACCESS_KEY_ID and SCALITY_SECRET_ACCESS_KEY (envFrom). Compose an exposure onto the S3 port when it is reached from outside the cluster, and set endpoint to that public name when you do.
Import: import 'github.com/metio/kurly/workloads/zenko-cloudserver/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "zenko-cloudserver" |
image | string | — | — |
endpoint | string | — | "zenko-cloudserver" |
region | string | — | "us-east-1" |
backend | string | — | "file" |
logLevel | string | — | "info" |
dataSize | quantity | — | "50Gi" |
metadataSize | quantity | — | "10Gi" |
storageClass | string | — | — |
secretName | string | — | "zenko-cloudserver" |
env | object | — | — |
resources | object | — | {"limits":{"memory":"2Gi"},"requests":{"cpu":"200m","memory":"512Mi"}} |
labels | object | — | — |
annotations | object | — | — |
zigbee2mqtt
A Zigbee2MQTT bridge (it puts a Zigbee network onto MQTT, so the devices are usable by anything that speaks MQTT). A plain composable http workload on the project's own image: the device database and settings are files under /app/data on a PersistentVolume and everything else is published to the broker. The adapter is the whole question in a cluster — a pod cannot be handed a USB stick plugged into some node, so serialPort defaults to nothing and the arrangement that schedules anywhere is a tcp:// network coordinator. An MQTT broker is required. Single writer over a ReadWriteOnce volume, and two bridges driving one radio would fight: one replica, recreated. Serves the frontend on :8080.
server http
The Zigbee2MQTT bridge. mqttServer is the broker it publishes to. serialPort is the Zigbee adapter — a tcp://host:port network coordinator in a cluster; a USB adapter needs the pod pinned to the node it is plugged into, which is a decision to make deliberately. adapter names the driver (ember, zstack, deconz, zigate, zboss). Settings given here come through ZIGBEE2MQTT_CONFIG_*; the device database stays in configuration.yaml on the volume where Zigbee2MQTT manages it. Compose an exposure onto the frontend port.
Import: import 'github.com/metio/kurly/workloads/zigbee2mqtt/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "zigbee2mqtt" |
image | string | — | — |
mqttServer | string | — | "mqtt://mosquitto:1883" |
serialPort | string | — | — |
adapter | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
labels | object | — | — |
annotations | object | — | — |
znc
A ZNC server (an IRC bouncer that stays connected and replays what you missed) on the official image. A plain composable http workload that keeps its configuration, module data, and buffers on a PersistentVolume. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves IRC and the web admin on :6697; needs a znc.conf (with credentials) on the volume before it starts.
server http
The ZNC server. Keeps everything at /znc-data on the volume. Provide a znc.conf at /znc-data/configs/znc.conf (generate with `znc --makeconf` or mount from a Secret — it holds passwords). Route the port as TCP.
Import: import 'github.com/metio/kurly/workloads/znc/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "znc" |
image | string | — | — |
storageSize | quantity | — | "1Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"128Mi"},"requests":{"cpu":"25m","memory":"64Mi"}} |
labels | object | — | — |
annotations | object | — | — |
zoraxy
A Zoraxy server (an HTTP reverse proxy and forwarding tool driven entirely from a web management interface rather than a configuration file). A plain composable http workload keeping its configuration database, issued certificates and site definitions on a PersistentVolume. The Service publishes the management interface on :8000 and nothing else: Zoraxy opens the listeners for the sites it proxies at runtime, from what an operator configures — :80 and :443 out of the box — so those ports are not known at render time and are added with extraPort. It runs as root with a writable root filesystem because the entrypoint rewrites /etc/ssl/certs before starting the server and fails the container when it cannot; capabilities stay dropped and privilege escalation stays off. The Docker integration and mDNS discovery the image enables by default are switched off, since a pod has neither a Docker socket nor anything answering multicast. Single writer over a ReadWriteOnce volume: one replica, recreated.
server http
The Zoraxy server. Configuration database, certificates and site definitions at /opt/zoraxy/config on the volume. env is merged over the defaults PORT=8000, DOCKER=false, MDNS=false and ZEROTIER=false, so a key you set wins. The management interface has no authentication until the first account is created through it — compose an exposure onto the HTTP port only behind authentication, and add the proxied listeners with extraPort.
Import: import 'github.com/metio/kurly/workloads/zoraxy/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "zoraxy" |
image | string | — | — |
storageSize | quantity | — | "2Gi" |
storageClass | string | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
zot-oci-registry
A zot server (a vendor-neutral, OCI-native container image registry that stores images in the OCI image layout on disk rather than in a layout only it can read) on the official image; its image store lives on a PersistentVolume. Configuration is one JSON document rendered into a ConfigMap at /etc/zot, and the config parameter merges over it verbatim — that is where authentication, access control, sync and the S3 storage driver go, none of which kurly models. Unauthenticated and plaintext until you configure otherwise, so keep it in-cluster or put TLS and auth in front; the probes ask /v2/ and must become connection probes once auth is on. The CVE scanner the image's own config enables is off here, because its vulnerability database costs hundreds of megabytes and far more memory than the workload requests. Single writer over a ReadWriteOnce volume: one replica, recreated. Serves on :5000.
server http
The zot registry server. Images at /var/lib/registry in the OCI layout. logLevel replaces the image default of debug; ui enables the web UI with the search and mgmt extensions it reads; cve enables the vulnerability scanner (raise the limits with it); config merges verbatim over the rendered zot configuration. Usually reached in-cluster. Compose an exposure onto the HTTP port only if pulled from outside (with TLS/auth in front).
Import: import 'github.com/metio/kurly/workloads/zot-oci-registry/server.libsonnet'
| Parameter | Type | Required | Default |
|---|---|---|---|
name | string | — | "zot-oci-registry" |
image | string | — | — |
storageSize | quantity | — | "50Gi" |
storageClass | string | — | — |
logLevel | string | — | "info" |
ui | bool | — | true |
cve | bool | — | false |
config | object | — | — |
env | object | — | — |
resources | object | — | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"128Mi"}} |
labels | object | — | — |
annotations | object | — | — |
Kinds
The base workload each recipe starts from.
kurly.cron(name, image, schedule)
A scheduled job: a CronJob that runs to completion on a cron schedule.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "nightly-report" |
image | string | yes | — | "ghcr.io/acme/report:1.2.3" |
schedule | string | yes | — | "0 2 * * *" |
kurly.daemon(name, image)
A per-node agent: a DaemonSet running one pod on every node.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "node-exporter" |
image | string | yes | — | "ghcr.io/acme/node-agent:1.2.3" |
kurly.http(name, image)
An HTTP workload: a Deployment (2 replicas) and a ClusterIP Service. Compose an exposure feature to accept traffic from outside the cluster.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "storefront" |
image | string | yes | — | "ghcr.io/acme/storefront:1.2.3" |
kurly.job(name, image)
A one-off task that runs to completion: a Job with restartPolicy OnFailure. No Service, no replicas.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "db-migrate" |
image | string | yes | — | "ghcr.io/acme/migrate:1.2.3" |
kurly.stateful(name, image)
A workload with stable identity and per-pod storage: a StatefulSet plus the headless Service that names it. The store feature renders as a per-pod volumeClaimTemplate.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "postgres" |
image | string | yes | — | "ghcr.io/acme/postgres:16" |
kurly.worker(name, image)
A background worker: a Deployment with no Service. Reaches out (queues, schedules) rather than serving traffic.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "indexer" |
image | string | yes | — | "ghcr.io/acme/indexer:1.2.3" |
Features
Composable + capabilities. Each contributes only to the merged
config, so features late-bind regardless of compose order.
container
kurly.annotations(annotations)
Extra annotations on metadata and the pod template.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
annotations | object | yes | — | {"prometheus.io/scrape":"true"} |
kurly.args(args)
Appends arguments to the image's own entrypoint — typically a subcommand selecting the workload.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
args | array | yes | — | ["backend","--config=/etc/tik/pipelines.edn"] |
kurly.command(command)
Overrides the image entrypoint entirely.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
command | array | yes | — | ["/bin/app"] |
kurly.disableServiceLinks()
Suppresses the legacy {SVCNAME}_SERVICE_* env vars Kubernetes injects — for apps that read their own NAME-prefixed env as configuration and collide with them.
kurly.dns(policy, config, hostAliases)
Pod name resolution: a resolver policy, extra nameservers/searches/options, and static /etc/hosts entries for names no DNS serves. dnsPolicy 'None' takes resolv.conf entirely from config, so it must bring its own nameservers — a render-time check enforces that.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
policy | string | — | — | "None" |
config | object | — | — | {"nameservers":["10.0.0.10"],"searches":["corp.local"]} |
hostAliases | array | — | — | [{"hostnames":["db.internal"],"ip":"10.0.0.5"}] |
kurly.env(env)
Environment variables as a name→value map, appended to the container.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
env | object | yes | — | {"LOG_LEVEL":"info"} |
kurly.envField(name, fieldPath)
An environment variable read from the pod's OWN metadata (its name, namespace, node or IP) rather than from a literal. Kubernetes expands $(VAR) in command and args from the container's DECLARED environment and leaves an undeclared one as the literal text, so a stage advertising itself as $(POD_NAME).<service> without this starts, runs, and tells its peers to reach a hostname that does not exist. Composable several times.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "POD_NAME" |
fieldPath | string | yes | — | "metadata.name" |
kurly.envFromConfigMap(configMapName, prefix)
Pull every key of an EXISTING ConfigMap into the environment (envFrom configMapRef). An optional prefix is prepended to each variable name.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
configMapName | string | yes | — | "app-config" |
prefix | string | — | — | — |
kurly.envFromSecret(secretName, prefix)
Pull every key of an EXISTING Secret into the environment (envFrom secretRef); kurly mints no Secret. An optional prefix is prepended to each variable name.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
secretName | string | yes | — | "mailu" |
prefix | string | — | — | — |
kurly.extraPort(name, port, servicePort, protocol, appProtocol, expose)
A port beside the primary http one, for a workload that listens on more than one (SMTP + web UI, TCP + UDP, a second admin port). Composable several times. name is the shared identity of the container port and its Service port; servicePort defaults to the container port; expose=false keeps the port on the pod but off the Service; protocol is TCP or UDP.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "smtp" |
port | int | yes | — | 1025 |
servicePort | int | — | — | 1025 |
protocol | string | — | "TCP" | — |
appProtocol | string | — | — | — |
expose | bool | — | true | — |
kurly.image(image)
Overrides the container image.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
image | string | yes | — | "ghcr.io/acme/app:1.2.3" |
kurly.imagePullSecrets(names)
Names of existing Secrets the kubelet uses to pull the image.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
names | array | yes | — | ["regcred"] |
kurly.initContainer(container)
An init container run to completion before the main one — the full container spec, passed through. Composes more than once.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
container | object | yes | — | {"command":["sh","-c","echo ready"],"image":"busybox:1","name":"setup"} |
kurly.ipFamilies(families, policy)
The IP families EVERY Service the workload renders asks for. A cluster is single-stack IPv4, single-stack IPv6, or dual-stack; pinning a family it lacks gets the Service rejected, so kurly names none and lets the cluster decide.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
families | array | yes | — | ["IPv4","IPv6"] |
policy | string | — | — | "RequireDualStack" |
kurly.labels(labels)
Extra labels on metadata and the pod template (never on immutable selectors).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
labels | object | yes | — | {"team":"payments"} |
kurly.lifecycle(preStop, postStart)
Container lifecycle handlers (postStart / preStop), passed through verbatim.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
preStop | object | — | — | {"exec":{"command":["sh","-c","valkey-cli failover"]}} |
postStart | object | — | — | — |
kurly.livenessProbe(probe)
An explicit liveness probe spec (exec/tcpSocket/httpGet), overriding the default http probe.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
probe | object | yes | — | {"tcpSocket":{"port":6379}} |
kurly.podAnnotations(podAnnotations)
Annotations on the pod template ONLY — for sidecar injection and scrape hints that are meaningless on the controller object.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
podAnnotations | object | yes | — | {"linkerd.io/inject":"enabled"} |
kurly.podLabels(podLabels)
Labels on the pod template ONLY (never the workload metadata or the immutable selector) — for network-policy selectors and log collection.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
podLabels | object | yes | — | {"tier":"database"} |
kurly.port(port)
The container port the workload listens on (also the Service target).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
port | int | yes | — | 8080 |
kurly.priorityClassName(priorityClassName)
The pod's scheduling priority class.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
priorityClassName | string | yes | — | "high-priority" |
kurly.probes(path)
HTTP readiness and liveness probes on the named http port.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
path | path | — | "/healthz" | "/tickets.edn" |
kurly.readinessProbe(probe)
An explicit readiness probe spec (exec/tcpSocket/httpGet), overriding the default http probe.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
probe | object | yes | — | {"exec":{"command":["sh","-c","valkey-cli ping"]}} |
kurly.recreate()
The single-writer strategy: tears the old pod down before starting the new one, so a ReadWriteOnce store never deadlocks a rollout.
kurly.replicas(replicas)
The desired number of pod replicas.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
replicas | int | yes | — | 3 |
kurly.reserve(cpu, memory)
An arbitrary resource reservation — the given CPU and memory as requests, with the memory limit equal to its request and no CPU limit, the same policy the named presets use. For a consumer whose sizes are chosen rather than picked from a menu, where no preset name fits. Replaces resources wholesale; a stage with more than one container has no defined answer.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
cpu | string | yes | — | "500m" |
memory | string | yes | — | "512Mi" |
kurly.resources(requests, limits)
Container resource requests and/or limits.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
requests | object | — | — | {"cpu":"100m","memory":"128Mi"} |
limits | object | — | — | {"memory":"256Mi"} |
kurly.rollingUpdate(maxSurge, maxUnavailable)
RollingUpdate tuning so a new pod surges alongside the old during an update — the overlap a replication hand-off needs.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
maxSurge | any | — | — | 1 |
maxUnavailable | any | — | — | — |
kurly.runtimeClassName(runtimeClassName)
The sandbox the pod runs under (gVisor, Kata). The class names belong to the cluster, so there is no default — a workload that cannot name one cannot run where sandboxing is mandatory.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
runtimeClassName | string | yes | — | "gvisor" |
kurly.serviceAccount(serviceAccountName)
Runs the pod under a named ServiceAccount (also gates token automount). Yours wins over the one a workload's RBAC would mint, and kurly then mints none — the account is yours to own and annotate.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
serviceAccountName | string | yes | — | "storefront" |
kurly.serviceAccountAnnotations(annotations)
Annotations for the ServiceAccount kurly mints for a workload that declares RBAC — where cloud workload identity is wired (eks.amazonaws.com/role-arn, iam.gke.io/gcp-service-account). Moot when you bring your own account with serviceAccount().
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
annotations | object | yes | — | {"eks.amazonaws.com/role-arn":"arn:aws:iam::123456789012:role/storefront"} |
kurly.serviceAnnotations(annotations)
Annotations on the Service. A cloud load balancer is configured through these and nothing else, and the keys differ per provider — without them a LoadBalancer cannot be shaped on any managed cloud.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
annotations | object | yes | — | {"service.beta.kubernetes.io/aws-load-balancer-type":"nlb"} |
kurly.servicePort(port)
The port the Service publishes — the contract with clients, which the container port need not match.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
port | int | yes | — | 443 |
kurly.serviceType(type)
The Service type. LoadBalancer and NodePort exist only where the cluster provides them, so there is no default.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
type | string | yes | — | "LoadBalancer" |
kurly.shutdown(drain, grace, preStop)
Sets both halves of a graceful stop together, because either alone is how a rolling update drops requests: `drain` seconds of doing nothing after the pod is doomed and before SIGTERM, so the endpoint removal propagates while the container is still serving, and `grace` seconds before SIGKILL. THE DRAIN IS SPENT OUT OF THE GRACE PERIOD rather than added to it — both clocks start when the pod is marked for deletion — so drain >= grace is refused rather than rendered. The drain uses Kubernetes' native sleep handler, not an exec of /bin/sh: on an image with no shell the exec form fails, and a failed preStop hook stops nothing and raises nothing anybody sees. `preStop` replaces the sleep with a verbatim handler for an application that must be told to quiesce.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
drain | int | — | — | 15 |
grace | int | — | — | 45 |
preStop | object | — | — | — |
kurly.sidecar(container)
An extra container beside the workload's own, sharing the pod. It inherits the composed security posture unless it carries its own securityContext — so a sidecar does not restate a uid, and does not silently keep one when the consumer changes it.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
container | object | yes | — | {"image":"ghcr.io/acme/agent:1.0","name":"agent"} |
kurly.startupProbe(probe)
A startup probe spec (exec/tcpSocket/httpGet with failureThreshold/periodSeconds), gating readiness and liveness until a slow-starting app first comes up.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
probe | object | yes | — | {"failureThreshold":30,"periodSeconds":5,"tcpSocket":{"port":8080}} |
kurly.strategy(strategy)
The Deployment update strategy.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
strategy | string | yes | — | "RollingUpdate" |
kurly.terminationGracePeriod(seconds)
How long the pod gets to shut down gracefully (a preStop hook's window).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
seconds | int | yes | — | 120 |
kurly.version(version)
The workload version, stamped as app.kubernetes.io/version on every object.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
version | string | yes | — | "1.2.3" |
networking
kurly.headlessService(port, publishNotReady)
A headless Service (clusterIP: None) selecting the pods, for DNS peer discovery — the discovery a replication hand-off needs.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
port | int | — | — | 6379 |
publishNotReady | bool | — | false | — |
kurly.networkPolicy(ingress, egress, policyTypes)
The low-level Kubernetes variant of the kurly.network axis: a networking.k8s.io/v1 NetworkPolicy firewalling the pods, with ingress/egress rules and policyTypes passed through verbatim. For an allow-list in the neutral vocabulary (and the Calico/Cilium variants) reach for kurly.network.*; both feed the same slot and join the networkPolicy exclusion group.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
ingress | array | — | — | — |
egress | array | — | — | — |
policyTypes | array | — | — | — |
kurly.publishNotReady(enabled)
Lists the workload's pods in its own Service before they are Ready. For a clustering member whose readiness DEPENDS on reaching its peers — it resolves them by DNS while forming the cluster — a Service that waits for readiness resolves to nothing and neither pod ever gets there.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
enabled | bool | — | true | — |
observability
kurly.alerts(namespace, severity, for_, unavailable, crashLooping, storageFull, memoryPressure, runbooks, labels, annotations, rules)
A PrometheusRule of alerting rules bound to THIS workload's own objects — its controller, its container, each of its claims. Rules that cannot fire are never emitted (no memory rule without a memory limit, no storage rule without a claim, no availability rule for a controller kind with no ready-versus-desired metric pair), because a rule that cannot fire reads as coverage and is not. Every rule names the gumshoe book that investigates or fixes it; `runbooks` supplies the base URL that turns it into a runbook_url. `namespace` scopes the expressions — without it, or without enforcedNamespaceLabel on the scraping Prometheus, a rule matches this name in every namespace. `for`, `storageFull` and `memoryPressure` are SLO decisions and belong to the caller.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
namespace | string | — | — | "apps" |
severity | string | — | "warning" | — |
for_ | string | — | "10m" | — |
unavailable | bool | — | true | — |
crashLooping | bool | — | true | — |
storageFull | int | — | 85 | — |
memoryPressure | int | — | 90 | — |
runbooks | string | — | — | "https://github.com/metio/gumshoe/blob/main/" |
labels | object | — | — | — |
annotations | object | — | — | — |
rules | array | — | — | — |
kurly.serviceMonitor(port, path, interval)
A Prometheus-Operator ServiceMonitor scraping the workload Service.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
port | string | — | "http" | — |
path | path | — | "/metrics" | — |
interval | string | — | — | — |
placement
kurly.affinity(affinity)
A pod/node affinity object, merged onto the pod template.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
affinity | object | yes | — | {"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"disktype","operator":"In","values":["ssd"]}]}]}}} |
kurly.nodeSelector(nodeSelector)
Restricts the pod to nodes carrying these labels.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
nodeSelector | object | yes | — | {"disktype":"ssd"} |
kurly.tolerations(tolerations)
Tolerations letting the pod schedule onto tainted nodes.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
tolerations | array | yes | — | [{"effect":"NoSchedule","key":"gpu","operator":"Exists"}] |
kurly.topologySpread(constraints)
Topology-spread constraints spreading the pods across a topology domain (keep version-bound labels in the selector so a rollout spreads the new set independently).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
constraints | array | yes | — | [{"labelSelector":{"matchLabels":{"app.kubernetes.io/name":"web"}},"maxSkew":1,"topologyKey":"kubernetes.io/hostname","whenUnsatisfiable":"DoNotSchedule"}] |
reliability
kurly.hpa(minReplicas, maxReplicas, targetCPU, targetMemory)
A HorizontalPodAutoscaler scaling the Deployment on CPU and/or memory utilization.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
minReplicas | int | yes | — | 2 |
maxReplicas | int | yes | — | 10 |
targetCPU | int | — | — | 80 |
targetMemory | int | — | — | — |
kurly.pdb(minAvailable, maxUnavailable)
A PodDisruptionBudget capping voluntary disruption. Set one of minAvailable / maxUnavailable.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
minAvailable | any | — | — | 1 |
maxUnavailable | any | — | — | — |
scheduling
kurly.concurrencyPolicy(concurrencyPolicy)
How to treat a job that is still running when the next is due (Allow/Forbid/Replace).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
concurrencyPolicy | string | yes | — | "Forbid" |
kurly.schedule(schedule)
The cron schedule expression.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
schedule | string | yes | — | "0 2 * * *" |
security
kurly.addCapabilities(capabilities)
Grants named Linux capabilities on top of the dropped-ALL default — how an app that needs one specific privilege (a DNS server binding :53 and managing routes) keeps the hardened posture for everything else.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
capabilities | array | yes | — | ["NET_BIND_SERVICE"] |
kurly.allowPrivilegeEscalation()
Allows the process to gain privileges its parent lacks — required to exec a binary carrying file capabilities (relaxes allowPrivilegeEscalation=false).
kurly.apiServerClient(rules, ports)
Declares a pod as a Kubernetes API client: adds the given Role rules AND best-effort NetworkPolicy egress to the apiserver, both as cross-cutting requirements that compose with (never clobber) a consumer own rbac()/networkPolicy().
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
rules | array | yes | — | [{"apiGroups":[""],"resources":["pods"],"verbs":["patch"]}] |
ports | array | — | [443,6443] | — |
kurly.clusterApiServerClient(rules, namespace, ports)
The cluster-wide counterpart of apiServerClient: rules a capability needs cluster-wide, plus apiserver egress, both as cross-cutting requirements that compose with a consumer own clusterRbac() rather than replacing it. It names the namespace for the same reason clusterRbac() does, and the namespaces every cluster grant contributes are checked against each other rather than silently overwritten.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
rules | array | yes | — | [{"apiGroups":[""],"resources":["nodes"],"verbs":["list","watch"]}] |
namespace | string | yes | — | "observability" |
ports | array | — | [443,6443] | — |
kurly.clusterRbac(rules, namespace)
Mints a ClusterRole with the given rules and the ClusterRoleBinding tying it to the workload's ServiceAccount — the grant a cluster add-on needs and a tenant workload must never ask for. The namespace is REQUIRED: a ClusterRoleBinding subject is not resolved against the object beside it, so a subject without one grants nothing while looking correct. Objects are named <workload>-<namespace>, because cluster-scoped names are global and two tenants would otherwise share one ClusterRole. Renders clusterScoped: true in the catalogue.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
rules | array | yes | — | [{"apiGroups":[""],"resources":["pods"],"verbs":["get","list","watch"]}] |
namespace | string | yes | — | "observability" |
kurly.hostIPC()
Shares the node's IPC namespace (System V IPC, POSIX shared memory). Drops the pod's own user namespace.
kurly.hostNetwork()
Puts the pod on the node's network stack: its container ports ARE node ports and no Service sits in the path. Also sets dnsPolicy to ClusterFirstWithHostNet unless one is given, because a host-network pod otherwise inherits the node's resolver and cannot resolve a single in-cluster Service while starting and running perfectly. Drops the pod's own user namespace, which Kubernetes forbids alongside a shared host namespace.
kurly.hostPID()
Shares the node's process namespace, so the container sees and can signal every process on the host — what a runtime security agent or a profiler needs. Drops the pod's own user namespace.
kurly.hostPath(mountPath, path, type, readOnly)
Mounts a directory or file from the NODE into the container. `type` defaults to a CHECKED one ('Directory'): left empty the kubelet creates whatever is missing, so a mistyped path yields an empty directory and an agent that reads nothing rather than a pod that fails. path defaults to mountPath.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
mountPath | string | yes | — | "/var/run/containerd/containerd.sock" |
path | string | — | — | — |
type | string | — | "Directory" | — |
readOnly | bool | — | true | — |
kurly.hostUsers()
Shares the host user namespace instead of an own one — needed on Windows nodes and where user namespaces are unavailable (relaxes hostUsers=false).
kurly.keepCapabilities()
Keeps the runtime default Linux capabilities instead of dropping ALL — the way a root process keeps CAP_NET_BIND_SERVICE to bind a privileged port.
kurly.privileged()
Removes the container isolation entirely: every capability, every device, and the ability to reconfigure the node. Only for software that cannot do its job otherwise — a CNI programming the dataplane, a security agent reading kernel events — and never for an application. The drop-ALL and no-escalation fields are suppressed alongside it, since a privileged container has neither, and the catalogue reports pss.level privileged.
kurly.rbac(rules)
Mints a ServiceAccount, a namespaced Role with the given rules, and the RoleBinding, and runs the pod under that ServiceAccount.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
rules | array | yes | — | [{"apiGroups":[""],"resources":["configmaps"],"verbs":["get","list","watch"]}] |
kurly.rootUser()
Drops runAsNonRoot so the container may run as the image USER; add runAs(0) to pin uid 0.
kurly.runAs(uid, gid, fsGroup)
Pins the run-as user/group (and matching fsGroup) for images that do not declare a non-root USER.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
uid | int | yes | — | 12345 |
gid | int | — | — | — |
fsGroup | int | — | — | — |
kurly.supplementalGroups(groups)
Extra group memberships for every container in the pod — how a pod reaches storage owned by a fixed GID it does not run as (a shared NFS/CephFS export). Distinct from fsGroup, which changes ownership of the pod's own volumes.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
groups | array | yes | — | [2000] |
kurly.writableRootFilesystem()
Makes the root filesystem writable (relaxes readOnlyRootFilesystem).
storage
kurly.config(files, mountPath, subPath)
Renders a ConfigMap from a filename→content map and mounts it read-only. By default the whole ConfigMap mounts as a directory at mountPath; with subPath=true each file is mounted individually at mountPath/<filename> (a subPath mount), so a single config file drops into a directory the image already populates without shadowing the rest.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
files | object | yes | — | {"app.conf":"key = value"} |
mountPath | path | — | "/etc/config" | — |
subPath | bool | — | false | — |
kurly.scratch(mountPath, sizeLimit)
A writable emptyDir — the escape valve a read-only root filesystem needs for /tmp and the like.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
mountPath | path | yes | — | "/tmp" |
sizeLimit | quantity | — | — | — |
kurly.secretMount(secretName, mountPath, readOnly, optional, defaultMode)
Mounts an EXISTING Secret (kurly never mints key material).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
secretName | string | yes | — | "tik-tls" |
mountPath | path | yes | — | "/etc/tls" |
readOnly | bool | — | true | — |
optional | bool | — | false | — |
defaultMode | int | — | — | — |
kurly.store(mountPath, size, accessModes, storageClass, selector, annotations)
The workload's own PersistentVolumeClaim, mounted at a path.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
mountPath | path | yes | — | "/var/lib/tik" |
size | quantity | yes | — | "1Gi" |
accessModes | array | — | ["ReadWriteOnce"] | — |
storageClass | string | — | — | — |
selector | object | — | — | — |
annotations | object | — | — | — |
Exposure
Routing recipes composed onto an http workload. All join the
exposure exclusion group — one exposure per workload.
kurly.expose.dns(hostname, ttl, target, annotations)
Adds external-dns annotations to the exposure resource (the HTTPRoute for a Gateway API recipe, the Ingress for the Ingress one) so external-dns creates the DNS record. A modifier composed after an exposure. external-dns already discovers the exposed hostname, so reach for this to override — a different/additional hostname, a ttl, or a target (the address the record points at). annotations passes through provider-specific keys.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
hostname | hostname | — | — | "alias.example.com" |
ttl | int | — | — | 300 |
target | string | — | — | "ingress.example.net." |
annotations | object | — | — | — |
kurly.expose.gateway(host, gateway, gatewayNamespace, sectionName)
Attaches an HTTPRoute to an existing shared Gateway (the usual platform-team setup). `host` takes one name or a list of them.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "storefront.example.com" |
gateway | string | yes | — | "shared" |
gatewayNamespace | string | — | — | — |
sectionName | string | — | — | — |
kurly.expose.guard(paths, service, serviceNamespace, port)
Sinks specific path prefixes on the HTTPRoute to a status-responder Service instead of the workload — the portable way to take a path off the public internet (answer 403/404) while the workload stays reachable in-cluster. A modifier composed after a Gateway API exposure, not an exposure itself; a cross-namespace responder needs a ReferenceGrant on that side. Compose it more than once to sink different paths to different responders.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
paths | array | yes | — | ["/admin","/stats"] |
service | string | yes | — | "not-found" |
serviceNamespace | string | — | — | "shared-http-services" |
port | int | — | 5678 | — |
kurly.expose.ingress(host, ingressClass, annotations, tls)
Routes the host to the workload through the Ingress API. `host` takes one name or a list of them; an Ingress rule carries one host, so several names become several rules over the same backend.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "storefront.example.com" |
ingressClass | string | — | — | "nginx" |
annotations | object | — | — | {"cert-manager.io/cluster-issuer":"letsencrypt"} |
tls | string | — | — | "storefront-tls" |
kurly.expose.listenerSet(host, listenerSet, listenerSetNamespace, sectionName)
Attaches an HTTPRoute to an existing ListenerSet (per-tenant listener ownership). `host` takes one name or a list of them.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "storefront.example.com" |
listenerSet | string | yes | — | "tenant-a" |
listenerSetNamespace | string | — | — | — |
sectionName | string | — | — | — |
kurly.expose.ownGateway(host, gatewayClass, annotations, tls)
Generates a dedicated Gateway plus the HTTPRoute — for clusters with no shared Gateway to attach to. `host` takes one name or a list of them; a listener publishes one hostname, so several names become several listeners, the first keeping the name that an existing sectionName already selects.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "storefront.example.com" |
gatewayClass | string | yes | — | "istio" |
annotations | object | — | — | {"service.beta.kubernetes.io/aws-load-balancer-type":"nlb"} |
tls | string | — | — | "storefront-tls" |
kurly.expose.ownListenerSet(host, gateway, gatewayNamespace, tls)
Generates a ListenerSet that adds the workload's own listener to a shared Gateway, plus the HTTPRoute. The Gateway must opt in via spec.allowedListeners. `host` takes one name or a list of them, one listener each.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "storefront.example.com" |
gateway | string | yes | — | "shared" |
gatewayNamespace | string | — | — | — |
tls | string | — | — | "storefront-tls" |
kurly.expose.probe(host, module, scheme, prober, proberPath, interval)
Attaches a prometheus-operator Probe to the workload, so Prometheus black-box-monitors its public URL through a blackbox-exporter — the outside-in check that complements the in-cluster ServiceMonitor scrape. A modifier composed onto a workload. host is explicit (target a specific health path, any exposure style); prober is the blackbox-exporter address; module selects its check (http_2xx expects a 2xx).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
host | hostname | yes | — | "web.example.com" |
module | string | — | "http_2xx" | — |
scheme | string | — | "https" | — |
prober | string | — | "blackbox-exporter:9115" | — |
proberPath | string | — | "/probe" | — |
interval | string | — | "30s" | — |
kurly.expose.referenceGrant(fromNamespaces)
Lets HTTPRoutes in other namespaces route to this workload's Service — the cross-namespace consent Gateway API requires, granted on the Service side and naming the allowed namespaces. Deploy a shared status-responder once, grant the tenant namespaces, and their guard rules can target its Service. A modifier, not an exposure.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
fromNamespaces | array | yes | — | ["team-a","team-b"] |
Network policy
Allow-list firewalls, one recipe per CNI. The rules are written once in a
neutral vocabulary and translated into each CNI's own kind; all join the
networkPolicy exclusion group — a workload firewalls one way.
denyAll is standalone: place it with kurly.list
rather than composing it onto a workload.
kurly.network.calico(allowFrom, allowTo, extraSpec)
An allow-list projectcalico.org/v3 NetworkPolicy named after the workload (the aggregated API, never the v1 storage CRD). allowFrom/allowTo take neutral peers, translated into Calico rules; extraSpec passes Calico-only spec fields (order, serviceAccountSelector, …) through verbatim.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
allowFrom | array | — | — | [{"pods":{"role":"web"}}] |
allowTo | array | — | — | — |
extraSpec | object | — | — | {"order":100} |
kurly.network.cilium(allowFrom, allowTo, extraSpec)
An allow-list cilium.io/v2 CiliumNetworkPolicy named after the workload. allowFrom/allowTo take neutral peers, translated into Cilium rules; extraSpec passes Cilium-only spec fields (an L7 rules block, toFQDNs, …) through verbatim.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
allowFrom | array | — | — | [{"pods":{"role":"web"},"ports":[8080]}] |
allowTo | array | — | — | — |
extraSpec | object | — | — | — |
kurly.network.denyAll(name, global, extraSpec)
The standalone default-DENY baseline an allow-list assumes — one per CNI (denyAll.kubernetes / denyAll.calico / denyAll.cilium), each selecting every pod and naming no allows. Not composed onto a workload: place it into a manifest set with kurly.list. global=true (Calico/Cilium) emits the cluster-wide kind; extraSpec passes exceptions (an allow for kube-dns) verbatim.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | — | "default-deny" | — |
global | bool | — | false | — |
extraSpec | object | — | — | — |
kurly.network.kubernetes(allowFrom, allowTo, ingress, egress, policyTypes)
An allow-list networking.k8s.io/v1 NetworkPolicy named after the workload, selecting its own pods — deny-by-default for that pod, opening only the listed peers. allowFrom/allowTo take neutral peers; ingress/egress take verbatim native rules for what the vocabulary does not cover; policyTypes forces the denied directions (null lets Kubernetes infer).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
allowFrom | array | — | — | [{"namespace":"ingress","pods":{"app.kubernetes.io/name":"gateway"},"ports":[8080]}] |
allowTo | array | — | — | [{"cidr":"10.0.0.0/8"}] |
ingress | array | — | — | — |
egress | array | — | — | — |
policyTypes | array | — | — | — |
Service mesh
Sidecar injection plus the object that enforces mTLS — the half no
admission policy can check, since a policy sees the object being written
and can neither observe traffic nor require that another object exists.
The marker differs per mesh — Istio's webhook selects on a label, Linkerd's
injector reads an annotation — and the recipe knows which. Recipes join the
mesh exclusion group. strictNamespace is
standalone and has one member per mesh that has an object for it: it
selects every pod in the namespace, so place it with
kurly.list.
kurly.mesh.istio(mtls, inject, proxyImage, peerAuthentication)
Runs the workload in an Istio mesh: the sidecar injection LABEL (sidecar.istio.io/inject) on the pod template — a label rather than an annotation because Istio's injection webhook selects on labels only, so the annotation form injects nothing in a namespace that is not itself labelled — plus a security.istio.io/v1 PeerAuthentication named after the workload selecting its own pods, with mode STRICT so the sidecar refuses plaintext. mtls=null emits no PeerAuthentication (the namespace or mesh default stands); inject=false skips the marker for a cluster that labels the namespace; proxyImage names the image the INJECTED containers pull (Istio publishes from registry.istio.io/release, which an allow-list cluster refuses and an air-gapped one cannot reach), and kurly.mirror follows it onto a private registry; peerAuthentication merges verbatim into the emitted spec.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
mtls | string | — | "STRICT" | — |
inject | bool | — | true | — |
proxyImage | string | — | — | "ghcr.io/acme/mesh/proxyv2:1.30.3" |
peerAuthentication | object | — | — | — |
kurly.mesh.linkerd(inboundPolicy, inject, proxyImage)
Runs the workload in a Linkerd mesh. Everything is an ANNOTATION, enforcement included: linkerd.io/inject asks the proxy-injector for a sidecar (its webhook is called for every pod in every namespace that has not opted out, so unlike Istio there is no namespace to label first), and config.linkerd.io/default-inbound-policy=all-authenticated makes the proxy accept only mesh-authenticated clients. Linkerd has no object equivalent to a PeerAuthentication. Note that Linkerd installs a default probe authorization so an all-authenticated policy does not break kubelet probes, so a DECLARED PROBE PATH stays reachable without identity by design. inboundPolicy also takes cluster-authenticated / all-unauthenticated / deny / audit, or null to leave the namespace or control-plane default in force; inject=false suits an already-annotated namespace; proxyImage names the image the injected proxy pulls (Linkerd publishes from cr.l5d.io, which an allow-list cluster refuses and an air-gapped one cannot reach), and kurly.mirror follows it onto a private registry.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
inboundPolicy | string | — | "all-authenticated" | — |
inject | bool | — | true | — |
proxyImage | string | — | — | "ghcr.io/acme/mesh/linkerd-proxy:edge-26.8.1" |
kurly.mesh.strictNamespace()
The standalone namespace-wide mTLS floor, one member per mesh that has an object for it — strictNamespace.istio() emits a PeerAuthentication selecting every pod in the namespace it is applied to. Not composed onto a workload (a workload that emitted one would legislate for its neighbours), so place it into a manifest set with kurly.list, the way network.denyAll is. There is no linkerd member: its floor is an annotation on the Namespace object or a control-plane setting, and kurly renders neither.
Security
Pod Security Standards profiles, composed as mixins. The last profile composed wins; single-knob hatches (in Features) fine-tune after it.
kurly.security.baseline
Relaxes what only restricted requires (root allowed, default capabilities kept, privilege escalation and unpinned seccomp permitted); keeps the read-only root filesystem and user namespaces.
kurly.security.privileged
Emits no security fields at all — the manifest constrains nothing.
kurly.security.restricted
The default posture, written out. Compose after another profile to re-tighten.
Helpers
Rendering terminals — turn a composed app or an explicit set of parts into the output a consumer applies or publishes.
kurly.certificate(name, dnsNames, issuer, secretName, issuerKind, duration, renewBefore)
Authors a cert-manager Certificate — the CR cert-manager reconciles into a TLS Secret by obtaining a certificate for the DNS names from an issuer. The mint end of the same seam as externalSecret: a workload names the tls Secret it terminates on (an exposure's tls, keycloak's tlsSecret) and authors none, so this fills it with a real, auto-renewed certificate. secretName defaults to the Certificate's own name, so a workload's tls parameter pointed at that name lines up. issuerRef defaults to a ClusterIssuer.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "storefront-tls" |
dnsNames | array | yes | — | ["storefront.example.com"] |
issuer | string | yes | — | "letsencrypt-prod" |
secretName | string | — | — | — |
issuerKind | string | — | "ClusterIssuer" | — |
duration | string | — | — | — |
renewBefore | string | — | — | — |
kurly.externalSecret(name, secretStoreRef, data, refreshInterval)
Authors an External Secrets Operator ExternalSecret — the CR ESO reconciles into a Kubernetes Secret by pulling values from an external store (Vault, AWS/GCP Secrets Manager). kurly never mints key material (a policy invariant), so any named Secret a workload references can be filled by ESO instead of applied by hand. The target Secret takes the ExternalSecret's own name, matching the name the workload parameter points at. secretStoreRef and the data entries pass through verbatim — kurly does not model ESO's remoteRef schema, which would drift against its API.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "loki-storage" |
secretStoreRef | object | yes | — | — |
data | array | yes | — | — |
refreshInterval | string | — | "1h" | — |
kurly.join(parts)
Builds one flat array from parts that may be null (dropped) or nested arrays (flattened one level), for assembling any value with conditionals and optional groups. A Jsonnet `if` with no else is null when false, so an unmet condition drops out.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
parts | array | yes | — | — |
kurly.limitRange(name, defaultMemory, requestMemory, requestCpu, maxMemory, defaultEphemeral, maxEphemeral)
Authors a LimitRange — the namespace default that stops a noisy neighbour before it starts. Every kurly workload already caps its own memory, but a hand-applied or non-kurly pod can be BestEffort and eat a node; a LimitRange gives every container in the namespace a default request and a default LIMIT (so nothing runs uncapped) and an optional per-container max. CPU carries only a default request (no limit — a CPU limit throttles idle headroom, unlike memory). ephemeral-storage is opt-in, to bound the node-disk vector.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | — | "resource-defaults" | — |
defaultMemory | quantity | — | "512Mi" | — |
requestMemory | quantity | — | "128Mi" | — |
requestCpu | quantity | — | "100m" | — |
maxMemory | quantity | — | — | "4Gi" |
defaultEphemeral | quantity | — | — | "1Gi" |
maxEphemeral | quantity | — | — | "10Gi" |
kurly.list(parts)
Renders manifests as a kind: List. Takes one composed app (its hidden owned manifests — the store PVC, the config ConfigMap — ride along), or an array assembling several: apps expand to their manifests, bare manifests (a Certificate, an ExternalSecret) pass through, sublists flatten, and null entries (an if with no else) drop out.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
parts | any | yes | — | — |
kurly.mirror(registry, manifests)
Points every image in already-rendered manifests at another registry, for a cluster that pulls from a private one. Rewrites the rendered output, not config: an initContainer, a grafted-on sidecar and a custom resource's image are all unreachable from config, so a config-level knob would redirect the main container and leave the rest pulling from the internet. Only the registry (the first path segment) changes; repository, tag and digest are untouched.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
registry | string | yes | — | "harbor.internal/dockerhub" |
manifests | object | yes | — | — |
kurly.priorityClass(name, value, description, preemptionPolicy, globalDefault)
Authors a PriorityClass — the cluster-scoped tier that decides who wins under contention: the kubelet evicts the lowest-priority pods first under node pressure, and a pending high-priority pod can preempt lower ones. The other half of noisy-neighbour control (limits cap each pod; priority decides who survives). Create the tiers once, then assign per workload with kurly.priorityClassName. value orders them (higher wins); preemptionPolicy 'Never' waits for room rather than evicting others; globalDefault applies to pods naming none (at most one cluster-wide).
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | yes | — | "critical" |
value | int | yes | — | 1000000 |
description | string | — | — | — |
preemptionPolicy | string | — | — | "Never" |
globalDefault | bool | — | false | — |
kurly.production(app, host, gateway, gatewayNamespace, sectionName, gatewayClass, gatewayAnnotations, tls, issuer, issuerKind, cpu, memory, allowFrom, allowTo, networkVariant, replicas, priorityClassName)
Bundles the standard production concerns onto a composed app — exposure, TLS, DNS, a NetworkPolicy allow-list, a resource tier, replicas, a priority class — so a self-service portal writes ONE call per tenant instead of re-assembling the recipe. Returns the parts kurly.list renders (the app with the features composed on, plus the cert-manager Certificate the exposure terminates on when an issuer is named). Each concern applies only when asked for: no host skips exposure, no issuer leaves the certificate to the operator, empty allow-lists skip the NetworkPolicy. Attaches an HTTPRoute to a shared `gateway`, or generates a dedicated one with `gatewayClass`.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
app | object | yes | — | — |
host | hostname | — | — | "tenant1.example.com" |
gateway | string | — | — | "shared" |
gatewayNamespace | string | — | — | — |
sectionName | string | — | — | — |
gatewayClass | string | — | — | "istio" |
gatewayAnnotations | object | — | — | — |
tls | string | — | — | "tenant1-tls" |
issuer | string | — | — | "letsencrypt-prod" |
issuerKind | string | — | "ClusterIssuer" | — |
cpu | string | — | — | "250m" |
memory | string | — | — | "256Mi" |
allowFrom | array | — | — | — |
allowTo | array | — | — | — |
networkVariant | string | — | "kubernetes" | — |
replicas | int | — | — | 2 |
priorityClassName | string | — | — | "standard" |
kurly.resourceQuota(name, requestsCpu, requestsMemory, limitsCpu, limitsMemory, requestsStorage, ephemeralStorage, pods)
Authors a ResourceQuota — the namespace total ceiling, so one tenant namespace cannot starve the cluster. Caps the SUM across the namespace (each field omitted when null): the aggregate requests and limits, the storage requested, and the pod count. A requests quota needs every pod to declare requests, so pair it with a limitRange.
| Parameter | Type | Required | Default | Example |
|---|---|---|---|---|
name | string | — | "namespace-quota" | — |
requestsCpu | quantity | — | — | "8" |
requestsMemory | quantity | — | — | "16Gi" |
limitsCpu | quantity | — | — | "16" |
limitsMemory | quantity | — | — | "24Gi" |
requestsStorage | quantity | — | — | "200Gi" |
ephemeralStorage | quantity | — | — | "40Gi" |
pods | int | — | — | 50 |