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>
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):
dev→ghcr.io/brvncde-dotcom/vncmail-plus-devmain→ghcr.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).