Files
SRCmail/deploy/k8s/README.md
T
Bernd RodlerandClaude Opus 4.8 a3d551b640 feat(deploy): k8s manifests for microk8s (vncmail.sandbox.vnc.de)
Bulwark is stateful (local /app/data) — Vercel serverless (read-only fs)
crashes it. Deploy as a container with 4 persistent volumes on microk8s,
alongside bulwark.sandbox.vnc.de. Adds deploy/k8s/ (namespace, pvc, deployment,
service, ingress, secret template, runbook) + rewrites setup doc off Vercel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:09:54 +02:00

3.0 KiB

VNCmail+ on Kubernetes (microk8s)

Deploys the VNCmail+ fork as a new subdomain (vncmail.sandbox.vnc.de), alongside the existing bulwark.sandbox.vnc.de. This is Bulwark's native model: a long-lived container + persistent volumes. (Vercel was dropped — its serverless filesystem is read-only, which crashes Bulwark's disk-backed features.)

Image

CI builds and pushes the image on every push to dev/main (.github/workflows/docker-publish.yml):

  • devghcr.io/brvncde-dotcom/vncmail-plus-dev
  • mainghcr.io/brvncde-dotcom/vncmail-plus-beta
  • release tag → ghcr.io/brvncde-dotcom/vncmail-plus (clean name)

The manifests use the -dev image. For production, pin a digest instead of :latest:

kubectl -n vncmail set image deploy/vncmail-plus \
  vncmail-plus=ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256:<digest>

Prerequisites — match your cluster

These manifests use microk8s defaults; confirm they match how bulwark.sandbox.vnc.de is deployed and edit if not:

Thing File Check with
StorageClass (microk8s-hostpath) pvc.yaml kubectl get sc
IngressClass (public) ingress.yaml kubectl get ingressclass
cert-manager issuer (letsencrypt-prod) ingress.yaml kubectl get clusterissuer + bulwark's ingress

Quickest: copy bulwark's own settings — kubectl get ingress -A | grep bulwark then kubectl get ingress <name> -n <ns> -o yaml.

Deploy

# 1. Namespace
kubectl apply -f namespace.yaml

# 2. GHCR pull secret (package is private by default).
#    Use a GitHub PAT with read:packages.
kubectl create secret docker-registry ghcr-pull \
  --namespace vncmail \
  --docker-server=ghcr.io \
  --docker-username=brvncde-dotcom \
  --docker-password=<GITHUB_PAT_with_read:packages> \
  --docker-email=br@vnc.biz
#    (Or make the package public in GHCR and remove imagePullSecrets from deployment.yaml.)

# 3. App config secret
cp secret.example.yaml secret.yaml
#    edit secret.yaml → set SESSION_SECRET (openssl rand -base64 32)
kubectl apply -f secret.yaml

# 4. Everything else
kubectl apply -k .        # kustomization: pvc, deployment, service, ingress

# 5. Watch it come up
kubectl -n vncmail rollout status deploy/vncmail-plus
kubectl -n vncmail get pods,ingress

DNS

Point vncmail.sandbox.vnc.de at the same ingress load-balancer IP as bulwark.sandbox.vnc.de (A/AAAA or CNAME). cert-manager issues the TLS cert once DNS resolves.

Verify

curl -sI https://vncmail.sandbox.vnc.de/api/health     # expect 200

Then open https://vncmail.sandbox.vnc.de and log in with a full @sandbox.vnc.de address (e.g. bernd.rodler@sandbox.vnc.de) — Stalwart authenticates the full email, not a bare username.

Update after a UI change

git push origin dev                 # CI rebuilds ghcr.io/...-dev
kubectl -n vncmail rollout restart deploy/vncmail-plus   # pull new image

Promote to production the usual dev-first way (see ../../VNCMAIL-SETUP.md).