Navidrome on Kubernetes without Docker Compose, with Kapitan
Navidrome 0.64.2, a music server with a web UI and the Subsonic API, with its SQLite database and its music on claims, 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
navidrome/navidrome@101145742f4762164202cb4858c9126258bdc463(contrib/docker-compose/docker-compose.yml),sha256:ef8a611ca3cf06af8879253809632b7005ff47cb892018006bb3e290022f8958 - 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 77 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 0, HIGH 28, MEDIUM 42, LOW 6, 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/deluan/navidrome.
What it deploys
recipes.navidrome(init.yml)recipes.navidrome.navidrome(navidrome.yml):navidrome
Required parameters
No parameter has to be set.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.navidrome
parameters:
namespace: navidrome
kapitan:
vars:
target: navidrome
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/navidrome:1.0.0@sha256:9a7c239b59e53509e695ba12ee3e194922e3c7bb86b1881a53ac9f23fbff53a1
output_path: inventory/classes/recipes/navidrome
The first command fetches the recipe into inventory/classes/recipes/navidrome, 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/navidrome@sha256:9a7c239b59e53509e695ba12ee3e194922e3c7bb86b1881a53ac9f23fbff53a1 \
--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 |
|---|---|
| Navidrome version | components.navidrome.image, tag and digest |
Navidrome’s settings, such as ND_SCANSCHEDULE or ND_BASEURL |
components.navidrome.env.<variable> |
size of the claim data, 1Gi |
components.navidrome.volume_claims.data.spec.resources.requests.storage |
size of the claim music, 1Gi |
components.navidrome.volume_claims.music.spec.resources.requests.storage |
| exposure | a route or Ingress of the target to the Service navidrome on port 4533 |
| resources | components.navidrome.resources |
The claim data holds the SQLite database, the cache of transcoded files
and artwork, and Navidrome’s settings. SQLite allows one replica.
The root filesystem is read-only, as the compose file asks. /tmp is an
emptyDir, because SQLite needs a temporary file for a database migration
of the first start (DEC-215 in the spec). With the compose file alone,
Navidrome 0.64.2 exits at its first start for that reason.
Mapping
The service navidrome becomes the component navidrome. Its name stays,
so no reference is rewritten.
| Compose | Recipe |
|---|---|
service navidrome |
StatefulSet navidrome, Service navidrome |
deluan/navidrome:latest |
docker.io/deluan/navidrome:0.64.2@sha256:38dc2727bfcfd5ede290f8ada114fc90368146f265ae4701ddddbcbe2a44ee52, which latest was at migration |
port 4533:4533 |
Service port http on 4533, ClusterIP |
volume navidrome_data at /data |
claim template data, ReadWriteOnce, 1Gi |
read_only: true |
readOnlyRootFilesystem: true and an emptyDir at /tmp (DEC-215) |
version, container_name, restart: unless-stopped |
not rendered |
the comment #- "/mnt/music:/music:ro" |
claim template music, ReadWriteOnce, 1Gi, read-only at /music (DEC-216) |
the comment # user: 1000:1000 |
UID, GID and fsGroup 1000 under restricted (DEC-217) |
Proof
check navidrome --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. It
starts a pod of Navidrome’s image, which carries curl and ffmpeg, as
UID 1000 with the claim music-navidrome-0 mounted, and writes a FLAC file
of three seconds of silence with a title, an artist and an album. From
there, through the Service navidrome on port 4533, it
- gets
/ping, and Subsonic error 40 from/rest/pingbefore a user exists - creates the admin through
/auth/createAdminand gets 403 for a second one - gets
okfrom/rest/pingas the admin, and error 40 for a wrong password - starts a scan with
/rest/startScan, finds the track by its title with/rest/search3, 3 seconds long, and downloads it byte for byte with/rest/download - deletes the pod
navidrome-0and, once its successor is ready, getsokas the admin and finds the track again.
Versions
1.0.0, :sha256:9a7c239b59e53509e695ba12ee3e194922e3c7bb86b1881a53ac9f23fbff53a1
The recipe's files are in kadet-hub/recipes. All recipes