docs(deploy): sharpen k8s admin runbook — inventory, pre-flight, ordered apply
Self-contained guide: exactly what to deploy (7 objects + image), 3 cluster values to match against bulwark, copy-paste apply order, verify, update/rollback, troubleshooting table. Plain kubectl apply (no GitOps). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a3d551b640
commit
476c76c420
+97
-56
@@ -1,88 +1,129 @@
|
|||||||
# VNCmail+ on Kubernetes (microk8s)
|
# VNCmail+ — Admin Deployment Guide (microk8s)
|
||||||
|
|
||||||
Deploys the VNCmail+ fork as a **new subdomain** (`vncmail.sandbox.vnc.de`),
|
Deploy VNCmail+ (VNC's Bulwark fork) as a container at **`vncmail.sandbox.vnc.de`**,
|
||||||
alongside the existing `bulwark.sandbox.vnc.de`. This is Bulwark's native model:
|
**alongside** the existing `bulwark.sandbox.vnc.de`. Plain `kubectl apply` — no
|
||||||
a long-lived container + persistent volumes. (Vercel was dropped — its serverless
|
GitOps needed.
|
||||||
filesystem is read-only, which crashes Bulwark's disk-backed features.)
|
|
||||||
|
|
||||||
## Image
|
> Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/
|
||||||
|
> telemetry to `/app/data`, which needs persistent volumes.
|
||||||
|
|
||||||
CI builds and pushes the image on every push to `dev`/`main`
|
---
|
||||||
(`.github/workflows/docker-publish.yml`):
|
|
||||||
|
|
||||||
- `dev` → `ghcr.io/brvncde-dotcom/vncmail-plus-dev`
|
## 1. What you are deploying
|
||||||
- `main` → `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
|
| # | Object | File | Purpose |
|
||||||
`:latest`:
|
|---|--------|------|---------|
|
||||||
```
|
| 1 | Namespace `vncmail` | `namespace.yaml` | Isolates the app |
|
||||||
kubectl -n vncmail set image deploy/vncmail-plus \
|
| 2 | 4× PersistentVolumeClaim | `pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` |
|
||||||
vncmail-plus=ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256:<digest>
|
| 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` |
|
||||||
|
|
||||||
## Prerequisites — match your cluster
|
**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.
|
||||||
|
|
||||||
These manifests use microk8s defaults; confirm they match how
|
---
|
||||||
`bulwark.sandbox.vnc.de` is deployed and edit if not:
|
|
||||||
|
|
||||||
| Thing | File | Check with |
|
## 2. Pre-flight — confirm 3 cluster values (2 min)
|
||||||
|-------|------|-----------|
|
|
||||||
| 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 —
|
The manifests use microk8s defaults. **Copy the exact values the existing
|
||||||
`kubectl get ingress -A | grep bulwark` then `kubectl get ingress <name> -n <ns> -o yaml`.
|
Bulwark uses** so VNCmail+ matches your cluster:
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Namespace
|
# 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)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deploy/k8s
|
||||||
|
|
||||||
|
# a) Namespace
|
||||||
kubectl apply -f namespace.yaml
|
kubectl apply -f namespace.yaml
|
||||||
|
|
||||||
# 2. GHCR pull secret (package is private by default).
|
# b) Image-pull secret — the GHCR package is private.
|
||||||
# Use a GitHub PAT with read:packages.
|
# Use a GitHub PAT (classic) with the read:packages scope.
|
||||||
kubectl create secret docker-registry ghcr-pull \
|
kubectl create secret docker-registry ghcr-pull \
|
||||||
--namespace vncmail \
|
--namespace vncmail \
|
||||||
--docker-server=ghcr.io \
|
--docker-server=ghcr.io \
|
||||||
--docker-username=brvncde-dotcom \
|
--docker-username=brvncde-dotcom \
|
||||||
--docker-password=<GITHUB_PAT_with_read:packages> \
|
--docker-password='<GITHUB_PAT_read:packages>' \
|
||||||
--docker-email=br@vnc.biz
|
--docker-email=br@vnc.biz
|
||||||
# (Or make the package public in GHCR and remove imagePullSecrets from deployment.yaml.)
|
|
||||||
|
|
||||||
# 3. App config secret
|
# c) App config secret — copy the template, set a real SESSION_SECRET, apply.
|
||||||
cp secret.example.yaml secret.yaml
|
cp secret.example.yaml secret.yaml
|
||||||
# edit secret.yaml → set SESSION_SECRET (openssl rand -base64 32)
|
# edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)"
|
||||||
kubectl apply -f secret.yaml
|
kubectl apply -f secret.yaml
|
||||||
|
|
||||||
# 4. Everything else
|
# d) Everything else (PVCs, Deployment, Service, Ingress)
|
||||||
kubectl apply -k . # kustomization: pvc, deployment, service, ingress
|
kubectl apply -k .
|
||||||
|
|
||||||
# 5. Watch it come up
|
|
||||||
kubectl -n vncmail rollout status deploy/vncmail-plus
|
|
||||||
kubectl -n vncmail get pods,ingress
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## DNS
|
> 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`.
|
||||||
|
|
||||||
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
|
## 4. Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sI https://vncmail.sandbox.vnc.de/api/health # expect 200
|
kubectl -n vncmail rollout status deploy/vncmail-plus # -> successfully rolled out
|
||||||
```
|
kubectl -n vncmail get pods,pvc,ingress
|
||||||
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
|
# 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
|
```bash
|
||||||
git push origin dev # CI rebuilds ghcr.io/...-dev
|
# CI rebuilds ghcr.io/brvncde-dotcom/vncmail-plus-dev on every push to `dev`.
|
||||||
kubectl -n vncmail rollout restart deploy/vncmail-plus # pull new image
|
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>
|
||||||
```
|
```
|
||||||
Promote to production the usual dev-first way (see ../../VNCMAIL-SETUP.md).
|
|
||||||
|
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`. |
|
||||||
|
|||||||
Reference in New Issue
Block a user