Mumble on Kubernetes
Production deployment for the brunner.ninja Mumble server. It runs the
official Mumble server image with trusted TLS, a MetalLB address, and the
shared PostgreSQL instance in the default namespace.
Architecture
mumblevoip/mumble-server:v1.5.915-1runs as an unprivileged user.- MetalLB advertises
192.168.0.14; TCP and UDP use port64738on the same LoadBalancer Service. brunner-ninja-tls, maintained by cert-manager, supplies the certificate for bothbrunner.ninjaand*.brunner.ninja.- A small sidecar watches the mounted certificate and asks Mumble to reload TLS
with
SIGUSR1after renewal. - Mumble stores channels, ACLs, registered users, bans, and server state in the
mumbledatabase onpostgresql.default.svc.cluster.local. /dataand the generated INI are ephemeral. No Mumble PVC is needed.- The single replica uses a
Recreatestrategy because two processes must not serve the same Mumble virtual server database concurrently.
The runtime INI is rendered into a mode-0600 in-memory volume. This avoids an
upstream container-entrypoint behavior that can print registerPassword when
configuration is supplied through environment variables.
Repository contents
mumble.yaml: LoadBalancer Service and Mumble Deployment.install.sh: administrator installation, credential generation, idempotent PostgreSQL bootstrap, and rollout verification.test.sh: local script and offline Kubernetes schema validation.ci-deployer.yaml: restricted Gitea Actions ServiceAccount, Role, binding, and revocable token request.create-ci-kubeconfig.sh: one-time CI identity bootstrap..gitea/workflows/deploy.yml: validation on every push and pull request; deployment after successful validation ofmain.
The old host-level mumble-server.ini is intentionally ignored because it
contains obsolete plaintext credentials. Do not commit kubeconfigs, database
passwords, TLS keys, or generated Kubernetes Secret values.
Prerequisites
- Kubernetes and kubectl.
- MetalLB pool
defaultcontaining the reserved address192.168.0.14. default/postgresqlService and Secret. The Secret must containpostgres-password.default/brunner-ninja-tls, maintained by the existing cert-managerletsencrypt-dnssetup.- Router forwarding of TCP and UDP
64738to192.168.0.14. On the MikroTik, the two rules usedst-address-list=WANs, which also permits LAN hairpin connections throughbrunner.ninja.
Install or restore manually
Run the first installation with an administrator kubeconfig:
./install.sh
On the first run, the script creates default/mumble-secrets with independent
256-bit random database, SuperUser, and public-registry passwords. It then
creates the PostgreSQL role and database if absent. Later runs preserve the
Secret and database, reset the role to the stored password, reapply ownership
and grants, and safely recreate or update the workload.
Rotate only the public Mumble registry password with:
ROTATE_REGISTRATION_PASSWORD=true ./install.sh
Retrieve the Mumble administrator password without placing it in Git:
kubectl get secret mumble-secrets -n default \
-o jsonpath='{.data.superuser-password}' | base64 -d; echo
Connect to brunner.ninja:64738 or mumble.brunner.ninja:64738.
Restoring a PostgreSQL backup
Stop Mumble before replacing its database:
kubectl delete deployment mumble -n default
Restore the mumble database and ensure its objects are owned by role
mumble, then run ./install.sh. The bootstrap is idempotent: it does not drop
an existing database or tables. Mumble may migrate an older compatible schema
when it starts. Its channels and registrations remain in PostgreSQL.
Validate locally
./test.sh
The test checks both shell scripts and validates the Kubernetes resources with
kubeconform without contacting the cluster. It uses a locally installed
kubeconform binary when available, otherwise the official
ghcr.io/yannh/kubeconform:v0.7.0 container, pinned by digest, through Docker.
Automatic deployment with Gitea Actions
The workflow validates every push and pull request. A successful push to
main applies mumble.yaml and polls the Deployment for up to five minutes.
It does not build a container because this repository deliberately uses the
official, version-pinned Mumble image.
CI does not invoke install.sh and cannot create application credentials or
bootstrap PostgreSQL. Its Role can call get, patch, and update only for
the existing default/mumble Service and Deployment. It cannot create or
delete workloads and has no direct Kubernetes Secrets API permission.
One-time CI setup
-
Install Mumble manually with
./install.shso the Service, Deployment, application Secret, role, and database exist. -
With an administrator kubeconfig active, run:
./create-ci-kubeconfig.sh -
Copy the single base64 output line into the Gitea repository Actions secret named
KUBE_CONFIG_BASE64. -
Protect
mainand require the validation job before merging. -
Push a harmless README change and confirm the deploy job reports a healthy rollout.
The runner networking in the workflow matches the existing brunner.ninja
runner: API URL https://host.containers.internal:6443, with TLS server name
192.168.0.2.
To revoke CI access, delete the token and remove the Gitea secret:
kubectl delete secret mumble-deployer-token -n default
Running ./create-ci-kubeconfig.sh again recreates the token and prints a new
kubeconfig.
Security boundary
The Role prevents ordinary API reads of Secrets and limits updates to two named
objects. Kubernetes RBAC cannot restrict individual fields inside a Deployment
patch, however. A malicious change merged into main could modify the Mumble
pod specification and potentially expose Secrets already referenced by that
workload. Branch protection and review of manifest and workflow changes remain
part of the security boundary. Admission policy would be required to constrain
specific pod fields further.
Gitea CI or Argo CD?
Gitea CI is the recommended deployment mechanism for this repository today. It is already proven on this cluster, the deployment is only two resources, and it does not automatically undo deliberate maintenance such as deleting the Deployment while restoring PostgreSQL.
| Capability | Gitea Actions used here | Argo CD |
|---|---|---|
| Trigger | Push to main |
Continuous Git reconciliation/webhook |
| Drift detection | No | Built-in diff and health view |
| Self-healing | No; rerun CI or install.sh |
Optional automatic reconciliation |
| Deleted Deployment | Stays deleted for maintenance | Recreated when self-heal is enabled |
| Cluster credential | Restricted token per repository | Repository credential plus central Argo controller |
| Rollout view | Action log | Application resource tree, health, sync history, and UI |
| Pruning | Not performed | Optional, with confirmation controls available |
| Secret handling | Existing Secrets remain out of Git | Still needs an external Secret strategy |
Argo CD becomes attractive when several more workloads should share one
consistent deployment model, when live drift must be visible or automatically
repaired, or when multiple clusters/environments are introduced. It can remove
per-repository Kubernetes deploy tokens because CI only needs to update Git.
Adoption should start with an AppProject, explicit namespace/resource limits,
and a maintenance procedure that disables sync before database restores.