web and worker images, plus a migration Job that runs before each install and upgrade. Postgres, Redis, and object storage are yours to provide.
Requirements
- Kubernetes 1.23+, any distribution.
- Helm 3.8+, and cosign to verify chart signatures.
- Postgres, a Redis-compatible instance, and an S3-compatible bucket reachable from the cluster. Run them in-cluster with their own charts (Valkey, RustFS, CloudNativePG) or use managed services.
- An ingress controller, to reach the dashboard from outside the cluster. Traefik is the tested default. Avoid ingress-nginx, which is retired and unmaintained.
Install
--version 1.4.2 installs app 1.4.2. Pin it explicitly, there is no floating tag. Published versions are listed on the GHCR package page.
Verify a chart before installing it:
helm test ovr checks the web readiness endpoint through its Service.
Required values
Set
existingSecret to supply all of these from a Secret you manage (Sealed Secrets, External Secrets, or by hand) instead. It needs the keys DATABASE_URL, REDIS_URL, BETTER_AUTH_SECRET, OVR_GIT_TOKEN_ENCRYPTION_KEY, STORAGE_ACCESS_KEY, and STORAGE_SECRET_KEY.
Object storage
Any S3-compatible service works: AWS S3, Cloudflare R2, RustFS, MinIO, and others.
Empty credentials fall through to the provider’s default credential chain, so annotating the ServiceAccount is enough to use IRSA or Workload Identity:
Sizing
CPU and memory scale with concurrent captures, not total build volume. Each capture group in flight runs its own browser instance.Expose it
Setweb.ingress.enabled: true and web.ingress.host, plus web.ingress.className and web.ingress.annotations for your controller and TLS setup. Or use kubectl port-forward, see the helm install output for the command.
With web.ingress.tls set, web.ingress.redirectHttps (default true) redirects HTTP to HTTPS. Implemented for className: traefik and className: nginx. Configure the redirect yourself on other controllers.
Scaling
All off by default.
Scale the worker on queue depth rather than CPU. A pod holding an idle browser between snapshots looks unloaded while it is busy, so a CPU-based autoscaler will scale it away mid-capture. Capture work lands on the BullMQ
snapshot-capture queue:
triggers has no default, since the address and authentication depend on your Redis. cooldownPeriod must outlast the longest capture group, and worker.terminationGracePeriodSeconds must cover the in-flight snapshot on top of that.
Network policies
networkPolicy.enabled adds a policy per component. The worker gets deny-all ingress. The web pod admits traffic on its port from anywhere in the cluster, narrow that with networkPolicy.web.from.
Egress always allows DNS, and is otherwise unrestricted. Setting networkPolicy.egress replaces that default entirely, so it must list everything the pods reach: Postgres, Redis, object storage, and your git provider.
Security
Containers run as UID/GID1001, drop all capabilities, and disable privilege escalation.
The worker omits the RuntimeDefault seccomp profile that the other pods set, because its browser sandbox needs unprivileged user namespaces that the profile blocks. The worker is therefore not admissible to a Pod Security Admission restricted namespace as shipped. To run it in one, set worker.containerSecurityContext.seccompProfile and disable the browser sandbox.
Update
--version is the only thing that moves what runs, since a released chart pins its own image tag. Config changes without a version bump still roll the affected pods.
Full values reference
helm show values oci://ghcr.io/open-visual-regression/charts/ovr --version X.Y.Z prints every value with its inline documentation.
