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.
- Latest version:
1.0.0, published - Source: Docker Compose file
sissbruecker/linkding@24b5ad6cc9bde497b5d1b1e86aed5a2fb7d25c2f(docker-compose.yml),sha256:d8f0b8b359100e6721dbdf240cf8fb62afc37bd9c737eaa35fc18c0f58d2785f - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.20.2, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 4307 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 8, HIGH 521, MEDIUM 2861, LOW 905, UNKNOWN 12. Scanned 2026-11-14 12:09 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/sissbruecker/linkding.
What it deploys
recipes.linkding(init.yml)recipes.linkding.linkding(linkding.yml):linkding
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
- gets
/health - gets 401 from
/api/bookmarks/without credentials, and again after a login with a wrong password - logs in through the login form as the superuser of
tests/target.yml, creates a bookmark through the REST API withdisable_scraping, so linkding loads nothing from its URL, and lists it - deletes the pod
linkding-0and, once its successor is ready, lists the bookmark again in the session from before the restart.
Versions
1.0.0, :sha256:90988952b961bc4565c684a64750728d7e2d1bbd50ae5cefe47d10b5f4c725ed
The recipe's files are in kadet-hub/recipes. All recipes