Open-source podcast hosting platform. Castopod manages your episodes, generates RSS feeds for distribution to
Apple Podcasts, Spotify, and other directories, tracks anonymous listener analytics, and federates with the
Fediverse via ActivityPub. Metadata lives in MariaDB and uploaded audio and images live on a persistent volume.
The chart applies migrations and creates the superadmin on first start, so the instance never opens with a public
install wizard.
Key Features
RSS feed generation — automatic, standards-compliant podcast feeds for all directories
Built-in analytics — privacy-respecting listener tracking with hashed IDs
Fediverse integration — ActivityPub support for social interactions and episode sharing
Ready on first start — setup init container runs migrations and creates the superadmin from a Secret
Hardened pod — non-root www-data, no privilege escalation, all capabilities dropped except NET_BIND_SERVICE
Scheduler sidecar — scheduled publishing, WebSub, and federation tasks run every minute
MariaDB backend — bundled subchart or external database on any port
Optional Redis cache — authenticated Redis subchart as the cache handler
Persistent media — one PVC holds the writable directory and uploaded audio and images
S3 backup — scheduled archive of the data volume to S3-compatible storage
Configurable listener — castopod.port changes the real FrankenPHP port
Ingress or Gateway API — Ingress or HTTPRoutes
CDN support — separate media base URL for audio file delivery
castopod.baseURL only sets the URL that Castopod embeds in links and feeds. It does not expose the Service: enable
ingress or gatewayAPI for that hostname (see the Deployment Examples below) before using the public URL.
Getting Started
Without a base URL, Ingress, or HTTPRoute, the chart uses http://localhost:8080 and disables HTTPS redirection
for a local check:
helm install castopod helmforge/castopodkubectl rollout status deploy/castopod-castopod --timeout=300skubectl get secret castopod-castopod-admin -o jsonpath='{.data.admin-password}' | base64 -d; echokubectl port-forward svc/castopod-castopod 8080:80
Open http://localhost:8080/cp-auth/login and log in with [email protected] and the password above.
# values.yaml — Castopod with MariaDB and Redis for cachingcastopod: baseURL: 'https://podcast.example.com'mariadb: enabled: true auth: password: 'mariadb-password'# Castopod requires a Redis password; the subchart generates one.redis: enabled: true architecture: standalonepersistence: enabled: true size: 20Giingress: enabled: true ingressClassName: traefik hosts: - host: podcast.example.com paths: - path: / pathType: Prefix
# values.yaml — Castopod with media served from a CDN# Audio files are stored in the PVC but served via CDN URLcastopod: baseURL: 'https://podcast.example.com' mediaBaseURL: 'https://cdn.example.com/castopod'mariadb: enabled: true auth: password: 'mariadb-password'persistence: enabled: true size: 20Giingress: enabled: true ingressClassName: traefik hosts: - host: podcast.example.com paths: - path: / pathType: Prefix
Superadmin password (8+ characters); generated once when empty.
castopod.admin.existingSecret
string
""
Existing Secret with the superadmin password.
castopod.admin.existingSecretPasswordKey
string
admin-password
Key in the existing Secret.
setup.databaseWaitTimeoutSeconds
integer
300
Seconds the setup init container waits for MariaDB.
setup.resources
object
{}
Resources for the setup init container.
scheduler.enabled
boolean
true
Run the task scheduler sidecar (spark tasks:run every minute).
scheduler.resources
object
{}
Resources for the scheduler container.
baseURL is required and affects all RSS feeds
All episode audio URLs, podcast RSS feed links, and API responses are built using castopod.baseURL. If this value is
incorrect, podcast directories (Apple Podcasts, Spotify, etc.) will be unable to download episodes from your instance.
Always set the exact public URL including https:// and without a trailing slash.
Analytics
Parameter
Type
Default
Description
analytics.salt
string
""
Salt for hashing listener IDs. Generated once (64 chars) and kept on upgrade.
analytics.existingSecret
string
""
Existing Kubernetes Secret containing the analytics salt.
analytics.existingSecretKey
string
analytics-salt
Key inside the existing secret holding the salt value.
Persist the analytics salt for consistent reporting
The analytics salt is used to hash listener IP addresses into anonymous IDs. If the salt changes, historical listener
data becomes inconsistent — the same listener appears as multiple distinct users across time periods. The chart keeps
a generated salt across upgrades; set analytics.existingSecret to manage it outside the release.
Database — Embedded Subchart
Parameter
Type
Default
Description
mariadb.enabled
boolean
true
Deploy a bundled MariaDB subchart for Castopod.
mariadb.architecture
string
standalone
MariaDB deployment architecture.
mariadb.auth.database
string
castopod
Database name created by the subchart.
mariadb.auth.username
string
castopod
Database username created by the subchart.
mariadb.auth.password
string
""
Database password (auto-generated if empty).
Database — External
Parameter
Type
Default
Description
database.external.host
string
""
External MariaDB hostname or IP.
database.external.port
string
"3306"
External MariaDB port.
database.external.name
string
castopod
Database name on the external server.
database.external.username
string
castopod
Username for the external database.
database.external.password
string
""
Password for the external database (plain text — prefer secret).
database.external.existingSecret
string
""
Existing secret containing the database password.
database.external.existingSecretPasswordKey
string
password
Key inside the existing secret for the password.
Cache — Redis Subchart
Parameter
Type
Default
Description
redis.enabled
boolean
false
Deploy a bundled Redis subchart for caching.
redis.architecture
string
standalone
Redis deployment architecture.
redis.auth.enabled
boolean
true
Must stay true: Castopod requires a Redis password.
Persistence
One PVC holds two paths: the volume root is mounted at /var/www/html/writable (cache, sessions, logs, temporary
files) and the public-media subPath at /var/www/html/public/media (uploaded audio, artwork, and site images).
Parameter
Type
Default
Description
persistence.enabled
boolean
true
Enable a PVC for writable files and uploaded media.
persistence.size
string
10Gi
PVC size. Increase based on expected audio file volume.
persistence.storageClass
string
""
StorageClass for the PVC.
persistence.accessMode
string
ReadWriteOnce
PVC access mode.
persistence.existingClaim
string
""
Use an existing PVC instead of creating one.
persistence.annotations
object
{}
Annotations for the PVC.
Backup
S3 backup covers the data volume only — not MariaDB
The S3 backup CronJob archives the Castopod data volume (writable files and uploaded audio and images). It does
not back up the MariaDB database, which contains episode metadata, podcast definitions, user accounts, and
analytics data. Configure a separate MariaDB backup strategy (e.g. the HelmForge MariaDB chart backup feature) to
protect the full Castopod dataset.
Parameter
Type
Default
Description
backup.enabled
boolean
false
Enable scheduled S3 backup CronJob.
backup.schedule
string
"0 3 * * *"
Cron schedule for backups.
backup.suspend
boolean
false
Suspend the CronJob without deleting it.
backup.concurrencyPolicy
string
Forbid
CronJob concurrency policy.
backup.successfulJobsHistoryLimit
integer
3
Number of successful Job records to keep.
backup.failedJobsHistoryLimit
integer
3
Number of failed Job records to keep.
backup.backoffLimit
integer
1
Job retry limit.
backup.archivePrefix
string
castopod
Prefix for backup archive filenames.
backup.images.archiver
string
docker.io/library/busybox:1.37
Image used for tar archive.
backup.images.uploader
string
docker.io/helmforge/mc:1.0.0
Image used for S3 upload.
backup.resources
object
{}
Resources for backup containers.
backup.s3.endpoint
string
""
S3-compatible endpoint URL.
backup.s3.bucket
string
""
Target bucket name.
backup.s3.prefix
string
castopod
Key prefix within the bucket.
backup.s3.createBucketIfNotExists
boolean
true
Create the bucket automatically if it does not exist.
backup.s3.existingSecret
string
""
Existing secret containing S3 access and secret keys.
backup.s3.existingSecretAccessKeyKey
string
access-key
Key in the existing secret for the S3 access key.
backup.s3.existingSecretSecretKeyKey
string
secret-key
Key in the existing secret for the S3 secret key.
backup.s3.accessKey
string
""
Inline S3 access key (ignored when existingSecret is set).
backup.s3.secretKey
string
""
Inline S3 secret key (ignored when existingSecret is set).
Service
Parameter
Type
Default
Description
service.type
string
ClusterIP
Kubernetes service type.
service.port
integer
80
Service port exposed to the cluster.
service.annotations
object
{}
Annotations for the Service.
Ingress
Parameter
Type
Default
Description
ingress.enabled
boolean
false
Enable an Ingress resource.
ingress.ingressClassName
string
traefik
Ingress class name.
ingress.annotations
object
{}
Annotations for the Ingress (e.g. cert-manager).
ingress.hosts
array
[]
Ingress host and path rules.
ingress.tls
array
[]
TLS configuration (secret name and hosts).
Gateway API
Parameter
Type
Default
Description
gatewayAPI.enabled
boolean
false
Render Gateway API HTTPRoutes.
gatewayAPI.httpRoutes
array
[]
HTTPRoutes; each requires parentRefs.
Probes
Parameter
Type
Default
Description
probes.path
string
/healthcheck
HTTP path used by every probe.
probes.startup.enabled
boolean
true
Enable startup probe.
probes.startup.initialDelaySeconds
integer
5
Startup probe initial delay.
probes.startup.periodSeconds
integer
5
Startup probe period.
probes.startup.timeoutSeconds
integer
3
Startup probe timeout.
probes.startup.failureThreshold
integer
30
Startup probe failure threshold.
probes.liveness.enabled
boolean
true
Enable liveness probe.
probes.liveness.initialDelaySeconds
integer
0
Liveness probe initial delay.
probes.liveness.periodSeconds
integer
15
Liveness probe period.
probes.liveness.timeoutSeconds
integer
5
Liveness probe timeout.
probes.liveness.failureThreshold
integer
3
Liveness probe failure threshold.
probes.readiness.enabled
boolean
true
Enable readiness probe.
probes.readiness.initialDelaySeconds
integer
0
Readiness probe initial delay.
probes.readiness.periodSeconds
integer
10
Readiness probe period.
probes.readiness.timeoutSeconds
integer
5
Readiness probe timeout.
probes.readiness.failureThreshold
integer
3
Readiness probe failure threshold.
Resources and Security
Parameter
Type
Default
Description
resources
object
{}
CPU and memory requests and limits for the Castopod container.
podSecurityContext
object
non-root uid 33, RuntimeDefault seccomp
Pod-level security context.
securityContext
object
no escalation, drop ALL, add NET_BIND_SERVICE
Container security context for every Castopod container.
automountServiceAccountToken
boolean
false
Mount the ServiceAccount token.
Why the chart does not run the upstream s6-overlay supervisor
The image’s /init supervisor depends on a setuid helper that fails without privilege escalation. The chart runs
FrankenPHP and the scheduler as separate containers and runs the upstream bootstrap in the setup init container, so
the pod can stay non-root with no privilege escalation. The FrankenPHP binary carries the NET_BIND_SERVICE file
capability, which therefore stays in the bounding set.
Service Account
Parameter
Type
Default
Description
serviceAccount.create
boolean
false
Create a dedicated ServiceAccount.
serviceAccount.name
string
""
Override the ServiceAccount name.
serviceAccount.annotations
object
{}
Annotations for the ServiceAccount.
Scheduling
Parameter
Type
Default
Description
nodeSelector
object
{}
Node selector for scheduling.
tolerations
array
[]
Tolerations for scheduling.
affinity
object
{}
Affinity rules.
topologySpreadConstraints
array
[]
Topology spread constraints.
priorityClassName
string
""
PriorityClass for the pod.
terminationGracePeriodSeconds
integer
30
Termination grace period.
podLabels
object
{}
Extra labels for the pod.
podAnnotations
object
{}
Extra annotations for the pod.
Extra
Parameter
Type
Default
Description
extraVolumes
array
[]
Extra volumes to attach to the pod.
extraVolumeMounts
array
[]
Extra volume mounts for the container.
extraManifests
array
[]
Extra Kubernetes manifests deployed alongside the chart.
Common Issues
Episodes not playing in podcast apps after URL change
If you change castopod.baseURL after publishing episodes, all RSS feed URLs will change and existing subscribers may
lose access until their podcast app refreshes the feed. Plan your URL carefully before the first publish. If you must
change it, use a 301 redirect from the old URL to the new one.
Log in with the superadmin email
The setup init container creates the superadmin from castopod.admin.* once, when no owner exists. Log in at
/cp-auth/login with the email and the password from <release>-castopod-admin. To use the /cp-install wizard
instead, set castopod.admin.create=false and complete it before exposing the instance: the first visitor becomes
the owner.
Pod stuck in Init:0/1
The setup init container waits up to setup.databaseWaitTimeoutSeconds for MariaDB. Read
kubectl logs deploy/<release>-castopod -c setup to see whether the database is unreachable or rejects the
credentials.