Stirling-PDF on Kubernetes without Helm, with Kapitan
Stirling-PDF 3.2.0, a web UI and HTTP API that merges, splits, converts and edits PDF files, with its settings and users on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from the project’s Helm chart stirling-pdf-chart 4.0.0.
- Latest version:
1.0.0, published - Source chart:
https://docs.stirlingpdf.com/Stirling-PDF-chart/stirling-pdf-chart:4.0.0@sha256:fe3700310f5422606f5f983000afb39ddf6b54b66d034c6f041fefb8c0e788f9 - Proof level:
deploys, compared with the chart 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 F 652 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 6, HIGH 41, MEDIUM 499, LOW 106, UNKNOWN 0. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/stirlingtools/stirling-pdf.
What it deploys
recipes.stirling-pdf(init.yml)recipes.stirling-pdf.stirling-pdf(stirling-pdf.yml):stirling-pdf-stirling-pdf-chart
Required parameters
applications.stirling-pdf.admin.username and
applications.stirling-pdf.admin.password are the login and password of
the first admin. The recipe puts them into the Secret stirling-pdf-admin,
from which the container reads SECURITY_INITIALLOGIN_USERNAME and
SECURITY_INITIALLOGIN_PASSWORD. Stirling-PDF creates the account on its
first start, while its database on the claim holds no user, and does not
create the default admin with the password stirling then. A later change
of either parameter does not reach the account. Change the password in the
web UI instead.
Set the password to a ref with the backend of your choice. Without either
parameter 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.stirling-pdf
parameters:
namespace: stirling-pdf
kapitan:
vars:
target: stirling-pdf
applications:
stirling-pdf:
admin:
username: <login>
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/stirling-pdf:1.0.0@sha256:095a0a26d9e5b800bbd520d2a09d98b31ef8e7b440dcfccc778afdec45e217bc
output_path: inventory/classes/recipes/stirling-pdf
The first command fetches the recipe into inventory/classes/recipes/stirling-pdf, 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/stirling-pdf@sha256:095a0a26d9e5b800bbd520d2a09d98b31ef8e7b440dcfccc778afdec45e217bc \
--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
<c> stands for components.stirling-pdf-stirling-pdf-chart.
| Chart value | Inventory |
|---|---|
image.tag |
<c>.image, tag and digest |
envs |
<c>.env, a mapping of name to value |
persistence.size |
<c>.persistent_volume_claims.stirling-pdf-stirling-pdf-chart.storage |
persistence.storageClass |
<c>.persistent_volume_claims.stirling-pdf-stirling-pdf-chart.storage_class |
resources |
<c>.resources |
service.type |
<c>.service.type |
rootPath |
<c>.env.SYSTEM_ROOTURIPATH and the path of each probe in <c>.healthcheck |
nodeSelector, tolerations, affinity |
<c>.node_selector, <c>.tolerations, <c>.affinity |
deployment.preStopSleepSeconds, .terminationGracePeriodSeconds |
<c>.lifecycle.preStop.exec.command, <c>.grace_period |
probes.startup.failureThreshold |
<c>.healthcheck.startup.failure_threshold |
The chart’s resources are empty. Its README asks for a memory limit,
because the image sizes the Java heap from it. Without one the heap may
grow to 70% of the node’s memory.
Running as stirlingpdfuser, the image cannot start LibreOffice as its
second user stirlingofficeuser, which needs root. LibreOffice then runs
as stirlingpdfuser in the image’s Landlock and seccomp sandbox.
The chart’s Ingress, HTTPRoute, ServiceMonitor, PodDisruptionBudget, HorizontalPodAutoscaler and cluster mode with PostgreSQL, Valkey, S3 and a licence are off by default and not part of the recipe. The ServiceAccount has no role.
Any list set in the target is appended to the recipe’s list. To replace
one, set the key to ~ in a class included after the recipe and the new
list in the target.
Proof
check stirling-pdf --level deploys with tests/values.yaml and
tests/target.yml, on kind for each Kubernetes minor CI tests. The four
objects differ in three ways. The Deployment and the Service select on
name, the chart on app.kubernetes.io/name and instance, and both
select the same pods. The container is named after the component, the
chart’s stirling-pdf-chart. The ServiceAccount sets
automountServiceAccountToken: true, the API default the chart leaves
unset, while the pod opts out as in the chart. The Secret
stirling-pdf-admin is only in the recipe, accepted as a Referenced
Secret: the chart’s container reads exactly its keys username and
password through envs. The chart binds no role.
tests/smoke checks that the pod passes Pod Security restricted and
that /api/v1/info/status reports the image tag through the Service. The
merge endpoint and the sign-up answer 401 without a login, and so does a
login as admin with stirling. As the admin of tests/target.yml it
merges two one-page PDFs that the test writes, and qpdf --check and
pdftotext in the image find a valid file with both pages in order. The test changes the
setting ui.appNameNavbar through the admin API, deletes the pod, and
reads the new value from its successor, which loads it from settings.yml
on the claim.
Versions
1.0.0, :sha256:095a0a26d9e5b800bbd520d2a09d98b31ef8e7b440dcfccc778afdec45e217bc
The recipe's files are in kadet-hub/recipes. All recipes