Add builtin-vnclagoon theme (cyan #00D4FF accent on navy #0A0E1A, DM Sans body + Syne headings, self-hosted OFL fonts); set as default theme policy; default mode dark. Add VNCmail wordmark SVGs (on-dark/on-light) + wire logo/company via k8s secret template. Placeholder wordmark — swap official styleguide SVG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VNCmail+ — Admin Deployment Guide (microk8s)
Deploy VNCmail+ (VNC's Bulwark fork) as a container at vncmail.sandbox.vnc.de,
alongside the existing bulwark.sandbox.vnc.de. Plain kubectl apply — no
GitOps needed.
Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/ telemetry to
/app/data, which needs persistent volumes.
1. What you are deploying
| # | Object | File | Purpose |
|---|---|---|---|
| 1 | Namespace vncmail |
namespace.yaml |
Isolates the app |
| 2 | 4× PersistentVolumeClaim | pvc.yaml |
/app/data/{settings,admin,admin-state,telemetry} |
| 3 | Secret vncmail-env |
secret.yaml (you create it) |
App config (JMAP URL, session secret, branding) |
| 4 | Secret ghcr-pull |
(you create it — command below) | Pull the private image from GHCR |
| 5 | Deployment vncmail-plus |
deployment.yaml |
The app pod |
| 6 | Service vncmail-plus |
service.yaml |
ClusterIP :80 → pod :3000 |
| 7 | Ingress vncmail-plus |
ingress.yaml |
TLS host vncmail.sandbox.vnc.de |
Image: ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest
(built automatically by CI from the dev branch). For anything beyond the
sandbox, pin a digest — see §5.
2. Pre-flight — confirm 3 cluster values (2 min)
The manifests use microk8s defaults. Copy the exact values the existing Bulwark uses so VNCmail+ matches your cluster:
# Find bulwark's ingress and read off its class + cert-manager annotations:
kubectl get ingress -A | grep -i bulwark
kubectl get ingress <bulwark-ingress-name> -n <bulwark-ns> -o yaml
# List available storage classes and ingress classes:
kubectl get sc
kubectl get ingressclass
kubectl get clusterissuer # cert-manager issuers (if used)
Then edit if they differ from the defaults below:
| Value | Default in manifests | File to edit |
|---|---|---|
| StorageClass | microk8s-hostpath |
pvc.yaml (all 4) |
| IngressClass | public |
ingress.yaml |
| cert-manager issuer | letsencrypt-prod |
ingress.yaml |
3. Deploy (copy-paste, in order)
cd deploy/k8s
# a) Namespace
kubectl apply -f namespace.yaml
# b) Image-pull secret — the GHCR package is private.
# Use a GitHub PAT (classic) with the read:packages scope.
kubectl create secret docker-registry ghcr-pull \
--namespace vncmail \
--docker-server=ghcr.io \
--docker-username=brvncde-dotcom \
--docker-password='<GITHUB_PAT_read:packages>' \
--docker-email=br@vnc.biz
# c) App config secret — copy the template, set a real SESSION_SECRET, apply.
cp secret.example.yaml secret.yaml
# edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)"
kubectl apply -f secret.yaml
# d) Everything else (PVCs, Deployment, Service, Ingress)
kubectl apply -k .
Alternative to (b): make the GHCR package public (GitHub → Packages → vncmail-plus-dev → Package settings → Change visibility), then delete the
imagePullSecrets:block fromdeployment.yaml.
4. Verify
kubectl -n vncmail rollout status deploy/vncmail-plus # -> successfully rolled out
kubectl -n vncmail get pods,pvc,ingress
# DNS: point vncmail.sandbox.vnc.de at the same ingress IP as bulwark.sandbox.vnc.de.
# cert-manager issues TLS once DNS resolves. Then:
curl -sI https://vncmail.sandbox.vnc.de/api/health # -> HTTP/2 200
Open https://vncmail.sandbox.vnc.de and log in with a full email address
(e.g. bernd.rodler@sandbox.vnc.de) — Stalwart authenticates the full email, not
a bare username.
5. Update to a new build
# CI rebuilds ghcr.io/brvncde-dotcom/vncmail-plus-dev on every push to `dev`.
kubectl -n vncmail rollout restart deploy/vncmail-plus # pulls :latest (imagePullPolicy: Always)
# Production: pin a digest instead of :latest so rollouts are deterministic.
kubectl -n vncmail set image deploy/vncmail-plus \
vncmail-plus=ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256:<digest>
Rollback: kubectl -n vncmail rollout undo deploy/vncmail-plus
6. Troubleshooting
| Symptom | Cause / fix |
|---|---|
Pod ImagePullBackOff |
ghcr-pull secret missing/expired, or package still private. Recreate the secret (§3b) or make the package public. |
Pod CrashLoopBackOff, logs show EACCES/permission on /app/data |
Volume not writable by uid 1001. securityContext.fsGroup: 1001 is set in deployment.yaml — keep it; some storage drivers also need it on the PVC. |
PVC stuck Pending |
Wrong storageClassName in pvc.yaml. Set it to one from kubectl get sc. |
| Ingress has no address / no cert | Wrong ingressClassName or cert issuer. Match bulwark's (§2). Check kubectl -n vncmail describe ingress vncmail-plus. |
| Login shows "Ein Fehler ist aufgetreten" | Use the full email (user@sandbox.vnc.de), not a bare username. |
| Can't reach Stalwart | Check JMAP_SERVER_URL in the secret = https://stalwart.sandbox.vnc.de. |