Blocking the very first real deploy of the sandbox: base/deployment.yaml
referenced an imagePullSecrets entry ("ghcr-pull") that was never created,
which fails pod startup regardless of whether the image needs auth at
all - kubelet errors trying to resolve the named secret before it gets
anywhere near actually pulling.
Confirmed by execution (anonymous GHCR token, pull succeeded) that
ghcr.io/brvncde-dotcom/vncmail-plus-dev is public. Removing the block is
deploy/k8s/README.md's own documented alternative for exactly this case.
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:
# 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)
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 frombase/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
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):
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. |