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.
- Latest version:
1.0.0, published - Source: Docker Compose file
usememos/dotcom@b5fd0a3dff1408783452c627b05dc7aecf745c47(content/docs/deploy/docker-compose.mdx),sha256:f2d53cb23eea5cd5f65beb4b716d443ac2e225027e398ac41ab9e9fa79d5ef02 - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade D 6 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 0, HIGH 2, MEDIUM 2, LOW 1, UNKNOWN 1. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/neosmemo/memos.
What it deploys
recipes.memos(init.yml)recipes.memos.memos(memos.yml):memos
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
- gets
Service ready.from/healthzand the web UI from/ - requires from
/api/instance/profilethe image’s version, the instance URL, the public access policy and that the instance needs its setup - creates the first account through
/api/userswithout a login and requires the roleADMIN, then gets 403 there for a second account - gets 400 from
/api/auth/signinfor a wrong password, signs in, creates a private memo with the access token and reads it back, and gets 401 for the memo without a token - deletes the pod
memos-0and, once the new pod is ready, requires that the instance needs no setup, signs in again and reads the memo, which needsmemos_prod.dbon the claim.
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
1.0.0, :sha256:712f2d5e43daa2305fef84f8707bb1e1f44470d49df5c641d71edcb8ac5568d4
The recipe's files are in kadet-hub/recipes. All recipes