kadet-hub logokadet-hub

Memos on Kubernetes without Docker Compose, with Kapitan

Memos 26.11.0, a self-hosted note-taking app with a timeline of Markdown notes, with its data in SQLite on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from the Docker Compose example in the Memos documentation.

What it deploys

Required parameters

applications.memos.instance_url is MEMOS_INSTANCE_URL, the URL users reach Memos at, with its scheme, such as https://memos.example.com. Memos builds the links of its sitemap from it and allows cross-origin requests with cookies from that origin. On the first start the URL also makes the instance public, so visitors without an account see the memos marked public (DEC-171). An admin can change that in the instance settings. A later change of the URL leaves it as it is. Without the parameter the compile stops with Interpolation key 'applications.memos.instance_url' not found.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.memos
parameters:
  namespace: memos
  kapitan:
    vars:
      target: memos
  applications:
    memos:
      instance_url: https://memos.example.com

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.0:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/memos:1.0.0@sha256:712f2d5e43daa2305fef84f8707bb1e1f44470d49df5c641d71edcb8ac5568d4
        output_path: inventory/classes/recipes/memos

The first command fetches the recipe into inventory/classes/recipes/memos, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/memos@sha256:712f2d5e43daa2305fef84f8707bb1e1f44470d49df5c641d71edcb8ac5568d4 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

Setting Inventory
Memos version components.memos.image, tag and digest
instance URL applications.memos.instance_url
other environment variables of Memos, such as MEMOS_LOG_LEVEL or MEMOS_WEBHOOK_PRIVATE_NETWORK_ALLOWLIST components.memos.env.<variable>
exposure a route or Ingress of the target to the Service memos on port 5230
size of the claim data, 1Gi components.memos.volume_claims.data.spec.resources.requests.storage
resources components.memos.resources

Memos keeps its state in /var/opt/memos on the claim: the SQLite database memos_prod.db and uploaded files. SQLite allows one replica. Memos answers /healthz with Service ready. while its database answers. The recipe renders no probe, because neither the compose file nor the image defines a health check.

The Service memos serves plain HTTP on port 5230, the port the compose file publishes, for the web UI and the API alike. TLS belongs to the route or Ingress in front of it.

Mapping

The service memos becomes the component memos. Its name stays, so no reference is rewritten.

Compose Recipe
service memos StatefulSet memos, Service memos
neosmemo/memos:stable docker.io/neosmemo/memos:26.11.0@sha256:087e7dc6b76b31fcd1107a5767398b2a310ced209adb791efd8760c745dc8034, which stable was at migration
port 5230:5230 Service port http on 5230 to the container’s port 5230, ClusterIP
bind mount ./data at /var/opt/memos claim template data, ReadWriteOnce, 1Gi
MEMOS_PORT: 5230, MEMOS_DRIVER: sqlite the same variables
MEMOS_INSTANCE_URL: https://memos.example.com applications.memos.instance_url (DEC-171)
container_name: memos not rendered
restart: unless-stopped not rendered
not in the compose file UID and GID 10001 with fsGroup 10001 under restricted (DEC-172)
not in the compose file ConfigMap memos with instance.json at /etc/memos/config, registration closed (DEC-173)

Proof

check memos --level deploys with tests/target.yml, on kind for each Kubernetes minor CI tests, with check 0.16.0. A compose file has no Kubernetes objects to compare with, so the recipe is proven by deploying it (RCP-38). tests/target.yml sets the instance URL to the Service’s own address, http://memos:5230.

tests/smoke requires that the pod passes Pod Security restricted. It then starts a pod of Memos’ image, whose busybox carries wget. From there, through the Service memos on port 5230, it

Not tested: the web UI in a browser, attachments, MySQL or PostgreSQL in place of SQLite, and a route or Ingress in front of the Service.

Versions

The recipe's files are in kadet-hub/recipes. All recipes