Transmute
Deploy Transmute, a self-hosted file conversion platform, with the official pinned image and a production-safe single-writer topology. Upstream source and release history are available in the Transmute repository.
Features
- Persistent users, jobs, uploads and outputs under
/app/data. - Stable JWT signing key across pod restarts and chart upgrades.
- Native OIDC with public and optional cluster-internal issuer URLs.
- Ingress, Gateway API v1 HTTPRoute and IPv4/IPv6 dual-stack Service support.
- External Secrets Operator resources using complete
items[].specdefinitions. - Non-root execution, read-only root filesystem, no Linux capabilities and no mounted ServiceAccount token.
- Helm connection test using the upstream readiness endpoint.
Install
Helm repository
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install transmute helmforge/transmute \
--namespace transmute --create-namespace
OCI registry
helm install transmute oci://ghcr.io/helmforgedev/helm/transmute \
--namespace transmute --create-namespace
Before enabling Ingress or Gateway API, port-forward the Service privately, open
http://127.0.0.1:8080, and register the first account. On a new volume, that
first account becomes administrator.
kubectl port-forward service/transmute-transmute 8080:8080 --namespace transmute
Single-writer design
Transmute stores its database and conversion state locally. For data integrity,
the chart requires replicaCount: 1, requires persistence and uses a Recreate
Deployment strategy. Horizontal replicas are not supported by upstream’s SQLite
architecture. config.dataDir must equal persistence.mountPath so application
state cannot be redirected to ephemeral storage.
Operational boundaries
The chart owns one Deployment, its Service, a chart-created PVC and authentication Secret, plus enabled routing, NetworkPolicy, ExternalSecret and ServiceAccount resources. The platform owns DNS, TLS and certificates, routing controllers, storage classes and snapshots, External Secrets Operator and its SecretStore, the OIDC provider, and any externally supplied Secret or PVC.
The health probes and Helm test verify the upstream endpoints and Service connectivity. They do not prove that every conversion format succeeds, that OIDC is reachable, or that the PVC has enough free space for another job. Log collection, metrics, alerts, dashboards and external availability checks remain platform responsibilities.
OIDC
Register this callback URI with Keycloak, Authentik, Authelia or another OpenID Connect provider:
https://transmute.example.com/api/oidc/callback
config:
appUrl: https://transmute.example.com
oidc:
enabled: true
issuerUrl: https://id.example.com/realms/main
internalUrl: http://keycloak.identity.svc/realms/main
clientId: transmute
clientSecret: replace-me
displayName: Company SSO
usernameClaim: preferred_username
autoCreateUsers: true
autoLaunch: false
internalUrl is optional and is useful when pods cannot reach the public
issuer. Transmute derives the callback from config.appUrl and requests the
fixed scopes openid email profile.
External Secrets
Use auth.existingSecret to point the Deployment to the Secret produced by ESO:
auth:
existingSecret: transmute-auth
externalSecrets:
enabled: true
items:
- fullnameOverride: transmute-auth
spec:
secretStoreRef:
name: platform-secrets
kind: ClusterSecretStore
target:
name: transmute-auth
creationPolicy: Owner
data:
- secretKey: AUTH_SECRET_KEY
remoteRef:
key: applications/transmute
property: auth-secret-key
- secretKey: OIDC_CLIENT_SECRET
remoteRef:
key: applications/transmute
property: oidc-client-secret
Gateway API
The Gateway API controller and parent Gateway must already exist. A route rule
without backendRefs targets the chart Service automatically. Gateway API and
Ingress may be enabled together.
gatewayAPI:
enabled: true
httpRoutes:
- name: public
parentRefs:
- name: shared-gateway
namespace: gateway-system
sectionName: https
hostnames:
- transmute.example.com
See the Gateway API documentation for controller and Gateway setup.
Dual-stack networking
Leave the Service IP family values unset to inherit the cluster default, or request dual-stack without forcing an unavailable address family:
service:
ipFamilyPolicy: PreferDualStack
Explicit ipFamilies: [IPv4, IPv6] is intended for clusters configured for
IPv4/IPv6 dual-stack.
Backup
All durable data is stored on the single PVC. Quiesce application writes and use a CSI snapshot or file-level volume backup. Back up the authentication Secret as well. The chart does not provide an application-aware online backup job.
Upgrade to chart v2
Version 2 is a breaking correction of the chart’s public contract. Before upgrading:
- replace
config.queueConcurrencywith conversion and compression worker concurrency values; - replace
config.jobTimeoutwithconfig.conversionJobStaleAfterMinutes; - replace
auth.enabledwithconfig.allowUnauthenticated; - remove
config.storageDir,auth.allowRegistrationandauth.initialAdmin; - replace
oidc.providerUrlwithoidc.issuerUrl; - set
config.appUrlinstead ofoidc.redirectUri; - replace
oidc.autoRegisterwithoidc.autoCreateUsers; - migrate the legacy ESO fields to
externalSecrets.items[].spec; - ensure the authentication Secret uses
AUTH_SECRET_KEY; - keep one replica and persistent storage enabled.
The old initial administrator, registration, redirect URI, OIDC scope and local authentication switches did not correspond to upstream Transmute settings and have been removed.
Configuration reference
Image, naming and identity
| Value | Default | Purpose |
|---|---|---|
replicaCount |
1 |
Fixed single-writer replica count |
image.repository |
ghcr.io/transmute-app/transmute |
Official image |
image.tag |
v2.1.1 |
Pinned application release |
image.pullPolicy |
IfNotPresent |
Image pull policy |
imagePullSecrets |
[] |
Private registry credentials |
nameOverride, fullnameOverride |
"" |
Resource naming overrides |
commonLabels |
{} |
Labels on chart resources |
serviceAccount.create |
true |
Create the ServiceAccount |
serviceAccount.name |
"" |
Existing or generated name |
serviceAccount.annotations |
{} |
ServiceAccount annotations |
serviceAccount.automountServiceAccountToken |
false |
API token mount |
Application and authentication
| Value | Default | Purpose |
|---|---|---|
config.appUrl |
"" |
Public URL, required for OIDC |
config.dataDir |
/app/data |
Durable application data |
config.conversionWorkerConcurrency |
5 |
Conversion worker count |
config.conversionJobStaleAfterMinutes |
60 |
Stale job threshold |
config.compressionWorkerConcurrency |
5 |
Compression worker count |
config.invalidatePdfDigitalSignatures |
false |
Permit signature-invalidating PDF changes |
config.maxChunkSize |
0 |
Upload chunk size; zero uses upstream behavior |
config.allowUnauthenticated |
false |
Permit unauthenticated use |
config.domainAuthConfigPath |
"" |
Optional domain-auth configuration file |
auth.secretKey |
generated | Inline JWT signing key |
auth.existingSecret |
"" |
Existing JWT/OIDC Secret |
auth.secretKeyKey |
AUTH_SECRET_KEY |
JWT field in the Secret |
auth.algorithm |
HS256 |
JWT signing algorithm |
auth.accessTokenExpireMinutes |
60 |
Access-token lifetime |
OIDC
| Value | Default | Purpose |
|---|---|---|
oidc.enabled |
false |
Enable upstream OIDC |
oidc.issuerUrl |
"" |
Public issuer URL |
oidc.internalUrl |
"" |
Optional pod-facing issuer URL |
oidc.clientId |
"" |
OIDC client ID |
oidc.clientSecret |
"" |
Inline client secret |
oidc.clientSecretKey |
OIDC_CLIENT_SECRET |
Client secret field |
oidc.displayName |
SSO |
Login button label |
oidc.usernameClaim |
preferred_username |
Local username claim |
oidc.autoCreateUsers |
true |
Create users at first OIDC login |
oidc.autoLaunch |
false |
Start OIDC automatically from login |
Network and routes
| Value | Default | Purpose |
|---|---|---|
service.type |
ClusterIP |
Service type |
containerPorts.http |
8080 |
Transmute process HTTP port |
service.port |
8080 |
Service port |
service.annotations |
{} |
Service annotations |
service.ipFamilyPolicy |
omitted | Single or dual-stack policy |
service.ipFamilies |
omitted | Ordered IP families |
ingress.* |
disabled | Class, annotations, hosts and TLS |
gatewayAPI.enabled |
false |
Render HTTPRoutes |
gatewayAPI.httpRoutes |
[] |
Canonical route definitions |
networkPolicy.enabled |
false |
Render ingress policy |
networkPolicy.ingressFrom |
[] |
Allowed ingress peers |
networkPolicy.egressIsolation |
false |
Enable explicit egress isolation |
networkPolicy.extraEgress |
[] |
DNS, OIDC and integration egress rules |
Storage, runtime and scheduling
| Value | Default | Purpose |
|---|---|---|
persistence.enabled |
true |
Required persistent storage |
persistence.size |
20Gi |
PVC capacity |
persistence.storageClass |
"" |
Storage class; - disables dynamic provisioning |
persistence.accessModes |
[ReadWriteOnce] |
PVC access modes |
persistence.mountPath |
/app/data |
Container mount path |
persistence.existingClaim |
"" |
Existing PVC |
resources |
CPU 200m, memory 256Mi/2Gi | Container resources |
podSecurityContext, securityContext |
hardened | Pod and container security |
livenessProbe, readinessProbe, startupProbe |
upstream health endpoints | Probes |
externalSecrets.enabled |
false |
Render ExternalSecrets |
externalSecrets.apiVersion |
external-secrets.io/v1 |
ESO API version |
externalSecrets.refreshInterval |
1h |
Default refresh interval |
externalSecrets.items |
[] |
Full ExternalSecret specs |
env, envFrom |
[] |
Extra environment configuration |
volumeMounts, volumes |
[] |
Extra pod storage |
nodeSelector, tolerations, affinity |
empty | Scheduling constraints |
topologySpreadConstraints |
[] |
Topology spreading |
priorityClassName |
"" |
Pod priority class |
terminationGracePeriodSeconds |
30 |
Shutdown grace period |
Troubleshooting
Check rollout, events, logs and the chart test:
RELEASE=transmute
NAMESPACE=transmute
kubectl rollout status deployment \
--selector "app.kubernetes.io/instance=${RELEASE}" --namespace "${NAMESPACE}"
kubectl describe pod --selector "app.kubernetes.io/instance=${RELEASE}" \
--namespace "${NAMESPACE}"
kubectl logs --selector "app.kubernetes.io/instance=${RELEASE}" \
--namespace "${NAMESPACE}" --all-containers --tail=200
helm test "${RELEASE}" --namespace "${NAMESPACE}"
For OIDC, confirm that config.appUrl produces the registered callback, that
discovery returns the exact oidc.issuerUrl, and that pods can reach the public
or internal provider URL. For conversion failures, inspect memory limits,
temporary space, PVC capacity and application logs.