kadet-hub logokadet-hub

linkding on Kubernetes: a bookmark manager on SQLite

linkding 1.47.0, a bookmark manager with a web UI and a REST API, with its SQLite database on a claim and its first superuser from the inventory, rendered by the kadet-hub kubernetes generator 1.14.0 from its Docker Compose file.

What it deploys

Required parameters

applications.linkding.superuser_name and applications.linkding.superuser_password are LD_SUPERUSER_NAME and LD_SUPERUSER_PASSWORD. linkding creates this superuser at a start when no user of that name exists (DEC-341 in docs/specs/recipes.md). Set the password as a ref.

linkding reads the password only when it creates the user, so a changed parameter does not change the password of an existing superuser. Change it in the admin app at /admin or with python manage.py changepassword in the pod. A changed name creates a second superuser.

Without either parameter the compile stops with Interpolation key and its name.

Include

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

classes:
  - recipes.linkding
parameters:
  namespace: linkding
  kapitan:
    vars:
      target: linkding
  applications:
    linkding:
      superuser_name: admin
      superuser_password: <ref>

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/linkding:1.0.0@sha256:90988952b961bc4565c684a64750728d7e2d1bbd50ae5cefe47d10b5f4c725ed
        output_path: inventory/classes/recipes/linkding

The first command fetches the recipe into inventory/classes/recipes/linkding, 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/linkding@sha256:90988952b961bc4565c684a64750728d7e2d1bbd50ae5cefe47d10b5f4c725ed \
  --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
linkding version components.linkding.image, tag and digest
linkding’s options, such as LD_CSRF_TRUSTED_ORIGINS or LD_DISABLE_URL_VALIDATION components.linkding.env.<variable>, credentials through components.linkding.secrets with a ref
size of the claim data, 1Gi components.linkding.volume_claims.data.spec.resources.requests.storage
exposure a route or Ingress of the target to the Service linkding on port 9090
resources components.linkding.resources

The claim data is mounted at /etc/linkding/data. It holds the SQLite database db.sqlite3, the key secretkey.txt with which Django signs sessions, and the folders of favicons, preview images and uploaded assets. SQLite allows one replica. linkding’s backups page describes python manage.py full_backup, which runs in the pod as well.

The background tasks are off: LD_DISABLE_BACKGROUND_TASKS is True (DEC-340 in the spec). The image starts its task processor through supervisord, which refuses to start as a user other than root. Favicons, preview images, snapshots on the Internet Archive and the refresh of titles and descriptions therefore do nothing, even when a user turns them on in the settings. The REST API still reads the title and description of a page when it creates a bookmark without them.

Mapping

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

Compose Recipe
service linkding StatefulSet linkding, Service linkding, Secret linkding with LD_SUPERUSER_PASSWORD
sissbruecker/linkding:latest docker.io/sissbruecker/linkding:1.47.0@sha256:e35cb50e0581178f245125ffaa909c565416c94c9f22ace305a7d234a4345522, which latest was at migration
port ${LD_HOST_PORT:-9090}:9090 Service port http on 9090, ClusterIP
bind mount ${LD_HOST_DATA_DIR:-./data} at /etc/linkding/data claim template data, ReadWriteOnce, 1Gi
env_file: .env, which the documentation copies from .env.sample env with each line of .env.sample but the ones below
LD_SUPERUSER_NAME= and LD_SUPERUSER_PASSWORD= the required parameters, the password through the Secret linkding (DEC-341)
LD_DISABLE_BACKGROUND_TASKS=False True (DEC-340)
LD_DB_PASSWORD= not rendered, linkding reads it only for PostgreSQL
the image’s HEALTHCHECK, curl -f on /health readiness probe with the same sh -c command every 30 seconds with a timeout of 1 second and 3 failures
container_name, restart: unless-stopped not rendered
no user, so the image’s root UID, GID and fsGroup 33 under restricted (DEC-339)

.env.sample has the SHA-256 12a0e13fc1b005ea484b96c666813d1a601de5ad85c9db82262b67950dcb10bf.

Proof

check linkding --level deploys with tests/target.yml, on kind for each Kubernetes minor CI tests. A compose file has no Kubernetes objects to compare with, so the recipe is proven by deploying it (RCP-38).

tests/smoke checks that the pod passes Pod Security restricted and starts a pod of linkding’s image, which carries curl. From there, through the cluster IP of the Service linkding on port 9090, it

Versions

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