Helm Chart Deployment
This repository ships two Helm charts, for two different deployment models:
| Chart | Path | Workload kind | Use when |
|---|---|---|---|
emgr | helm/emgr/ | Standard Kubernetes Deployment (via the bjw-s common library chart) | You want a normal, always-on Deployment - fixed replicaCount, an optional HPA, a PersistentVolumeClaim for local_fs storage. |
emgr-serverless | helm/serverless/ | Knative Service (via the Bitnami common library chart) | You run Knative and want scale-to-zero, and S3/MinIO storage (no local disk needed). |
Both charts install emgr itself; pick one, not both, for a given deployment.
Installing a published chart
Both charts are published two ways; pick whichever your tooling prefers. Row one (chart-releaser) has published every version below since 2025. Row two (OCI, signed) is new: it publishes on the next vX.Y.Z release tag whose chart version: has moved, not retroactively for versions already on the classic repository.
| Reference | Signed | Source | |
|---|---|---|---|
| Classic Helm repository | https://vaam-apps.github.io/image-resizer/ | No | .github/workflows/deploy-docs.yml's deploy-helm job (helm/chart-releaser-action), on every push to main where a chart's own version: moved. |
| OCI registry | oci://ghcr.io/vaam-apps/charts/emgr and oci://ghcr.io/vaam-apps/charts/emgr-serverless | Yes (cosign, keyless) | .github/workflows/release.yml, on a vX.Y.Z release tag, again only when a chart's own version: moved since its last publish. |
Both track each chart's hand-managed version: field (Chart.yaml), not the application's own release number (appVersion:) - see .github/workflows/release-please.yml's header for why the two are deliberately independent. A given version: therefore lands in both places or neither; there is no release where one has it and the other doesn't.
# Classic repository
helm repo add image-resizer https://vaam-apps.github.io/image-resizer/
helm repo update
helm install emgr image-resizer/emgr --namespace emgr --create-namespace
# OCI registry - no `helm repo add` needed. `CHART_VERSION` is
# `helm/emgr/Chart.yaml`'s own `version:` at the release you want, not the
# application's `vX.Y.Z` tag - see the table above for why the two differ.
CHART_VERSION=0.1.9
IMAGE=ghcr.io/vaam-apps/charts/emgr:$CHART_VERSION
# Optional, needs `cosign` and network access to sigstore's
# infrastructure. Read the output, not just the exit code - the identity
# below only matches a chart this repository's own release.yml built and
# signed; the same caveat vpay's own release runbook gives applies here
# too, unread until someone actually reads a `Subject`/`githubWorkflowRef`
# claim off a real signature.
cosign verify \
--certificate-identity-regexp '^https://github\.com/vaam-apps/image-resizer/\.github/workflows/release\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$IMAGE"
helm install emgr oci://ghcr.io/vaam-apps/charts/emgr --version "$CHART_VERSION" \
--namespace emgr --create-namespaceemgr-serverless installs the identical way, substituting that chart's own name, OCI path and Chart.yaml from the table above.
Prerequisites
- A Kubernetes cluster and Helm 3.
- For
emgr-serverless: Knative Serving installed on the cluster, andcert-managerif you enabledomain.tls.issuerRef.
emgr (Deployment chart)
Chart location
helm/emgr/. Chart.yaml pins its common dependency to bjw-s's library chart 4.0.1 (see helm/emgr/Chart.lock) - most of the templating (Deployment, Service, Ingress, ConfigMap, HPA, PVC) comes from that library via templates/common.yaml's single {{ include "bjw-s.common.loader.all" $ }}. Two plain templates are layered on top:
templates/poddisruptionbudget.yaml- aPodDisruptionBudgetwithminAvailable: 1, added because the pinned4.0.1library predates that chart's ownpodDisruptionBudgetvalues key. Without it, a voluntary disruption (node drain, cluster upgrade) could evict everymainreplica at once.- The probes under
controllers.main.containers.app.probesinvalues.yaml- the bjw-s common library does not synthesize liveness/readiness/startup probe defaults, so without this block traffic would route to pods before storage finishes initializing, and a wedged container would never be restarted.
Probes
All three probes are plain Kubernetes HTTP probes against /health on port 3000 (values.yaml, controllers.main.containers.app.probes):
| Probe | initialDelay | period | timeout | failureThreshold |
|---|---|---|---|---|
| liveness | 5s | 10s | 2s | 3 |
| readiness | 5s | 10s | 2s | 3 |
| startup | 0s | 5s | - | 30 (150s total before the container is killed for never finishing startup) |
What the chart configures out of the box
values.yaml's configMaps.config.data sets exactly three variables:
LOCAL_FS_STORAGE_PATH: /tmp/data/images
CDN_BASE_URL: https://emgr.com
PERFORMANCE_PROFILE: high_throughput...loaded via envFrom: configMapRef on the app container. A permission-fix init container (running as root) chowns/chmods the mounted ReadWriteMany PVC to uid/gid 1001 before the app container (which runs as that non-root user, defaultPodOptions.securityContext) starts.
Signing / metrics-auth secrets (GH #84)
emgr fails closed at startup without SIGNING_KEY/SIGNING_SALT (or ALLOW_UNSIGNED_REQUESTS), and without METRICS_AUTH_TOKEN (or ALLOW_UNAUTHENTICATED_METRICS) on an otel-enabled image (see Installation).
This is a real cluster, not a local compose.yaml, so the chart does not default ALLOW_UNSIGNED_REQUESTS/ALLOW_UNAUTHENTICATED_METRICS to true the way compose.yaml does for local development - that would silently trade security for convenience. Instead values.yaml's controllers.main.containers.app.env already wires all three variables to a Secret named emgr-signing:
env:
SIGNING_KEY:
valueFrom:
secretKeyRef: { name: emgr-signing, key: signing-key }
SIGNING_SALT:
valueFrom:
secretKeyRef: { name: emgr-signing, key: signing-salt }
METRICS_AUTH_TOKEN:
valueFrom:
secretKeyRef: { name: emgr-signing, key: metrics-auth-token }You must create that Secret yourself - the chart deliberately does not, so the key/salt/token never live in values.yaml or a values override:
kubectl create secret generic emgr-signing --namespace emgr \
--from-literal=signing-key="$(openssl rand -hex 32)" \
--from-literal=signing-salt="$(openssl rand -hex 16)" \
--from-literal=metrics-auth-token="$(openssl rand -hex 32)"If the Secret is missing, the pod fails loudly (CreateContainerConfigError, visible in kubectl describe pod) instead of starting insecurely. To point at a differently-named Secret, override the name: fields above in your own values file. See the bjw-s common library's env/envFrom docs for the full schema. METRICS_AUTH_TOKEN only matters once the image itself is an otel-enabled build - see the image tag note below.
Image tag
values.yaml's global.version defaults to fs-latest - a floating tag (moves on every push to main; .github/workflows/build.yml has no release-tag trigger, so there's no immutable semver tag to default to instead). fs-latest is a local_fs-only, non-otel build, so METRICS_AUTH_TOKEN doesn't apply unless you override global.version to one of the *_otel-* flavours (see Docker deployment for the full tag list). Pin global.version to a specific fs-<sha> (or fs_otel-<sha>) tag for anything beyond a quick smoke test - pullPolicy: Always on the container is a direct consequence of the default floating tag, so different pods don't silently end up running different builds under the same tag name.
Deployment steps
# From the local chart directory - see "Installing a published chart" above for the packaged alternative
helm dependency build ./helm/emgr
# Create the emgr-signing Secret first (see above) - the chart's values.yaml
# already points at it, no values override needed unless you want to
# rename the Secret or its keys.
helm install emgr ./helm/emgr --namespace emgr --create-namespace
kubectl get pods -n emgr
kubectl get svc -n emgrUpgrade / uninstall:
helm upgrade emgr ./helm/emgr --namespace emgr
helm uninstall emgr --namespace emgremgr-serverless (Knative chart)
Chart location
helm/serverless/, depending on Bitnami's common library chart (Chart.yaml; pulled from oci://registry-1.docker.io/bitnamicharts). templates/knative-service.yaml renders a single serving.knative.dev/v1 Service; templates/kserving-domain-mapping.yaml optionally adds a Knative DomainMapping (and a cert-manager Certificate, if domain.tls.issuerRef is set) when domain.enabled: true.
Probes
Unlike the Deployment chart, these are Knative-native probes (still plain httpGet on /health, port 3000 from ports.http1.port) - readinessProbe, livenessProbe and startupProbe, each with no explicit timing overrides, so Knative's own probe defaults apply.
What the chart configures out of the box
values.yaml's env map is wired for S3/MinIO storage, with the MinIO credentials already sourced from a Secret (name minio, keys access-key/secret-key) via secretKeyRef - the same pattern you'd use to add signing/metrics-auth secrets, see below:
env:
MINIO_ACCESS_KEY_ID: { secretKeyRef: { name: minio, key: access-key } }
MINIO_SECRET_ACCESS_KEY: { secretKeyRef: { name: minio, key: secret-key } }
MINIO_ENDPOINT_URL: http://minio:9000
MINIO_BUCKET: emgr
# ...Signing / metrics-auth secrets (GH #84)
Exactly like the emgr chart above, values.yaml's env map already wires SIGNING_KEY/SIGNING_SALT/METRICS_AUTH_TOKEN to a Secret named emgr-signing, using the same secretKeyRef shape as MINIO_ACCESS_KEY_ID above:
env:
SIGNING_KEY: { secretKeyRef: { name: emgr-signing, key: signing-key } }
SIGNING_SALT: { secretKeyRef: { name: emgr-signing, key: signing-salt } }
METRICS_AUTH_TOKEN:
{ secretKeyRef: { name: emgr-signing, key: metrics-auth-token } }Create that Secret yourself before installing (see the kubectl create secret command in the emgr chart section above - the same Secret works for both charts). Without it, the Knative revision fails its startup probe and never goes Ready, instead of serving unsigned requests or an open /metrics.
Image tag and registry (GH #85, fixed)
values.yaml's image.registry is empty and image.repository carries the full ghcr.io/vaam-store/image-resizer path - helm template used to render a doubled ghcr.io/ghcr.io/... reference when both fields set a registry host; only repository does now. image.tag defaults to s3_otel-latest, matching this chart's env defaults (S3/MinIO storage, OTLP_* variables meaningful only on an otel build) and an image flavour .github/workflows/build.yml actually publishes (see Docker deployment for the full tag list). Like helm/emgr's global.version, this is still a floating tag - pin to a specific s3_otel-<sha> for anything beyond a quick smoke test.
values.yaml's env.LOG_LEVEL is a plain info - it used to be Docker Compose's ${LOG_LEVEL:-info} shell-substitution syntax, which Helm never evaluates and rendered as that literal string. Override env.LOG_LEVEL directly (--set env.LOG_LEVEL=debug) if you want something else.
Deployment steps
helm dependency build ./helm/serverless
# Create the emgr-signing Secret first (see above).
helm install emgr-serverless ./helm/serverless --namespace emgr --create-namespace
kubectl get ksvc -n emgrUpgrade / uninstall:
helm upgrade emgr-serverless ./helm/serverless --namespace emgr
helm uninstall emgr-serverless --namespace emgrNotes
- Both charts are published two ways - see Installing a published chart above. The commands in each chart's own "Deployment steps" section install from the local
helm/emgr/helm/serverlessdirectories in this checkout instead (helm dependency buildfirst, to fetch the pinnedcommonlibrary chart intocharts/), which is the right choice when you're changing the chart itself, not just deploying it. - Configuration beyond what's listed above (performance tuning, the SSRF guard, resolution limits, presets) works the same way: add the env var from Configuration to whichever chart's env mechanism you're using.
.github/workflows/ci.yml'shelm-verifyjob runshelm dependency build,helm lintandhelm templatefor both charts on every PR (GH #84 / #85) - a rendering defect like the ones described above now fails a build instead of reaching a user.