# 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: ```bash # Find bulwark's ingress and read off its class + cert-manager annotations: kubectl get ingress -A | grep -i bulwark kubectl get ingress -n -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) ```bash 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='' \ --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 from `deployment.yaml`. --- ## 4. Verify ```bash 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 ```bash # 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: ``` 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`. |