kadet-hub logokadet-hub

Nextcloud on Kubernetes without Helm, with Kapitan

Nextcloud 35.0.2 as one replica with its background jobs and an HTTPRoute to a Gateway the cluster already runs, rendered by the kadet-hub kubernetes generator 1.9.0 from Nextcloud’s Helm chart 9.4.1. The database is the included recipe postgresql, the cache and file locking valkey.

What it deploys

Required parameters

applications.nextcloud.host is the host name of the route and Nextcloud’s trusted domain. applications.nextcloud.gateway.name and applications.nextcloud.gateway.namespace name the Gateway the route attaches to, whose listener must admit routes from the target’s namespace. applications.nextcloud.admin.password, the admin account’s password, and applications.valkey.password, which the included recipe valkey requires, are refs with the backend of your choice. Without one of them the compile stops with Interpolation key '<key>' not found.

Include

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

classes:
  - recipes.nextcloud
parameters:
  namespace: nextcloud
  kapitan:
    vars:
      target: nextcloud
  applications:
    nextcloud:
      host: <host>
      admin:
        password: <ref>
      gateway:
        name: <gateway>
        namespace: <gateway namespace>
    valkey:
      password: <ref>

Fetching it

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

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/nextcloud:1.0.1@sha256:dc50173cb04ad7248e76268481a3ddd9ae5c491df836cc7971c68a96bfef2305
        output_path: inventory/classes/recipes/nextcloud
      - type: oci
        source: ghcr.io/kadet-hub/recipes/postgresql:1.0.1@sha256:32c3af2cef6414e9dd57589bf984975b12c525b7c3136615fd5ac2ea7b1f688e
        output_path: inventory/classes/recipes/postgresql
      - type: oci
        source: ghcr.io/kadet-hub/recipes/valkey:1.0.1@sha256:9ba856132f2ed05ab6748d6f23f0ca7df86947593c1dd9fa4b7af21ae3f61e24
        output_path: inventory/classes/recipes/valkey

The first command fetches the recipe into inventory/classes/recipes/nextcloud, 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 each recipe the fetch inventory pins:

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

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

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/valkey@sha256:9ba856132f2ed05ab6748d6f23f0ca7df86947593c1dd9fa4b7af21ae3f61e24 \
  --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

Chart value Inventory
image.tag components.nextcloud.image, tag and digest
nextcloud.host, nextcloud.trustedDomains, httpRoute.hostnames applications.nextcloud.host
nextcloud.username applications.nextcloud.admin.username, admin by default
nextcloud.password applications.nextcloud.admin.password
httpRoute.parentRefs applications.nextcloud.gateway.name and .namespace, and .section for the listener’s sectionName, unset by default
persistence.size, persistence.storageClass components.nextcloud.persistent_volume_claims.nextcloud-nextcloud.storage, .storage_class
persistence.nextcloudData.size, persistence.nextcloudData.storageClass components.nextcloud.persistent_volume_claims.nextcloud-nextcloud-data.storage, .storage_class
nextcloud.extraEnv components.nextcloud.env, which the cron sidecar gets too
resources components.nextcloud.resources
nodeSelector, tolerations, affinity components.nextcloud.node_selector, components.nextcloud.tolerations, components.nextcloud.affinity

The database, its owner and the Cluster’s name are settings of postgresql, which its README lists. Nextcloud reads the host, database and credentials from the Secret <cluster>-app, where <cluster> is applications.postgresql.cluster.

The image writes the host into config.php when it installs Nextcloud. A later change of applications.nextcloud.host also needs occ config:system:set trusted_domains.

Set TRUSTED_PROXIES in components.nextcloud.env to the addresses of the Gateway implementation’s proxies. It has no default, and without it Nextcloud takes the proxy’s address for the client’s.

The image accepts PHP uploads up to PHP_UPLOAD_LIMIT, 512M by default, set in components.nextcloud.env. Gateway API has no request body limit of its own, so the Gateway implementation must allow bodies of that size in its own policy.

The chart’s liveness probe restarts a container that is still installing, and the image installs only on its first start. The recipe therefore runs the chart’s startup probe, which waits up to 330 seconds.

SMTP, notify_push, an office server, imaginary, metrics and more than one replica are not part of this version.

Proof

check nextcloud --level deploys with tests/values.yaml and tests/target.yml, on kind kindest/node:v1.37.0, with cnpg and gateway-api installed first. The objects of postgresql and valkey are left out of the comparison and applied with the recipe. The Deployment and the Service select on name, the chart on app.kubernetes.io/name, instance and component. tests/target.yml runs one PostgreSQL instance.

tests/smoke waits for the Cluster and reads status.php through the Gateway of check, with the route’s host name, until it reports "installed":true. It then writes a file over WebDAV as the admin and reads it back, and checks that occ names Redis as the file locking backend. The redirects from /.well-known/carddav and /.well-known/caldav to /remote.php/dav are compared at renders only, because cloud-provider-kind drops the path of a redirect.

Versions

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