Web UI (kopia server)¶
Kopia ships a built-in web UI — an HTML view of a repository's snapshots,
policies, sources, and tasks. Kopiur exposes it declaratively: set spec.server
on a Repository (or ClusterRepository) and the operator runs kopia server
start in a Deployment and puts a Service in front of it. There is no
enabled bool — the presence of the spec.server block is what turns it on,
and removing the block tears everything back down.
Kopiur creates the workload and the Service only. Routing the Service to the
outside world (an Ingress/HTTPRoute) is yours to wire — see
Exposing the Service.
When would you use this?¶
The UI is an interactive surface for a human. Reach for it when you want to:
- Browse and verify snapshots, policies, and sources visually, without the kubectl plugin.
- Restore ad hoc through the UI — pick a snapshot, mount it, pull a file.
- Give an operator a point-and-click view of a repository's contents — ideally read-only, so browsing can't accidentally delete a backup.
You do not need it for normal operation. Scheduled backups, restores, and maintenance all run headless in short-lived mover Jobs — the UI is never on that path. Because it is a long-lived pod that holds the repository decryption key (see the warning below), only run it where you actually want interactive access, and tear it down when you're done.
The UI holds the decryption key
By default the UI is full read/write/delete, and the server pod always
holds the repository decryption key — even in read-only mode.
Setting readOnly blocks mutation but not reading: anyone who
can reach the UI can still read and restore every backup. So treat exposing the UI
exactly like exposing the repository itself: keep it ClusterIP (the default),
put authentication in front of it, and restrict who can reach the Service with a
NetworkPolicy.
The spec.server surface¶
| Field | Type | Default | What it does |
|---|---|---|---|
auth |
externally-tagged enum (generate | secretRef | insecure) |
generate |
UI login. Omitted ⇒ operator-generated credentials. Never defaults to no-auth. |
readOnly |
bool | false |
Read-only UI — connect the repository read-only so the UI cannot create/delete/alter backups (browse + restore only). Forced on when the Repository has spec.mode: ReadOnly. |
service.type |
enum(ClusterIP|NodePort|LoadBalancer) |
ClusterIP |
How the Service is exposed. Routing outside the cluster is your job. |
service.port |
int | 51515 |
Listen + Service port. |
service.annotations |
map | — | Applied to the Service — the seam for your ingress/LB controller. |
resources |
ResourceRequirements | — | Requests/limits for the server pod. |
securityContext |
SecurityContext | hardened default | Override the default hardened container security context. |
namespace |
string | — | ClusterRepository only, required — which namespace the server objects land in (a cluster-scoped owner has no implicit namespace). |
There is no enabled field: presence of spec.server is "on", absence is
"off". See the full field reference.
How to deploy it¶
spec.server is just a field on a Repository, so it deploys like any other CRD
edit — kubectl apply, or through GitOps (Flux/Argo). The smallest
form adds the block to a repository and takes the safe defaults (operator-minted
credentials, ClusterIP):
# Smallest web-UI form on a namespaced Repository (docs/server.md)
#
# Adding a `spec.server` block turns the kopia web UI on (there is no `enabled`
# bool — presence is "on", absence is "off"). This is the minimal form: take the
# safe defaults (operator-minted credentials, ClusterIP Service, port 51515).
#
# The Secret named here must already exist in this namespace (the repository's
# backend auth + encryption password). For a complete server example with
# HTTPRoute/NetworkPolicy templates see deploy/examples/25-repository-server-ui.yaml.
#
# Field shapes verified against crates/api: externally-tagged backend
# (`backend.s3`), and `server.auth.generate` (externally-tagged auth enum).
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas-primary
namespace: apps
spec:
backend:
s3:
{
bucket: my-backups,
endpoint: s3.amazonaws.com,
region: us-east-1,
auth: { secretRef: { name: nas-primary-creds } },
}
encryption:
passwordSecretRef: { name: nas-primary-creds, key: KOPIA_PASSWORD }
server:
auth: { generate: {} } # operator mints UI credentials into an owned Secret
Apply it like any other manifest — kubectl apply -f, or through GitOps.
What the operator creates for you¶
Once the repository is Ready, the controller materializes — all named
<repo>-kopia-ui and labeled app.kubernetes.io/name=kopiur-server,
app.kubernetes.io/instance=<repo>:
| Object | Name | Purpose |
|---|---|---|
Deployment |
<repo>-kopia-ui |
Runs kopia server start (1 replica, Recreate strategy, mover image, TCP readiness/liveness probes). |
Service |
<repo>-kopia-ui |
Fronts the Deployment on the configured port. |
ConfigMap |
<repo>-kopia-ui |
The server's work spec (which repo, port, auth mode). |
Secret |
<repo>-kopia-ui-auth |
generate mode only — the minted UI credentials (username/password). |
$ kubectl get deploy,svc,cm,secret -n apps \
-l app.kubernetes.io/name=kopiur-server,app.kubernetes.io/instance=nas-primary
The controller manages Deployments/Services/ConfigMaps/Secrets for this
feature. For a namespaced Repository the objects carry an ownerReference to
the repository; a ClusterRepository cleans them up via a finalizer instead (see
ClusterRepository).
The server needs features.kopiaUi.enabled in the chart
Writing the generated-auth Secret (and, for a ClusterRepository, the
cross-namespace credentials mirror) needs cluster-wide secrets
create/patch/delete — off by default (least privilege). Set
features.kopiaUi.enabled: true in the Helm chart. Without it the repository's
.status surfaces an actionable 403 naming the flag. See
Feature permissions.
The server runs without in-pod TLS
The operator starts kopia with --insecure — i.e. plain HTTP inside the pod.
That is deliberate: TLS termination belongs at your ingress/load balancer, not in
the server pod. The credentials still protect the UI; just don't expose the raw
Service to an untrusted network without TLS in front.
Try it end-to-end¶
Turn on the UI from a clean slate and prove it answers — 200 with auth, 401 without — without leaving the cluster. One apply-ready bundle, deploy/examples/tryit/server-ui.yaml: the apps Namespace, the backend Secret, and a Repository nas-primary with the minimal spec.server block.
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas-primary
namespace: apps
spec:
backend:
s3:
bucket: my-kopia-bucket
endpoint: s3.us-east-1.amazonaws.com
region: us-east-1
auth:
secretRef:
name: nas-primary-creds
encryption:
passwordSecretRef:
name: nas-primary-creds
key: KOPIA_PASSWORD
create:
enabled: true
# ── Turn on the web UI ────────────────────────────────────────────────────────
# Presence of `spec.server` is "on" (there is no `enabled` bool). The minimal
# form takes the safe defaults: operator-minted credentials into an owned
# Secret, a ClusterIP Service, port 51515.
server:
auth: { generate: {} }
1. Fill in the credentials (AWS_* + KOPIA_PASSWORD) in the secret section, then apply the bundle and wait for the repository to be Ready — the server objects materialize only once it is:
$ kubectl apply -f deploy/examples/tryit/server-ui.yaml
$ kubectl -n apps wait --for=condition=Ready repository/nas-primary --timeout=2m
2. Confirm the operator created the server objects (deep). All named nas-primary-kopia-ui* and labeled for the instance:
$ kubectl -n apps get deploy,svc,secret \
-l app.kubernetes.io/name=kopiur-server,app.kubernetes.io/instance=nas-primary
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nas-primary-kopia-ui 1/1 1 1 40s
NAME TYPE CLUSTER-IP PORT(S) AGE
service/nas-primary-kopia-ui ClusterIP 10.96.12.34 51515/TCP 40s
NAME TYPE DATA AGE
secret/nas-primary-kopia-ui-auth Opaque 2 40s
A 1/1 Deployment, a Service on 51515, and the nas-primary-kopia-ui-auth Secret (keys username/password) means the UI is up.
3. Read the minted credentials from that Secret:
$ kubectl -n apps get secret nas-primary-kopia-ui-auth \
-o jsonpath='{.data.username}' | base64 -d; echo
kopia
$ kubectl -n apps get secret nas-primary-kopia-ui-auth \
-o jsonpath='{.data.password}' | base64 -d; echo
<illustrative — your minted password>
4. Prove the UI answers (deep). Port-forward the Service and curl it — 200 with the credentials, 401 without:
$ kubectl -n apps port-forward svc/nas-primary-kopia-ui 51515:51515 &
# with the credentials → 200:
$ curl -su 'kopia:<password>' http://localhost:51515/ -o /dev/null -w '%{http_code}\n'
200
# without them → 401 (the UI never defaults to no-auth):
$ curl -s http://localhost:51515/ -o /dev/null -w '%{http_code}\n'
401
The server speaks plain HTTP inside the pod (the operator starts kopia with --insecure); TLS belongs at your ingress/LB, never the raw Service.
Illustrative output
The CLUSTER-IP, the minted password, and the AGEs vary per run — the load-bearing facts are the 1/1 nas-primary-kopia-ui Deployment, port 51515, the nas-primary-kopia-ui-auth Secret, and 200/401.
Tear it down by removing the spec.server block and re-applying — the operator deletes the Deployment, Service, ConfigMap, and the generated Secret it owns:
Authentication¶
spec.server.auth is an externally-tagged enum — you set exactly one of three
keys. It defaults to generate (never to no-auth).
| Mode | Shape | When to use |
|---|---|---|
generate (default) |
generate: { username? } |
Let the operator mint a random password. The simplest safe choice. |
secretRef |
secretRef: { name, usernameKey, passwordKey } |
You manage the UI credentials yourself (e.g. a shared/SSO-fronted password). |
insecure |
insecure: { acknowledgeInsecure: true } |
No login at all. A footgun; for throwaway/lab use only. |
generate — operator-minted credentials (recommended)¶
The operator creates a Secret <repo>-kopia-ui-auth once (keys username,
password), pins its reference to status.server.generatedSecretRef, and
never rotates it on later reconciles. The username defaults to kopia; set
generate: { username: alice } to change it. Read the password with:
$ kubectl get secret nas-primary-kopia-ui-auth -n apps \
-o jsonpath='{.data.password}' | base64 -d; echo
secretRef — bring your own credentials¶
Point at a Secret you own; all three keys are required:
insecure — no authentication¶
Disables the UI login entirely. It demands an explicit acknowledgement, so you can't reach it by accident:
insecure exposes the whole repository with no login
With insecure, anyone who can reach the Service has full read/write/delete
of every backup. The admission webhook rejects the mode unless you set
acknowledgeInsecure: true. Only use it on an isolated network you fully trust,
and pair it with a NetworkPolicy.
Read-only UI¶
Set spec.server.readOnly: true and the operator connects the server's repository
read-only (kopia repository connect --readonly) before starting the UI. Every
operation on that connection — and so everything the UI does — is then unable to
mutate the repository: creating, deleting, or altering snapshots/policies/
maintenance is rejected. It's the right default for a point-and-click browse and
restore surface where you never want a stray click to delete a backup.
server:
readOnly: true # the UI cannot mutate the repository (browse + restore only)
auth: { generate: {} }
The effective read-only state is spec.mode: ReadOnly OR
spec.server.readOnly: true:
- A
Repositorywithspec.mode: ReadOnlyalready serves restores only — its UI is forced read-only and you don't need the field. Setting an explicitreadOnly: falseon such a repository is rejected by the webhook (a read-only repository can't serve a writable UI). - A normal
ReadWriterepository (still taking backups via movers) gets a read-only UI by opting in withreadOnly: true.
The reconciler pins the resolved value to status.server.readOnly. A complete,
apply-ready read-only Repository (a ReadWrite repo with a read-only UI):
# Example 26 — A read-only Kopia web UI (`spec.server.readOnly`)
#
# Same as example 25, but the server connects the repository READ-ONLY: the
# operator runs `kopia repository connect --readonly` before `kopia server start`,
# so creating, deleting, or altering backups through the UI is rejected by the
# repository. This is the safe choice when you want humans to BROWSE and RESTORE
# through the UI but never mutate backups.
#
# ┌─ WHAT read-only DOES AND DOESN'T DO ───────────────────────────────────────┐
# │ DOES: block create / delete / alter of snapshots, policies, maintenance │
# │ through the UI (the repository connection rejects every write). │
# │ DOESN'T: hide data — the server pod still holds the DECRYPTION KEY, so │
# │ anyone who reaches the UI can still READ and RESTORE every backup. │
# │ Read-only is not confidentiality: keep auth on and ClusterIP. │
# │ NOTE: kopia's UI does not grey out the (now non-functional) write/delete │
# │ buttons; the actions simply fail at the backend. │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Effective read-only is `spec.mode: ReadOnly` OR `spec.server.readOnly: true`.
# A `Repository` with `spec.mode: ReadOnly` already forces the UI read-only (you
# don't need the field); use `server.readOnly: true` to get a read-only UI on a
# normal ReadWrite repository that still takes backups via movers. The effective
# value is pinned to `status.server.readOnly`.
#
# Verified against crates/api: `server.readOnly` is `Option<bool>` on ServerSpec.
---
apiVersion: v1
kind: Secret
metadata:
name: nas-primary-creds
namespace: apps
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "REPLACE_ME"
AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas-primary
namespace: apps
spec:
# This repository still takes backups normally (mode defaults to ReadWrite);
# only the *UI* is read-only, via server.readOnly below.
backend:
s3:
bucket: my-backups
endpoint: s3.us-east-1.amazonaws.com
region: us-east-1
auth:
secretRef:
name: nas-primary-creds
encryption:
passwordSecretRef:
name: nas-primary-creds
key: KOPIA_PASSWORD
server:
readOnly: true # the UI cannot mutate the repository (browse + restore only)
auth:
generate:
username: kopia
service:
type: ClusterIP
port: 51515
annotations: {}
Read-only blocks mutation, not reading
readOnly stops the UI from changing backups; it does not make the UI
confidential. The server pod still holds the repository decryption key, so
anyone who can reach the UI can still read and restore every backup. Keep auth
on and the Service ClusterIP regardless. Note too that kopia's UI does not grey
out the (now non-functional) write/delete buttons — the actions simply fail at the
backend.
Why a connection-level flag (not a server flag)
kopia 0.23 — the version Kopiur ships — has no kopia server start --readonly
flag; that landed later upstream. Kopiur achieves the same guarantee with the
read-only connection, whose read-only bit every later operation inherits. One
side effect: kopia may log occasional errors if its internal scheduler probes for
maintenance on a read-only connection. They're harmless (nothing can be written).
Exposing the Service¶
spec.server.service controls the Service; port defaults to 51515.
service.type |
Reach it from | Notes |
|---|---|---|
ClusterIP (default) |
inside the cluster | Use kubectl port-forward or your own ingress. The safe default. |
NodePort |
each node's IP | A static high port on every node. |
LoadBalancer |
an external IP | Provisioned by your cloud/LB controller. |
Kopiur creates the Service only — it never creates an Ingress or
HTTPRoute. Point your own router at Service <repo>-kopia-ui on the
configured port, and put TLS + (ideally) an additional auth layer there. The
full example carries commented HTTPRoute and NetworkPolicy
templates you can adapt. Use service.annotations to feed your ingress/LB
controller (e.g. an external-dns hostname or an LB class).
Accessing the UI¶
For a quick look, port-forward the Service and open it locally:
$ kubectl port-forward -n apps svc/nas-primary-kopia-ui 51515:51515
# then browse http://localhost:51515 and log in with the credentials above
For ongoing access, route an Ingress/HTTPRoute to the Service (with TLS),
and strongly consider a NetworkPolicy restricting who may reach it.
ClusterRepository server¶
A ClusterRepository is cluster-scoped and has no implicit namespace, so its
spec.server block requires a namespace (the fields are otherwise
identical, flattened in):
# Web UI on a cluster-scoped ClusterRepository (docs/server.md)
#
# A ClusterRepository has no implicit namespace, so its `spec.server` block
# REQUIRES `namespace` — which namespace the server Deployment/Service/ConfigMap
# (and any minted credential Secret) land in. The other server fields are
# identical to the namespaced Repository form, flattened in.
#
# Because a cluster-scoped object can't own namespaced children via an
# ownerReference, the controller tracks and cleans up the server objects with a
# finalizer + labels. If the repo credentials Secret lives in a different
# namespace than the server, the operator mirrors it next to the server pod
# (envFrom can't cross namespaces).
#
# Requires installScope=cluster. Field shapes verified against crates/api:
# externally-tagged `server.auth.generate`; `server.namespace` required here.
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: ClusterRepository
metadata:
name: platform
spec:
# backend / encryption / allowedNamespaces …
backend:
s3:
bucket: org-kopia-repo
endpoint: s3.us-east-1.amazonaws.com
region: us-east-1
auth:
secretRef:
name: platform-creds
namespace: kopiur-system # explicit ns required for cluster scope
encryption:
passwordSecretRef:
name: platform-creds
namespace: kopiur-system
key: KOPIA_PASSWORD
allowedNamespaces:
all: true
server:
namespace: kopiur-system # required: where the server objects land
auth: { generate: {} }
service: { type: ClusterIP, port: 51515 }
Because a cluster-scoped object can't own namespaced children via an
ownerReference, the controller tracks and cleans up the server objects with a
finalizer + labels instead. If the repository credentials Secret lives in a
different namespace than the server, the operator mirrors it next to the server
pod (envFrom can't cross namespaces). Changing server.namespace moves the
server: the operator deletes the objects in the old namespace and recreates them
in the new one (it tracks the last-applied namespace in status.server.namespace).
Filesystem backends require ReadWriteMany¶
For an object-store backend (S3, Azure, GCS, B2, …) the server connects over the network — no volume constraint. For a filesystem backend the server pod must mount the repository volume, and it is long-lived:
A filesystem-backed server needs a ReadWriteMany repo PVC
A long-lived server holding a ReadWriteOnce repo PVC would block every
backup/restore/maintenance mover that needs the same volume. The operator
therefore requires the repository PVC to be ReadWriteMany when spec.server
is set on a filesystem Repository, and rejects the reconcile otherwise. Use an
RWX-capable StorageClass (or an inline NFS export) for the repository volume, or
keep the UI on an object-store repository.
Server permissions on an NFS-backed repo¶
Like a mover, the server pod mounts the filesystem repo read-write and writes
to the backend — so it must be able to write the export. The server gets the same
hardened pod defaults as movers (fsGroup: 65532), but fsGroup is a no-op on
NFS. If the export is owned by a dedicated UID/GID, give the server the shared
group via spec.server.podSecurityContext (mirrors moverDefaults.podSecurityContext):
spec:
server:
podSecurityContext:
supplementalGroups: [3001] # the export's group; matches moverDefaults
moverDefaults:
podSecurityContext:
supplementalGroups: [3001]
Without it, the server CrashLoops on startup (it can't read/write the repo). See
Security context → NFS filesystem repositories.
The container-level spec.server.securityContext overrides the hardened
container context (e.g. runAsUser) independently.
Inspecting status¶
The reconciler pins a status.server block (it never stores a password):
| Field | Meaning |
|---|---|
endpoint |
In-cluster address, <service>.<namespace>.svc:<port>. |
namespace |
Namespace the server objects were last applied to (used to detect a namespace change). |
authMode |
Resolved auth discriminant — Generate / SecretRef / Insecure. |
readOnly |
Effective read-only state — true when spec.mode: ReadOnly or spec.server.readOnly: true. |
generatedSecretRef |
generate mode only — the operator-owned Secret holding the UI credentials. |
When the server is disabled, status.server is cleared to null.
Disabling it¶
Delete the spec.server block from the Repository manifest and re-apply it.
The operator deletes the Deployment, Service, ConfigMap, and any generated Secret
it owns:
Quick imperative form
To tear it down without editing the manifest, patch spec.server to null directly:
Re-apply your manifest afterward so your source of truth (especially under GitOps) doesn't put it back.
Full example¶
A complete, apply-ready Repository with spec.server (S3 backend, generate
auth, plus commented HTTPRoute + NetworkPolicy templates):
# Example 25 — Browse a repository with the Kopia web UI (`spec.server`)
#
# Setting `spec.server` on a Repository (or ClusterRepository) makes the operator
# run `kopia server start` in a Deployment and expose it via a Service, so you can
# browse snapshots/policies through Kopia's built-in HTML UI. Only a Service is
# created — wiring an Ingress/HTTPRoute is left to you (commented below).
#
# ┌─ SECURITY ─────────────────────────────────────────────────────────────────┐
# │ By default the UI is full read-write-DELETE. Anyone who can reach it can │
# │ READ, CREATE, and DELETE backups, and the server pod holds the repository │
# │ DECRYPTION KEY. Set `server.readOnly: true` (or `spec.mode: ReadOnly` on the │
# │ Repository) for a no-mutation UI — see example 26. Read-only still exposes │
# │ READING all backups, so keep auth + ClusterIP regardless. Defaults here are │
# │ the safe ones: │
# │ * `auth: { generate: {} }` — the operator mints random UI credentials into │
# │ an owned Secret (`<repo>-kopia-ui-auth`) and pins the ref to │
# │ `status.server.generatedSecretRef`. Read them with: │
# │ kubectl -n apps get secret nas-primary-kopia-ui-auth \ │
# │ -o jsonpath='{.data.password}' | base64 -d │
# │ * `service.type: ClusterIP` — not exposed outside the cluster by default. │
# │ The no-auth `insecure` mode exists but requires an explicit │
# │ `acknowledgeInsecure: true` (webhook-rejected otherwise). Strongly consider │
# │ a NetworkPolicy restricting who may reach the Service. │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Filesystem backends: a long-lived server holds the repo PVC, so it would block
# backup/restore movers on a ReadWriteOnce volume — the operator requires the repo
# PVC to be ReadWriteMany when `spec.server` is set on a filesystem Repository.
# Object-store backends (shown here) connect over the network and have no such
# constraint.
---
apiVersion: v1
kind: Secret
metadata:
name: nas-primary-creds
namespace: apps
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "REPLACE_ME"
AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas-primary
namespace: apps
spec:
backend:
s3:
bucket: my-backups
endpoint: s3.us-east-1.amazonaws.com
region: us-east-1
auth:
secretRef:
name: nas-primary-creds
encryption:
passwordSecretRef:
name: nas-primary-creds
key: KOPIA_PASSWORD
# The web UI server. Presence of this block enables it.
server:
# Default (and recommended): operator-generated credentials.
auth:
generate:
username: kopia # optional; defaults to "kopia"
# Alternatives:
# auth: { secretRef: { name: my-ui-creds, usernameKey: username, passwordKey: password } }
# auth: { insecure: { acknowledgeInsecure: true } } # NO auth — footgun
# readOnly: true # no-mutation UI (browse + restore only) — see example 26
service:
type: ClusterIP # ClusterIP (default) | NodePort | LoadBalancer
port: 51515
annotations: {} # the seam for your ingress/LB controller
# resources: { requests: { cpu: 25m, memory: 64Mi } }
# securityContext: {}
---
# Bring-your-own routing (uncommented as an illustration). Kopiur never creates an
# Ingress/HTTPRoute — point your own at the Service `nas-primary-kopia-ui:51515`.
#
# apiVersion: gateway.networking.k8s.io/v1
# kind: HTTPRoute
# metadata:
# name: nas-primary-kopia-ui
# namespace: apps
# spec:
# parentRefs:
# - name: internal
# namespace: gateway
# hostnames: ["kopia.example.internal"]
# rules:
# - backendRefs:
# - name: nas-primary-kopia-ui
# port: 51515
#
# And a NetworkPolicy is strongly recommended to restrict who can reach the Service:
#
# apiVersion: networking.k8s.io/v1
# kind: NetworkPolicy
# metadata:
# name: nas-primary-kopia-ui
# namespace: apps
# spec:
# podSelector:
# matchLabels:
# app.kubernetes.io/name: kopiur-server
# app.kubernetes.io/instance: nas-primary
# ingress:
# - from:
# - namespaceSelector:
# matchLabels: { kubernetes.io/metadata.name: gateway }
See also¶
- Repositories & backends — the
Repository/ClusterRepositorysurface this feature sits on. - Security context — the hardened default the server pod runs under, and how to override it.
- Installation — install scope and the RBAC the controller needs to manage the server objects.
- GitOps (Flux / Argo) — deploying the field through a GitOps pipeline.
deploy/examples/25-repository-server-ui.yaml— the apply-ready example above.deploy/examples/26-repository-server-ui-readonly.yaml— the read-only-UI variant.