# 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`.

> Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/
> telemetry to `/app/data`, which needs persistent volumes.

## Structure — base + overlays

```
deploy/k8s/
  base/               # shared manifest shapes (namespace-agnostic)
  overlays/
    dev/              # the live sandbox — vncmail.sandbox.vnc.de, namespace vncmail
    prod/             # scaffolded, NOT YET LIVE — see "Production status" below
  ca/                 # separate, isolated EJBCA internal CA — see ca/README.md.
                       # Never composed with base/ or either overlay above.
```

`kubectl apply -k overlays/dev` (or `overlays/prod`, once real) instead of
applying `base/` directly — `base/` alone has no namespace and won't apply
meaningfully on its own.

## Routine deploys go through CI + ArgoCD now

As of the GitLab CI/CD pipeline (`.gitlab-ci.yml`, see `../../VNCMAIL-SETUP.md`
§ CI/CD), **pushing to `dev` auto-builds and bumps the deploy tag; ArgoCD's
`vncmail-dev` Application applies it** — you should not normally need to run
`kubectl apply` for the sandbox by hand anymore, and CI never touches the
cluster directly (it only ever talks to the registry and to this git repo).
This guide's manual steps below are for first-time setup, the one-time
secret creation CI/ArgoCD deliberately never automate, and troubleshooting.

## Production status

**There is no production VNCmail+ deployment yet.** `overlays/prod/` exists
in the repo but is inert: its ingress hostname and its secret's
`JMAP_SERVER_URL` are both obvious placeholders (`vncmail.CHANGEME.invalid` /
`https://REPLACE-ME-prod-stalwart-not-yet-deployed.invalid`) that will fail
loudly rather than silently deploy against the wrong backend. Applying it
requires, in order: a real prod Stalwart instance to exist, a real hostname
decision, DNS, a real `secret.yaml`, and the `.gitlab-ci.yml` `promote` job's
`kubectl apply` step (currently a TODO placeholder) filled in. None of that
is CI's job to decide — it's an explicit, human-triggered event.

---

## 1. What you are deploying (per overlay)

| # | Object | File | Purpose |
|---|--------|------|---------|
| 1 | Namespace | `overlays/<env>/namespace.yaml` | Isolates the app (`vncmail` for dev, `vncmail-prod` for prod) |
| 2 | 4× PersistentVolumeClaim | `base/pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` |
| 3 | Secret `vncmail-env` | `overlays/<env>/secret.yaml` *(you create it)* | App config (JMAP URL, session secret, branding) |
| 4 | Image-pull secret | *(you create it — command below)* | Pull the (currently private) image |
| 5 | Deployment `vncmail-plus` | `base/deployment.yaml` (+ overlay patches) | The app pod |
| 6 | Service `vncmail-plus` | `base/service.yaml` | ClusterIP :80 → pod :3000 |
| 7 | Ingress `vncmail-plus` | `base/ingress.yaml` (+ overlay patches for prod) | TLS host |

**Image:** CI builds and pushes to `registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus`
(tag `sha-<sha>` per deploy, moving pointers `dev-latest`/`prod-latest`). The
`ghcr.io/brvncde-dotcom/vncmail-plus-dev` image referenced in `base/deployment.yaml`
is a legacy default only — CI overrides it per-deploy via `kubectl set image`,
so what's committed there never needs to track what's actually running.

---

## 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 <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 (in `base/`, so both overlays
pick up the fix):

| Value | Default in manifests | File to edit |
|-------|----------------------|--------------|
| StorageClass | `microk8s-hostpath` | `base/pvc.yaml` (all 4) |
| IngressClass | `public` | `base/ingress.yaml` |
| cert-manager issuer | `letsencrypt-prod` | `base/ingress.yaml` |

---

## 3. First-time setup (one-time, per environment — CI never does this)

```bash
cd deploy/k8s/overlays/dev     # or overlays/prod, once real

# a) Image-pull secret — the registry package is private.
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
#    Once CI has cut over to registry.gitlab.vnc.biz, this becomes a
#    docker-registry secret for that registry instead — see VNCMAIL-SETUP.md.

# b) 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

# c) Everything else (namespace, PVCs, Deployment, Service, Ingress)
kubectl apply -k .
```

> Alternative to (a): make the registry package public, then delete the
> `imagePullSecrets:` block from `base/deployment.yaml`.

After this one-time setup, routine deploys to `dev` happen automatically via
CI on every push — see "Routine deploys go through CI now" above. This
section is for first-time bring-up (or `overlays/prod`, once it's real) and
troubleshooting, not the everyday path.

---

## 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

Normally you don't — CI's `bump-dev` job + ArgoCD's automated sync do this
on every push to `dev`. To do it by hand (e.g. troubleshooting, before
automated sync is turned on):

```bash
kubectl -n vncmail set image deploy/vncmail-plus \
  vncmail-plus=registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus:sha-<sha>
```

ArgoCD will overwrite this on its next sync unless you also update
`deploy/k8s/overlays/dev/image-tag/kustomization.yaml` to match — that file
is CI-owned (see its header comment), so a by-hand `set image` is only ever
a temporary override, not a real fix.

Rollback (bypassing ArgoCD temporarily): `kubectl -n vncmail rollout undo deploy/vncmail-plus`.
The real rollback is reverting the commit that bumped the tag and letting
ArgoCD re-sync.

---

## 6. Troubleshooting

| Symptom | Cause / fix |
|---------|-------------|
| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3a) 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 `base/deployment.yaml` — keep it; some storage drivers also need it on the PVC. |
| PVC stuck `Pending` | Wrong `storageClassName` in `base/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`. |
