Installing PatchMon Server on K8S with Helm
PatchMon Helm Chart Documentation
Helm chart for deploying PatchMon on Kubernetes.
- Chart repository: github.com/RuTHlessBEat200/PatchMon-helm
- Application repository: github.com/PatchMon/PatchMon
Overview
PatchMon runs as a containerised application made up of four services:
- Database -- PostgreSQL 18 (StatefulSet with persistent storage)
- Redis -- Redis 8 (used for BullMQ job queues and caching, StatefulSet with persistent storage)
- Backend -- Node.js API server (Deployment with optional HPA)
- Frontend -- React application served via NGINX (Deployment with optional HPA)
The chart deploys all four components into a single namespace and wires them together automatically using init containers, internal ClusterIP services, and a shared ConfigMap.
Container Images
| Component | Image | Default Tag |
|---|---|---|
| Backend | ghcr.io/patchmon/patchmon-backend | 1.4.0 |
| Frontend | ghcr.io/patchmon/patchmon-frontend | 1.4.0 |
| Database | docker.io/postgres | 18-alpine |
| Redis | docker.io/redis | 8-alpine |
Available Tags (Backend and Frontend)
Both backend and frontend images share the same version tags.
| Tag | Description |
|---|---|
latest |
Latest stable release |
x.y.z |
Exact version pin (e.g. 1.4.0) |
x.y |
Latest patch in a minor series (e.g. 1.4) |
x |
Latest minor and patch in a major series (e.g. 1) |
edge |
Latest development build from the main branch -- may be unstable, for testing only |
Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- A PersistentVolume provisioner in the cluster (for database, Redis, and backend agent-file storage)
- (Optional) An Ingress controller (e.g. NGINX Ingress) for external access
- (Optional) cert-manager for automatic TLS certificate management
- (Optional) Metrics Server for HPA functionality
Quick Start
The quickest way to get PatchMon running is to use the provided
values-quick-start.yaml file.
It contains all required secrets inline and sensible defaults so you can install
with a single command.
Warning:
values-quick-start.yamlships with placeholder secrets and is intended for evaluation and testing only. Never use it in production without replacing all secret values.
1. Install the chart
wget https://github.com/RuTHlessBEat200/PatchMon-helm/blob/main/values-quick-start.yaml
helm install patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--create-namespace \
--values values-quick-start.yaml
2. Wait for pods to become ready
kubectl get pods -n patchmon -w
3. Access PatchMon
If ingress is enabled, open the host you configured (e.g. https://patchmon-dev.example.com).
Without ingress, use port-forwarding:
kubectl port-forward -n patchmon svc/patchmon-dev-frontend 3000:3000
Production Deployment
For production use, refer to the provided
values-prod.yaml file as a starting point.
It demonstrates how to:
- Use an external secret (e.g. managed by KSOPS, Sealed Secrets, or External Secrets Operator) instead of inline passwords
- Configure HTTPS with cert-manager
- Set the correct server protocol, host, and port for agent communication
1. Create your secrets
The chart does not auto-generate secrets. You must supply them yourself.
Required secrets:
| Key | Description |
|---|---|
postgres-password |
PostgreSQL password |
redis-password |
Redis password |
jwt-secret |
JWT signing secret for the backend |
ai-encryption-key |
Encryption key for AI provider credentials |
oidc-client-secret |
OIDC client secret (only if OIDC is enabled) |
You can either:
- Set passwords directly in your values file (
database.auth.password,redis.auth.password,backend.jwtSecret,backend.aiEncryptionKey), or - Create a Kubernetes Secret separately and reference it with
existingSecret/existingSecretPasswordKeyfields.
Example -- creating a secret manually:
kubectl create namespace patchmon
kubectl create secret generic patchmon-secrets \
--namespace patchmon \
--from-literal=postgres-password="$(openssl rand -hex 32)" \
--from-literal=redis-password="$(openssl rand -hex 32)" \
--from-literal=jwt-secret="$(openssl rand -hex 64)" \
--from-literal=ai-encryption-key="$(openssl rand -hex 32)"
Secret management tools for production:
- KSOPS -- encrypt secrets in Git using Mozilla SOPS
- Sealed Secrets -- encrypt secrets that only the cluster can decrypt
- External Secrets Operator -- sync secrets from external stores (Vault, AWS Secrets Manager, etc.)
- Vault -- enterprise-grade secret management
2. Create your values file
Start from values-prod.yaml and adjust to your environment:
global:
storageClass: "your-storage-class"
fullnameOverride: "patchmon-prod"
backend:
env:
serverProtocol: https
serverHost: patchmon.example.com
serverPort: "443"
corsOrigin: https://patchmon.example.com
existingSecret: "patchmon-secrets"
existingSecretJwtKey: "jwt-secret"
existingSecretAiEncryptionKey: "ai-encryption-key"
database:
auth:
existingSecret: patchmon-secrets
existingSecretPasswordKey: postgres-password
redis:
auth:
existingSecret: patchmon-secrets
existingSecretPasswordKey: redis-password
secret:
create: false # Disable chart-managed secret since we use an external one
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: patchmon.example.com
paths:
- path: /
pathType: Prefix
service:
name: frontend
port: 3000
- path: /api
pathType: Prefix
service:
name: backend
port: 3001
tls:
- secretName: patchmon-tls
hosts:
- patchmon.example.com
3. Install
helm install patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--create-namespace \
--values values-prod.yaml
Configuration Reference
Global Settings
| Parameter | Description | Default |
|---|---|---|
global.imageRegistry |
Override the image registry for all components | "" |
global.imageTag |
Override the image tag for backend and frontend (if set, takes priority over individual tags) | "" |
global.imagePullSecrets |
Image pull secrets applied to all pods | [] |
global.storageClass |
Default storage class for all PVCs | "" |
nameOverride |
Override the chart name used in resource names | "" |
fullnameOverride |
Override the full resource name prefix | "" |
commonLabels |
Labels added to all resources | {} |
commonAnnotations |
Annotations added to all resources | {} |
Database (PostgreSQL)
| Parameter | Description | Default |
|---|---|---|
database.enabled |
Deploy the PostgreSQL StatefulSet | true |
database.image.registry |
Image registry | docker.io |
database.image.repository |
Image repository | postgres |
database.image.tag |
Image tag | 18-alpine |
database.image.pullPolicy |
Image pull policy | IfNotPresent |
database.auth.database |
Database name | patchmon_db |
database.auth.username |
Database user | patchmon_user |
database.auth.password |
Database password (required if existingSecret is not set) |
"" |
database.auth.existingSecret |
Name of an existing secret containing the password | "" |
database.auth.existingSecretPasswordKey |
Key inside the existing secret | postgres-password |
database.replicaCount |
Number of replicas | 1 |
database.updateStrategy.type |
StatefulSet update strategy | RollingUpdate |
database.persistence.enabled |
Enable persistent storage | true |
database.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
database.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
database.persistence.size |
PVC size | 5Gi |
database.resources.requests.cpu |
CPU request | 100m |
database.resources.requests.memory |
Memory request | 128Mi |
database.resources.limits.cpu |
CPU limit | 1000m |
database.resources.limits.memory |
Memory limit | 1Gi |
database.service.type |
Service type | ClusterIP |
database.service.port |
Service port | 5432 |
database.nodeSelector |
Node selector | {} |
database.tolerations |
Tolerations | [] |
database.affinity |
Affinity rules | {} |
Redis
| Parameter | Description | Default |
|---|---|---|
redis.enabled |
Deploy the Redis StatefulSet | true |
redis.image.registry |
Image registry | docker.io |
redis.image.repository |
Image repository | redis |
redis.image.tag |
Image tag | 8-alpine |
redis.image.pullPolicy |
Image pull policy | IfNotPresent |
redis.auth.password |
Redis password (required if existingSecret is not set) |
"" |
redis.auth.existingSecret |
Name of an existing secret containing the password | "" |
redis.auth.existingSecretPasswordKey |
Key inside the existing secret | redis-password |
redis.replicaCount |
Number of replicas | 1 |
redis.updateStrategy.type |
StatefulSet update strategy | RollingUpdate |
redis.persistence.enabled |
Enable persistent storage | true |
redis.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
redis.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
redis.persistence.size |
PVC size | 5Gi |
redis.resources.requests.cpu |
CPU request | 50m |
redis.resources.requests.memory |
Memory request | 10Mi |
redis.resources.limits.cpu |
CPU limit | 500m |
redis.resources.limits.memory |
Memory limit | 512Mi |
redis.service.type |
Service type | ClusterIP |
redis.service.port |
Service port | 6379 |
redis.nodeSelector |
Node selector | {} |
redis.tolerations |
Tolerations | [] |
redis.affinity |
Affinity rules | {} |
Backend
| Parameter | Description | Default |
|---|---|---|
backend.enabled |
Deploy the backend | true |
backend.image.registry |
Image registry | ghcr.io |
backend.image.repository |
Image repository | patchmon/patchmon-backend |
backend.image.tag |
Image tag (overridden by global.imageTag if set) |
1.4.0 |
backend.image.pullPolicy |
Image pull policy | Always |
backend.replicaCount |
Number of replicas (>1 requires RWX storage for agent files) | 1 |
backend.updateStrategy.type |
Deployment update strategy | Recreate |
backend.jwtSecret |
JWT signing secret (required if existingSecret is not set) |
"" |
backend.aiEncryptionKey |
AI encryption key (required if existingSecret is not set) |
"" |
backend.existingSecret |
Name of an existing secret for JWT and AI encryption key | "" |
backend.existingSecretJwtKey |
Key for JWT secret inside the existing secret | jwt-secret |
backend.existingSecretAiEncryptionKey |
Key for AI encryption key inside the existing secret | ai-encryption-key |
backend.persistence.enabled |
Enable persistent storage for agent files | true |
backend.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
backend.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
backend.persistence.size |
PVC size | 5Gi |
backend.resources.requests.cpu |
CPU request | 10m |
backend.resources.requests.memory |
Memory request | 256Mi |
backend.resources.limits.cpu |
CPU limit | 2000m |
backend.resources.limits.memory |
Memory limit | 2Gi |
backend.service.type |
Service type | ClusterIP |
backend.service.port |
Service port | 3001 |
backend.autoscaling.enabled |
Enable HPA (requires RWX storage if >1 replica) | false |
backend.autoscaling.minReplicas |
Minimum replicas | 1 |
backend.autoscaling.maxReplicas |
Maximum replicas | 10 |
backend.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilisation | 80 |
backend.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilisation | 80 |
backend.initContainers.waitForDatabase.enabled |
Wait for database before starting | true |
backend.initContainers.waitForRedis.enabled |
Wait for Redis before starting | true |
backend.initContainers.fixPermissions.enabled |
Run a privileged init container to fix file permissions | false |
backend.nodeSelector |
Node selector | {} |
backend.tolerations |
Tolerations | [] |
backend.affinity |
Affinity rules | {} |
Backend Environment Variables
| Parameter | Description | Default |
|---|---|---|
backend.env.enableLogging |
Enable application logging | true |
backend.env.logLevel |
Log level (trace, debug, info, warn, error) |
info |
backend.env.logToConsole |
Log to stdout | true |
backend.env.serverProtocol |
Protocol used by agents to reach the backend (http or https) |
http |
backend.env.serverHost |
Hostname used by agents to reach the backend | patchmon.example.com |
backend.env.serverPort |
Port used by agents (80 or 443) |
80 |
backend.env.corsOrigin |
CORS allowed origin (should match the URL users access in a browser) | http://patchmon.example.com |
backend.env.dbConnectionLimit |
Database connection pool limit | 30 |
backend.env.dbPoolTimeout |
Pool timeout in seconds | 20 |
backend.env.dbConnectTimeout |
Connection timeout in seconds | 10 |
backend.env.dbIdleTimeout |
Idle connection timeout in seconds | 300 |
backend.env.dbMaxLifetime |
Max connection lifetime in seconds | 1800 |
backend.env.rateLimitWindowMs |
General rate limit window (ms) | 900000 |
backend.env.rateLimitMax |
General rate limit max requests | 5000 |
backend.env.authRateLimitWindowMs |
Auth rate limit window (ms) | 600000 |
backend.env.authRateLimitMax |
Auth rate limit max requests | 500 |
backend.env.agentRateLimitWindowMs |
Agent rate limit window (ms) | 60000 |
backend.env.agentRateLimitMax |
Agent rate limit max requests | 1000 |
backend.env.redisDb |
Redis database index | 0 |
backend.env.trustProxy |
Trust proxy headers (set to true or a number behind a reverse proxy) |
false |
backend.env.enableHsts |
Enable HSTS header | false |
backend.env.defaultUserRole |
Default role for new users | user |
backend.env.autoCreateRolePermissions |
Auto-create role permissions | false |
OIDC / SSO Configuration
| Parameter | Description | Default |
|---|---|---|
backend.oidc.enabled |
Enable OIDC authentication | false |
backend.oidc.issuerUrl |
OIDC issuer URL | "" |
backend.oidc.clientId |
OIDC client ID | "" |
backend.oidc.clientSecret |
OIDC client secret (required if existingSecret not set) |
"" |
backend.oidc.existingSecret |
Existing secret containing the OIDC client secret | "" |
backend.oidc.existingSecretClientSecretKey |
Key inside the existing secret | oidc-client-secret |
backend.oidc.scopes |
OIDC scopes | openid profile email |
backend.oidc.buttonText |
Login button text | Login with SSO |
backend.oidc.autoCreateUsers |
Auto-create users on first OIDC login | false |
backend.oidc.defaultRole |
Default role for OIDC-created users | user |
backend.oidc.syncRoles |
Sync roles from OIDC group claims | false |
backend.oidc.disableLocalAuth |
Disable local username/password authentication | false |
backend.oidc.sessionTtl |
OIDC session TTL in seconds | 86400 |
backend.oidc.groups.superadmin |
OIDC group mapped to the superadmin role | "" |
backend.oidc.groups.admin |
OIDC group mapped to the admin role | "" |
backend.oidc.groups.hostManager |
OIDC group mapped to the hostManager role | "" |
backend.oidc.groups.user |
OIDC group mapped to the user role | "" |
backend.oidc.groups.readonly |
OIDC group mapped to the readonly role | "" |
Frontend
| Parameter | Description | Default |
|---|---|---|
frontend.enabled |
Deploy the frontend | true |
frontend.image.registry |
Image registry | ghcr.io |
frontend.image.repository |
Image repository | patchmon/patchmon-frontend |
frontend.image.tag |
Image tag (overridden by global.imageTag if set) |
1.4.0 |
frontend.image.pullPolicy |
Image pull policy | IfNotPresent |
frontend.replicaCount |
Number of replicas | 1 |
frontend.updateStrategy.type |
Deployment update strategy | Recreate |
frontend.resources.requests.cpu |
CPU request | 10m |
frontend.resources.requests.memory |
Memory request | 50Mi |
frontend.resources.limits.cpu |
CPU limit | 1000m |
frontend.resources.limits.memory |
Memory limit | 512Mi |
frontend.service.type |
Service type | ClusterIP |
frontend.service.port |
Service port | 3000 |
frontend.autoscaling.enabled |
Enable HPA | false |
frontend.autoscaling.minReplicas |
Minimum replicas | 1 |
frontend.autoscaling.maxReplicas |
Maximum replicas | 10 |
frontend.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilisation | 80 |
frontend.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilisation | 80 |
frontend.initContainers.waitForBackend.enabled |
Wait for backend before starting | true |
frontend.nodeSelector |
Node selector | {} |
frontend.tolerations |
Tolerations | [] |
frontend.affinity |
Affinity rules | {} |
Ingress
| Parameter | Description | Default |
|---|---|---|
ingress.enabled |
Enable ingress resource | true |
ingress.className |
Ingress class name | "" |
ingress.annotations |
Ingress annotations | {} |
ingress.hosts |
List of ingress host rules | see values.yaml |
ingress.tls |
TLS configuration | [] (disabled) |
Other
| Parameter | Description | Default |
|---|---|---|
serviceAccount.create |
Create a ServiceAccount | false |
serviceAccount.annotations |
ServiceAccount annotations | {} |
serviceAccount.name |
ServiceAccount name | "" |
configMap.create |
Create the application ConfigMap | true |
configMap.annotations |
ConfigMap annotations | {} |
secret.create |
Create the chart-managed Secret (disable when using an external secret) | true |
secret.annotations |
Secret annotations | {} |
Persistent Volumes
The chart creates the following PersistentVolumeClaims:
| PVC | Component | Purpose | Default Size |
|---|---|---|---|
postgres-data |
Database | PostgreSQL data directory | 5Gi |
redis-data |
Redis | Redis data directory | 5Gi |
agent-files |
Backend | PatchMon agent scripts and branding assets | 5Gi |
All PVCs respect the global.storageClass setting unless overridden at the component level.
Note: The backend container runs as UID/GID 1000. If you use
hostPathvolumes or a storage provider that does not respectfsGroup, you may need to enablebackend.initContainers.fixPermissions.enabled(requires privileged init containers).
Updating PatchMon
Using global.imageTag
The simplest way to update both backend and frontend at once is to set global.imageTag:
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
-n patchmon \
-f values-prod.yaml \
--set global.imageTag=1.5.0
When global.imageTag is set it overrides both backend.image.tag and frontend.image.tag.
Pinning individual tags
You can also set each tag independently:
backend:
image:
tag: "1.5.0"
frontend:
image:
tag: "1.5.0"
Upgrading the chart version
# Upgrade with new values
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--values values-prod.yaml
# Upgrade and wait for rollout
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--values values-prod.yaml \
--wait --timeout 10m
Check the releases page for version-specific changes and migration notes.
Uninstalling
# Uninstall the release
helm uninstall patchmon -n patchmon
# Clean up PVCs (optional -- this deletes all data)
kubectl delete pvc -n patchmon -l app.kubernetes.io/instance=patchmon
Advanced Configuration
Custom Image Registry
Override the registry for all images (useful for air-gapped environments or private mirrors):
global:
imageRegistry: "registry.example.com"
This changes every image pull to use the specified registry:
registry.example.com/postgres:18-alpineregistry.example.com/redis:8-alpineregistry.example.com/patchmon/patchmon-backend:1.4.0registry.example.com/patchmon/patchmon-frontend:1.4.0registry.example.com/busybox:latest(init containers)
Without global.imageRegistry, components use their default registries (docker.io for database/Redis, ghcr.io for backend/frontend).
Multi-Tenant Deployment
Deploy multiple isolated instances in separate namespaces using fullnameOverride:
fullnameOverride: "patchmon-tenant-a"
backend:
env:
serverHost: tenant-a.patchmon.example.com
corsOrigin: https://tenant-a.patchmon.example.com
ingress:
hosts:
- host: tenant-a.patchmon.example.com
paths:
- path: /
pathType: Prefix
service:
name: frontend
port: 3000
- path: /api
pathType: Prefix
service:
name: backend
port: 3001
Horizontal Pod Autoscaling
backend:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
persistence:
accessModes:
- ReadWriteMany # RWX required when running multiple replicas
frontend:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
Note: Scaling the backend beyond one replica requires a storage class that supports
ReadWriteMany(RWX) access mode, because all replicas need write access to agent files.
Using an External Database
Disable the built-in database and point the backend at an external PostgreSQL instance:
database:
enabled: false
backend:
env:
# Configure the external database connection via environment variables
# or adjust your external DB settings accordingly
OIDC / SSO Integration
backend:
oidc:
enabled: true
issuerUrl: "https://auth.example.com/realms/master"
clientId: "patchmon"
clientSecret: "your-client-secret"
scopes: "openid profile email groups"
buttonText: "Login with SSO"
autoCreateUsers: true
syncRoles: true
groups:
superadmin: "patchmon-admins"
admin: ""
hostManager: ""
user: ""
readonly: ""
Troubleshooting
Check pod status
kubectl get pods -n patchmon
kubectl describe pod <pod-name> -n patchmon
kubectl logs <pod-name> -n patchmon
Check init container logs
kubectl logs <pod-name> -n patchmon -c wait-for-database
kubectl logs <pod-name> -n patchmon -c wait-for-redis
kubectl logs <pod-name> -n patchmon -c wait-for-backend
Check service connectivity
# Test database connection
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- nc -zv patchmon-prod-database 5432
# Test Redis connection
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- nc -zv patchmon-prod-redis 6379
# Check backend health
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- wget -qO- http://localhost:3001/health
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
Pods stuck in Init state |
Database or Redis not yet running | Check StatefulSet events: kubectl describe sts -n patchmon |
PVC stuck in Pending |
No matching StorageClass or no available PV | Verify storage class exists: kubectl get sc |
ImagePullBackOff |
Registry credentials missing or incorrect image reference | Check imagePullSecrets and image path |
| Ingress returns 404 / 502 | Ingress controller not installed or misconfigured path rules | Verify controller pods and ingress resource: kubectl describe ingress -n patchmon |
secret ... not found |
Required secret was not created before install | Create the secret or set secret.create: true with inline passwords |
Development
Lint the chart
helm lint .
Render templates locally
# Render with default values
helm template patchmon . --values values-quick-start.yaml
# Render with production values
helm template patchmon . --values values-prod.yaml
# Debug template rendering
helm template patchmon . --values values-quick-start.yaml --debug
Dry-run installation
helm install patchmon . \
--namespace patchmon \
--dry-run --debug \
--values values-quick-start.yaml
Support
- GitHub Issues: github.com/RuTHlessBEat200/PatchMon-helm/issues
- Application repository: github.com/PatchMon/PatchMon
No comments to display
No comments to display