Installation

This chapter is dedicated to the Installation of PatchMon Server and the Agent

Installing PatchMon Server on Docker

Overview

PatchMon runs as a containerised application made up of four services:

Container Images

Available Tags

Tag Description
latest Latest stable release
x.y.z Exact version pin (e.g. 1.2.3)
x.y Latest patch in a minor series (e.g. 1.2)
x Latest minor and patch in a major series (e.g. 1)
edge Latest development build in main branch - may be unstable, for testing only

Both backend and frontend images share the same version tags.


Quick Start

1. Download the files

curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/docker/docker-compose.yml
curl -fsSL -o env.example https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/docker/env.example

2. Create your .env file

Copy the example file and generate the three required secrets:

cp env.example .env

# Generate and insert strong secrets
sed -i "s/^POSTGRES_PASSWORD=$/POSTGRES_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/^REDIS_PASSWORD=$/REDIS_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/^JWT_SECRET=$/JWT_SECRET=$(openssl rand -hex 64)/" .env

Then open .env and configure your server access settings:

# Set these to the URL you will use to access PatchMon.
# SERVER_PROTOCOL, SERVER_HOST and SERVER_PORT are used by agents to connect back to PatchMon.
# CORS_ORIGIN should match the full URL you access PatchMon from in your browser.

SERVER_PROTOCOL=http
SERVER_HOST=localhost
SERVER_PORT=3000
CORS_ORIGIN=http://localhost:3000

Tip: If you are deploying PatchMon behind a reverse proxy with a domain name, set SERVER_PROTOCOL=https, SERVER_HOST=patchmon.example.com, SERVER_PORT=443, and CORS_ORIGIN=https://patchmon.example.com.

That's it. The docker-compose.yml uses env_file: .env to pass all your configuration straight into the containers. You do not need to edit the compose file itself.

For a full list of optional environment variables (rate limiting, logging, database pool tuning, session timeouts, etc.), see the Environment Variables page.

3. Start PatchMon

docker compose up -d

Once all containers are healthy, open your browser at http://localhost:3000 (or your configured URL) and complete the first-time setup to create your admin account.


Updating PatchMon

By default the compose file uses the latest tag. To update:

docker compose pull
docker compose up -d

This pulls the latest images, recreates containers, and keeps all your data intact.

Pinning to a specific version

If you prefer to pin versions, update the image tags in docker-compose.yml:

services:
  backend:
    image: ghcr.io/patchmon/patchmon-backend:1.4.0
  frontend:
    image: ghcr.io/patchmon/patchmon-frontend:1.4.0

Then run:

docker compose pull
docker compose up -d

Check the releases page for version-specific changes and migration notes.


Volumes

The compose file creates the following Docker volumes:

Volume Purpose
postgres_data PostgreSQL data directory
redis_data Redis data directory
agent_files PatchMon agent scripts
branding_assets Custom branding files (logos, favicons) - optional, new in 1.4.0

You can bind any of these to a host path instead of a Docker volume by editing the compose file.

Note: The backend container runs as user and group ID 1000. If you rebind the agent_files or branding_assets volumes to a host path, make sure that user/group has write permissions.


Docker Swarm Deployment

This section covers deploying PatchMon to a Docker Swarm cluster. For standard single-host deployments, use the production guide above.

Network Configuration

The default compose file uses an internal bridge network (patchmon-internal) for service-to-service communication. All services connect to this network and discover each other by service name.

If you are using an external reverse proxy network (e.g. Traefik):

  1. Keep all PatchMon services on patchmon-internal for internal communication
  2. Optionally add the frontend service to the reverse proxy network
  3. Make sure service name resolution works within the same network

Example: Traefik integration

services:
  frontend:
    image: ghcr.io/patchmon/patchmon-frontend:latest
    networks:
      - patchmon-internal
    deploy:
      replicas: 1
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.patchmon.rule=Host(`patchmon.example.com`)"

The frontend reaches the backend via patchmon-internal using the hostname backend, while Traefik routes external traffic to the frontend.

Troubleshooting: host not found in upstream "backend"

This usually means the frontend and backend are on different networks, or services haven't fully started. Check:


Development Setup

This section is for developers who want to contribute to PatchMon or run it locally in development mode.

Getting started

git clone https://github.com/PatchMon/PatchMon.git
cd PatchMon
docker compose -f docker/docker-compose.dev.yml up --watch --build

The development compose file:

Development ports

Service Port URL
Frontend 3000 http://localhost:3000
Backend API 3001 http://localhost:3001
PostgreSQL 5432 localhost:5432
Redis 6379 localhost:6379

Common commands

# Start with hot reload (attached)
docker compose -f docker/docker-compose.dev.yml up --watch

# Start detached
docker compose -f docker/docker-compose.dev.yml up -d

# Rebuild a specific service
docker compose -f docker/docker-compose.dev.yml up -d --build backend

# View logs
docker compose -f docker/docker-compose.dev.yml logs -f

How hot reload works

Building images locally

# Development images
docker build -f docker/backend.Dockerfile --target development -t patchmon-backend:dev .
docker build -f docker/frontend.Dockerfile --target development -t patchmon-frontend:dev .

# Production images
docker build -f docker/backend.Dockerfile -t patchmon-backend:latest .
docker build -f docker/frontend.Dockerfile -t patchmon-frontend:latest .

Installing PatchMon Server on Ubuntu 24

Overview

The PatchMon setup script automates the full installation on Ubuntu/Debian servers. It installs all dependencies, configures services, generates credentials, and starts PatchMon - ready to use in minutes.

It supports both fresh installations and updating existing instances.


Requirements

Requirement Minimum
OS Ubuntu 20.04+ / Debian 11+
CPU 2 vCPU
RAM 2 GB
Disk 15 GB
Access Root (sudo)
Network Internet access (to pull packages and clone the repo)

Fresh Installation

1. Prepare the server

apt-get update -y && apt-get upgrade -y
apt install curl jq bc -y

2. Run the setup script

curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh \
  && chmod +x setup.sh \
  && sudo bash setup.sh

3. Follow the interactive prompts

The script will ask you four things:

Prompt Description Default
Domain/IP The public DNS or local IP users will access PatchMon from patchmon.internal
SSL/HTTPS Enable Let's Encrypt SSL. Use y for public servers, n for internal networks n
Email Only asked if SSL is enabled - used for Let's Encrypt certificate notifications -
Release / Branch Lists the latest 3 release tags plus main. Pick the latest release unless you need a specific version Latest tag

After confirming your choices, the script runs fully unattended.


What the Script Does

The script performs these steps automatically:

  1. Checks timezone - confirms (or lets you change) the server timezone
  2. Installs prerequisites - curl, jq, git, wget, netcat-openbsd, sudo
  3. Installs Node.js 20.x - via NodeSource
  4. Installs PostgreSQL - creates an isolated database and user for this instance
  5. Installs Redis - configures ACL-based authentication with a dedicated Redis user and database
  6. Installs Nginx - sets up a reverse proxy with security headers
  7. Installs Certbot (if SSL enabled) - obtains and configures a Let's Encrypt certificate
  8. Creates a dedicated system user - PatchMon runs as a non-login, locked-down user
  9. Clones the repository to /opt/<your-domain>/
  10. Installs npm dependencies in an isolated environment
  11. Creates .env files - generates secrets and writes backend/.env and frontend/.env
  12. Runs database migrations - with self-healing for failed migrations
  13. Creates a systemd service - with NoNewPrivileges, PrivateTmp, and ProtectSystem=strict
  14. Configures Nginx - reverse proxy with HTTP/2, WebSocket support, and security headers
  15. Populates server settings in the database (server URL, protocol, port)
  16. Writes deployment-info.txt - all credentials and commands in one file

After Installation

  1. Visit http(s)://<your-domain> in your browser
  2. Complete the first-time admin setup (create your admin account)
  3. All credentials and useful commands are saved to:
/opt/<your-domain>/deployment-info.txt

Directory Structure

After installation, PatchMon lives at /opt/<your-domain>/:

/opt/<your-domain>/
  backend/
    .env              # Backend environment variables
    src/
    prisma/
  frontend/
    .env              # Frontend environment variables (baked at build)
    dist/             # Built frontend (served by Nginx)
  deployment-info.txt # Credentials, ports, and diagnostic commands
  patchmon-install.log

Environment Variables

The setup script generates a backend/.env with sensible defaults. You can customise it after installation.

File location: /opt/<your-domain>/backend/.env

Variables set by the script

Variable What the script sets
DATABASE_URL Full connection string with generated password
JWT_SECRET Auto-generated 50-character secret
CORS_ORIGIN <protocol>://<your-domain>
PORT Random port between 3001-3999
REDIS_HOST localhost
REDIS_PORT 6379
REDIS_USER Instance-specific Redis ACL user
REDIS_PASSWORD Auto-generated password
REDIS_DB Auto-detected available Redis database

Adding optional variables

To enable OIDC, adjust rate limits, configure TFA, or change other settings, add the relevant variables to backend/.env and restart the service.

For example, to enable OIDC SSO:

# Edit the .env file
sudo nano /opt/<your-domain>/backend/.env

Add at the bottom:

# OIDC / SSO
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://auth.example.com
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true

Then restart:

sudo systemctl restart <your-domain>

Full list of optional variables

All optional environment variables are documented in the Docker env.example file and on the Environment Variables page. The same variables work for both Docker and native installations. Key categories include:

After any .env change, restart the service: sudo systemctl restart <your-domain>


Updating an Existing Installation

To update PatchMon to the latest version, re-run the setup script with --update:

sudo bash setup.sh --update

The update process:

  1. Detects all existing PatchMon installations under /opt/
  2. Lets you select which instance to update
  3. Backs up the current code and database before making changes
  4. Pulls the latest code from the selected branch/tag
  5. Installs updated dependencies and rebuilds the frontend
  6. Runs any new database migrations (with self-healing)
  7. Adds any missing environment variables to backend/.env (preserves your existing values)
  8. Updates the Nginx configuration with latest security improvements
  9. Restarts the service

If the update fails, the script prints rollback instructions with the exact commands to restore from the backup.


Managing the Service

Replace <your-domain> with the domain/IP you used during installation (e.g. patchmon.internal).

Service commands

# Check status
systemctl status <your-domain>

# Restart
sudo systemctl restart <your-domain>

# Stop
sudo systemctl stop <your-domain>

# View logs (live)
journalctl -u <your-domain> -f

# View recent logs
journalctl -u <your-domain> --since "1 hour ago"

Other useful commands

# Test Nginx config
nginx -t && sudo systemctl reload nginx

# Check database connection
sudo -u <db-user> psql -d <db-name> -c "SELECT 1;"

# Check which port PatchMon is listening on
netstat -tlnp | grep <backend-port>

# View deployment info (credentials, ports, etc.)
cat /opt/<your-domain>/deployment-info.txt

Troubleshooting

Issue Solution
Script fails with permission error Run with sudo bash setup.sh
Service won't start Check logs: journalctl -u <your-domain> -n 50
Redis authentication error Verify REDIS_USER and REDIS_PASSWORD in backend/.env match Redis ACL. Run redis-cli ACL LIST to check
Database connection refused Check PostgreSQL is running: systemctl status postgresql
SSL certificate issues Run certbot certificates to check status. Renew with certbot renew
Nginx 502 Bad Gateway Backend may not be running. Check systemctl status <your-domain> and the backend port
Migration failures Check status: cd /opt/<your-domain>/backend && npx prisma migrate status
Port already in use The script picks a random port (3001-3999). Edit PORT in backend/.env and update the Nginx config

For more help, see the Troubleshooting page or check the installation log:

cat /opt/<your-domain>/patchmon-install.log

Please note: This script was built to automate the deployment of PatchMon however our preferred method of installation is via Docker. This method is hard to support due to various parameters and changes within the OS such as versions of Nginx causing issues on the installer.

Anyway, do enjoy and I understand if you're like me ... want to see the files in plain sight that is being served as the app ;)

PatchMon Environment Variables Reference

This document provides a comprehensive reference for all environment variables available in PatchMon. These variables can be configured in your backend/.env file (bare metal installations) or in the .env file alongside docker-compose.yml (Docker deployments using env_file).

Database Configuration

PostgreSQL database connection settings.

Variable Description Default Required Example
DATABASE_URL PostgreSQL connection string - Yes postgresql://user:pass@localhost:5432/patchmon_db
PM_DB_CONN_MAX_ATTEMPTS Maximum database connection attempts during startup 30 No 30
PM_DB_CONN_WAIT_INTERVAL Wait interval between connection attempts (seconds) 2 No 2

Usage Notes


Database Connection Pool (Prisma)

Connection pooling configuration for optimal database performance and resource management.

Variable Description Default Required Example
DB_CONNECTION_LIMIT Maximum number of database connections per instance 30 No 30
DB_POOL_TIMEOUT Seconds to wait for an available connection before timeout 20 No 20
DB_CONNECT_TIMEOUT Seconds to wait for initial database connection 10 No 10
DB_IDLE_TIMEOUT Seconds before closing idle connections 300 No 300
DB_MAX_LIFETIME Maximum lifetime of a connection in seconds 1800 No 1800

Sizing Guidelines

Small Deployment (1-10 hosts):

DB_CONNECTION_LIMIT=15
DB_POOL_TIMEOUT=20

Medium Deployment (10-50 hosts):

DB_CONNECTION_LIMIT=30  # Default
DB_POOL_TIMEOUT=20

Large Deployment (50+ hosts):

DB_CONNECTION_LIMIT=50
DB_POOL_TIMEOUT=30

Connection Pool Calculation

Use this formula to estimate your needs:

DB_CONNECTION_LIMIT = (expected_hosts * 2) + (concurrent_users * 2) + 5

Example: 20 hosts + 3 concurrent users:

(20 * 2) + (3 * 2) + 5 = 51 connections

Important Notes

Detecting Connection Pool Issues

When connection pool limits are hit, you'll see clear error messages in your backend console:

Typical Pool Timeout Error:

Host creation error: Error: Timed out fetching a new connection from the connection pool.
DATABASE CONNECTION POOL EXHAUSTED!
Current limit: DB_CONNECTION_LIMIT=30
Pool timeout: DB_POOL_TIMEOUT=20s
Suggestion: Increase DB_CONNECTION_LIMIT in your .env file

If you see these errors frequently, increase DB_CONNECTION_LIMIT by 10-20 and monitor your system.

Monitoring Connection Pool Usage

You can monitor your PostgreSQL connections to determine optimal pool size:

Check Current Connections:

# Connect to PostgreSQL
psql -U patchmon_user -d patchmon_db

# Run this query
SELECT count(*) as current_connections, 
       (SELECT setting::int FROM pg_settings WHERE name='max_connections') as max_connections
FROM pg_stat_activity 
WHERE datname = 'patchmon_db';

Database Transaction Timeouts

Control how long database transactions can run before being terminated.

Variable Description Default Required Example
DB_TRANSACTION_MAX_WAIT Maximum time (ms) to wait for a transaction to start 10000 No 10000
DB_TRANSACTION_TIMEOUT Maximum time (ms) for a standard transaction to complete 30000 No 30000
DB_TRANSACTION_LONG_TIMEOUT Maximum time (ms) for long-running transactions (e.g. bulk operations) 60000 No 60000

Usage Notes


Authentication & Security

JWT token configuration and security settings.

Variable Description Default Required Example
JWT_SECRET Secret key for signing JWT tokens - Yes your-secure-random-secret-key
JWT_EXPIRES_IN Access token expiration time 1h No 1h, 30m, 2h
JWT_REFRESH_EXPIRES_IN Refresh token expiration time 7d No 7d, 3d, 14d

Generating Secure Secrets

# Linux/macOS
openssl rand -hex 64

Time Format

Supports the following formats:

Examples: 30s, 15m, 2h, 7d

Development:

JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d

Production:

JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=3d

High Security:

JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=1d

Password Policy

Enforce password complexity requirements for local user accounts.

Variable Description Default Required Example
PASSWORD_MIN_LENGTH Minimum password length 8 No 8, 12, 16
PASSWORD_REQUIRE_UPPERCASE Require at least one uppercase letter true No true, false
PASSWORD_REQUIRE_LOWERCASE Require at least one lowercase letter true No true, false
PASSWORD_REQUIRE_NUMBER Require at least one number true No true, false
PASSWORD_REQUIRE_SPECIAL Require at least one special character true No true, false
PASSWORD_RATE_LIMIT_WINDOW_MS Rate limit window for password changes (ms) 900000 No 900000 (15 min)
PASSWORD_RATE_LIMIT_MAX Maximum password change attempts per window 5 No 5

Standard (default):

PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true

High Security:

PASSWORD_MIN_LENGTH=12
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true
PASSWORD_RATE_LIMIT_MAX=3

Usage Notes


Account Lockout

Protect against brute-force login attacks by temporarily locking accounts after repeated failures.

Variable Description Default Required Example
MAX_LOGIN_ATTEMPTS Failed login attempts before account lockout 5 No 5, 3, 10
LOCKOUT_DURATION_MINUTES Minutes the account stays locked after exceeding attempts 15 No 15, 30, 60

Usage Notes

Standard:

MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION_MINUTES=15

High Security:

MAX_LOGIN_ATTEMPTS=3
LOCKOUT_DURATION_MINUTES=30

Session Management

Control user session behavior and security.

Variable Description Default Required Example
SESSION_INACTIVITY_TIMEOUT_MINUTES Minutes of inactivity before automatic logout 30 No 30

Usage Notes


Two-Factor Authentication (TFA)

Settings for two-factor authentication when users have it enabled.

Variable Description Default Required Example
MAX_TFA_ATTEMPTS Failed TFA code attempts before lockout 5 No 5, 3
TFA_LOCKOUT_DURATION_MINUTES Minutes locked out after exceeding TFA attempts 30 No 30, 60
TFA_REMEMBER_ME_EXPIRES_IN "Remember this device" token expiration 30d No 30d, 7d, 90d
TFA_MAX_REMEMBER_SESSIONS Maximum remembered devices per user 5 No 5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD Failed attempts before flagging suspicious activity 3 No 3

Usage Notes

Standard:

MAX_TFA_ATTEMPTS=5
TFA_LOCKOUT_DURATION_MINUTES=30
TFA_REMEMBER_ME_EXPIRES_IN=30d
TFA_MAX_REMEMBER_SESSIONS=5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3

High Security:

MAX_TFA_ATTEMPTS=3
TFA_LOCKOUT_DURATION_MINUTES=60
TFA_REMEMBER_ME_EXPIRES_IN=7d
TFA_MAX_REMEMBER_SESSIONS=3
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=2

OIDC / SSO

OpenID Connect configuration for Single Sign-On. Set OIDC_ENABLED=true and fill in your identity provider details to enable SSO.

Variable Description Default Required Example
OIDC_ENABLED Enable OIDC authentication false No true, false
OIDC_ISSUER_URL Identity provider issuer URL - If OIDC enabled https://auth.example.com
OIDC_CLIENT_ID OAuth client ID - If OIDC enabled patchmon
OIDC_CLIENT_SECRET OAuth client secret - If OIDC enabled your-client-secret
OIDC_REDIRECT_URI Callback URL after authentication - If OIDC enabled https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES OAuth scopes to request openid email profile groups No openid email profile groups
OIDC_AUTO_CREATE_USERS Automatically create PatchMon accounts for new OIDC users true No true, false
OIDC_DEFAULT_ROLE Default role for auto-created OIDC users user No user, admin, viewer
OIDC_DISABLE_LOCAL_AUTH Disable local username/password login when OIDC is enabled false No true, false
OIDC_BUTTON_TEXT Login button text shown on the login page Login with SSO No Login with SSO, Sign in with Authentik

Group-to-Role Mapping

Map OIDC groups from your identity provider to PatchMon roles. This keeps role assignments in sync with your IdP.

Variable Description Default Required Example
OIDC_ADMIN_GROUP OIDC group name that maps to admin role - No PatchMon Admins
OIDC_USER_GROUP OIDC group name that maps to user role - No PatchMon Users
OIDC_SYNC_ROLES Sync roles from OIDC groups on each login true No true, false

Example: Authentik

OIDC_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example.com/application/o/patchmon/
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Authentik
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true

Example: Keycloak

OIDC_ENABLED=true
OIDC_ISSUER_URL=https://keycloak.example.com/realms/your-realm
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Keycloak

Usage Notes


Server & Network Configuration

Server protocol, host, and CORS settings.

Variable Description Default Required Example
PORT Backend API server port 3001 No 3001
NODE_ENV Node.js environment mode production No production, development
SERVER_PROTOCOL Server protocol http No http, https
SERVER_HOST Server hostname/domain localhost No patchmon.example.com
SERVER_PORT Server port 3000 No 3000, 443
CORS_ORIGIN Allowed CORS origin URL http://localhost:3000 No https://patchmon.example.com
CORS_ORIGINS Multiple allowed CORS origins (comma-separated) - No https://a.example.com,https://b.example.com
ENABLE_HSTS Enable HTTP Strict Transport Security true No true, false
TRUST_PROXY Trust proxy headers (when behind reverse proxy) true No true, false

Usage Notes

Example Configurations

Local Development:

SERVER_PROTOCOL=http
SERVER_HOST=localhost
SERVER_PORT=3000
CORS_ORIGIN=http://localhost:3000
ENABLE_HSTS=false
TRUST_PROXY=false

Production with HTTPS:

SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGIN=https://patchmon.example.com
ENABLE_HSTS=true
TRUST_PROXY=true

Multiple Domains:

SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGINS=https://patchmon.example.com,https://patchmon-alt.example.com
ENABLE_HSTS=true
TRUST_PROXY=true

Rate Limiting

Protect your API from abuse with configurable rate limits.

Variable Description Default Required Example
RATE_LIMIT_WINDOW_MS General rate limit window (milliseconds) 900000 No 900000 (15 min)
RATE_LIMIT_MAX Maximum requests per window (general) 5000 No 5000
AUTH_RATE_LIMIT_WINDOW_MS Authentication endpoints rate limit window (ms) 600000 No 600000 (10 min)
AUTH_RATE_LIMIT_MAX Maximum auth requests per window 500 No 500
AGENT_RATE_LIMIT_WINDOW_MS Agent API rate limit window (ms) 60000 No 60000 (1 min)
AGENT_RATE_LIMIT_MAX Maximum agent requests per window 1000 No 1000

Understanding Rate Limits

Rate limits are applied per IP address and endpoint category:

Calculating Windows

The window is a sliding time frame. Examples:

Default (Balanced):

RATE_LIMIT_WINDOW_MS=900000      # 15 minutes
RATE_LIMIT_MAX=5000              # ~5.5 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=500          # ~0.8 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=1000        # ~16 requests/second

Strict (High Security):

RATE_LIMIT_WINDOW_MS=900000      # 15 minutes
RATE_LIMIT_MAX=2000              # ~2.2 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=100          # ~0.16 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=500         # ~8 requests/second

Relaxed (Development/Testing):

RATE_LIMIT_WINDOW_MS=900000      # 15 minutes
RATE_LIMIT_MAX=10000             # ~11 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=1000         # ~1.6 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=2000        # ~33 requests/second

Redis Configuration

Redis is used for BullMQ job queues and caching.

Variable Description Default Required Example
REDIS_HOST Redis server hostname localhost No localhost, redis, 10.0.0.5
REDIS_PORT Redis server port 6379 No 6379
REDIS_USER Redis username (Redis 6+) - No default
REDIS_PASSWORD Redis authentication password - Recommended your-redis-password
REDIS_DB Redis database number 0 No 0, 1, 2

Usage Notes

Docker Deployment

In Docker, set REDIS_PASSWORD in your .env file. The compose file automatically passes it to both the Redis container (via its startup command) and the backend service (via env_file).

Bare Metal Deployment

The setup script configures Redis ACL with a dedicated user and password per instance. The credentials are written to backend/.env automatically.

Generating Secure Passwords

openssl rand -hex 32

Logging

Control application logging behavior and verbosity.

Variable Description Default Required Example
LOG_LEVEL Logging level info No debug, info, warn, error
ENABLE_LOGGING Enable/disable application logging true No true, false
PM_LOG_TO_CONSOLE Output logs to the console false No true, false
PM_LOG_REQUESTS_IN_DEV Log HTTP requests in development mode false No true, false
PRISMA_LOG_QUERIES Log all Prisma database queries false No true, false

Log Levels

Ordered from most to least verbose:

  1. debug: All logs including database queries, internal operations
  2. info: General information, startup messages, normal operations
  3. warn: Warning messages, deprecated features, non-critical issues
  4. error: Error messages only, critical issues

Development:

LOG_LEVEL=debug
ENABLE_LOGGING=true
PM_LOG_TO_CONSOLE=true
PM_LOG_REQUESTS_IN_DEV=true
PRISMA_LOG_QUERIES=true

Production:

LOG_LEVEL=info
ENABLE_LOGGING=true
PM_LOG_TO_CONSOLE=false
PRISMA_LOG_QUERIES=false

Production (Quiet):

LOG_LEVEL=warn
ENABLE_LOGGING=true
PRISMA_LOG_QUERIES=false

Timezone Configuration

Control timezone handling for timestamps and logs across the application.

Variable Description Default Required Example
TZ Timezone for timestamps and logs UTC No UTC, America/New_York, Europe/London

Usage Notes

Common Timezone Values

# UTC (recommended for servers)
TZ=UTC

# UK
TZ=Europe/London

# US
TZ=America/New_York       # Eastern
TZ=America/Chicago         # Central
TZ=America/Los_Angeles    # Pacific

# Europe
TZ=Europe/Paris
TZ=Europe/Berlin

# Asia
TZ=Asia/Tokyo
TZ=Asia/Shanghai

Body Size Limits

Control the maximum size of request bodies accepted by the API.

Variable Description Default Required Example
JSON_BODY_LIMIT Maximum JSON request body size 5mb No 5mb, 10mb, 1mb
AGENT_UPDATE_BODY_LIMIT Maximum body size for agent update payloads 2mb No 2mb, 5mb

Usage Notes


Encryption

Controls encryption of sensitive data stored in the database (e.g. AI provider API keys, bootstrap tokens).

Variable Description Default Required Example
AI_ENCRYPTION_KEY Encryption key for sensitive data at rest (64 hex characters) - No Output of openssl rand -hex 32
SESSION_SECRET Fallback key used if AI_ENCRYPTION_KEY is not set - No Output of openssl rand -hex 32

How It Works

The backend uses this priority chain to determine the encryption key:

  1. AI_ENCRYPTION_KEY - used directly if set (64 hex chars = 32 bytes, or any string which gets SHA-256 hashed)
  2. SESSION_SECRET - if AI_ENCRYPTION_KEY is not set, SHA-256 hashed to derive the key
  3. DATABASE_URL - if neither above is set, derives a key from the database URL (logs a security warning)
  4. Ephemeral - last resort, generates a random key (data encrypted with this key will be unreadable after a restart)

What Gets Encrypted

Usage Notes


User Management

Default settings for new users.

Variable Description Default Required Example
DEFAULT_USER_ROLE Default role assigned to new users user No user, admin, viewer

Available Roles

Usage Notes


Frontend Configuration

Frontend-specific environment variables (used during build and runtime).

Variable Description Default Required Example
VITE_API_URL Backend API base URL /api/v1 No http://localhost:3001/api/v1
VITE_APP_NAME Application name displayed in UI PatchMon No PatchMon
VITE_APP_VERSION Application version displayed in UI (from package.json) No 1.4.0
BACKEND_HOST Backend hostname (Docker only) backend No backend, localhost
BACKEND_PORT Backend port (Docker only) 3001 No 3001
VITE_ENABLE_LOGGING Enable frontend debug logging false No true, false

Usage Notes


Complete Example Configuration

Bare Metal (Production)

# Database
DATABASE_URL="postgresql://patchmon_user:secure_db_password@localhost:5432/patchmon_db"
PM_DB_CONN_MAX_ATTEMPTS=30
PM_DB_CONN_WAIT_INTERVAL=2

# Database Connection Pool
DB_CONNECTION_LIMIT=30
DB_POOL_TIMEOUT=20
DB_CONNECT_TIMEOUT=10
DB_IDLE_TIMEOUT=300
DB_MAX_LIFETIME=1800

# JWT
JWT_SECRET="generated-secure-secret-from-openssl"
JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=3d

# Server
PORT=3001
NODE_ENV=production
SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGIN=https://patchmon.example.com
ENABLE_HSTS=true
TRUST_PROXY=true

# Session
SESSION_INACTIVITY_TIMEOUT_MINUTES=30

# User
DEFAULT_USER_ROLE=user

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=5000
AUTH_RATE_LIMIT_WINDOW_MS=600000
AUTH_RATE_LIMIT_MAX=500
AGENT_RATE_LIMIT_WINDOW_MS=60000
AGENT_RATE_LIMIT_MAX=1000

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=secure_redis_password
REDIS_DB=0

# Logging
LOG_LEVEL=info
ENABLE_LOGGING=true

# Timezone
TZ=UTC

# TFA
TFA_REMEMBER_ME_EXPIRES_IN=30d
TFA_MAX_REMEMBER_SESSIONS=5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3

Docker (Production)

For Docker deployments, see docker/env.example for a complete template. Copy it to .env, fill in the required values, and run docker compose up -d. The compose file reads all variables via env_file: .env.


Troubleshooting

Common Issues

"timeout of 10000ms exceeded" when adding hosts:

Database connection failures on startup:

"Invalid or expired session" errors:

Rate limit errors (429 Too Many Requests):

CORS errors:

OIDC login fails:

Encrypted data unreadable after restart:


Security Best Practices

  1. Always set strong secrets:

    • Use openssl rand -hex 64 for JWT_SECRET
    • Use openssl rand -hex 32 for database and Redis passwords
  2. Enable HTTPS in production:

    • Set SERVER_PROTOCOL=https
    • Enable ENABLE_HSTS=true
    • Use proper SSL certificates
  3. Configure appropriate rate limits:

    • Adjust based on expected traffic
    • Lower limits for public-facing deployments
  4. Use session timeouts:

    • Don't set SESSION_INACTIVITY_TIMEOUT_MINUTES too high
    • Balance security with user experience
  5. Secure Redis:

    • Always set REDIS_PASSWORD
    • Use Redis ACLs in Redis 6+ for additional security
    • Don't expose Redis port publicly
  6. Enable account lockout:

    • Keep MAX_LOGIN_ATTEMPTS and LOCKOUT_DURATION_MINUTES at defaults or stricter
  7. Enforce password policy:

    • Keep all PASSWORD_REQUIRE_* options enabled
    • Consider increasing PASSWORD_MIN_LENGTH to 12+

Version Information

Installation of the PatchMon Agent

Introduction

Note : This is pre version 1.3.0 , new one needs to be made

The agent installer script authenticates with PatchMon server and proceeds to install the patchmon-agent.sh.

Adding a host:

Go to the Hosts page, press Add host:

image.png

or via the button on the top right of the hosts page:

image.png

You will be greeted by this screen where you enter in a friendly name for the server:

image.png

Groups will show empty unless you have added groups already.

Here is how to add groups

As soon as you press "Create Host" it will redirect you to the credentials and deployment script page:

image.png

Now we have two ways of installation however we should first take care of a few things:

The first is the checkbox "Force Install (bypass broken packages)"

At times in Linux, certain packages are broken and throw dependency issues when installing software which break the installation script. If you find this is happening then ideally you need to fix your server before you install more software, hoever that's not always possible so by pressing this flag it will bypass package manager issues.

Secondly, at times you may have a self-signed certificate in which this curl command needs an extra flag to go through. This would apply to both the Installation command as well as the agent communication within. Here is how to enable the -k flag installation-wide in settings.

Installing the agent in the Automated way

The one-line command includes the api-id and api-key which is utilized to download the PatchMon agent as well as to populate the credentials.txt file.

SSH into your server, ensure you are root and lets copy and paste this one-line command and see the output:

  🕐 Verifying system datetime and timezone...

📅 Current System Date/Time:
   • Date/Time: Sat Oct  4 07:16:00 PM BST 2025
   • Timezone: Europe/London

⚠️  Non-interactive installation detected

Please verify the date/time shown above is correct.
If the date/time is incorrect, it may cause issues with:
   • Logging timestamps
   • Scheduled updates
   • Data synchronization

✅ Continuing with installation...
✅ ✅ Date/time verification completed (assumed correct)

ℹ️  🚀 Starting PatchMon Agent Installation...
ℹ️  📋 Server: https://demo09.eu-west-02.patchmon.cloud
ℹ️  🔑 API ID: patchmon_90dd7fe...
ℹ️  🆔 Machine ID: a9295dcd387f979b...

🔧 Installation Diagnostics:
   • URL: https://demo09.eu-west-02.patchmon.cloud
   • CURL FLAGS: -s
   • API ID: patchmon_90dd7fe...
   • API Key: 49ff8a6f6b67abf5...

ℹ️  📦 Installing required dependencies...

ℹ️  Detected apt-get (Debian/Ubuntu)

ℹ️  Updating package lists...
Hit:1 https://cli.github.com/packages stable InRelease
Hit:2 https://linux.teamviewer.com/deb stable InRelease
Hit:3 https://download.docker.com/linux/ubuntu jammy InRelease
Hit:4 https://ppa.launchpadcontent.net/damentz/liquorix/ubuntu jammy InRelease
Hit:5 https://deb.nodesource.com/node_20.x nodistro InRelease
Hit:6 https://packages.microsoft.com/ubuntu/20.04/prod focal InRelease
Hit:7 https://packages.microsoft.com/repos/code stable InRelease
Hit:8 http://apt.pop-os.org/proprietary jammy InRelease
Hit:9 http://apt.pop-os.org/release jammy InRelease
Hit:10 https://apt.postgresql.org/pub/repos/apt jammy-pgdg InRelease
Hit:12 https://ngrok-agent.s3.amazonaws.com buster InRelease
Hit:13 http://apt.pop-os.org/ubuntu jammy InRelease                                         
Hit:14 http://apt.pop-os.org/ubuntu jammy-security InRelease
Hit:15 http://apt.pop-os.org/ubuntu jammy-updates InRelease
Hit:11 https://packagecloud.io/slacktechnologies/slack/debian jessie InRelease
Hit:16 http://apt.pop-os.org/ubuntu jammy-backports InRelease
Reading package lists... Done
N: Skipping acquire of configured file 'main/binary-i386/Packages' as repository 'https://apt.postgresql.org/pub/repos/apt jammy-pgdg InRelease' doesn't support architecture 'i386'

ℹ️  Installing jq, curl, and bc...
✅ All required packages are already installed

✅ Dependencies installation completed

ℹ️  📁 Setting up configuration directory...
ℹ️  📁 Creating new configuration directory...
ℹ️  🔐 Creating API credentials file...
ℹ️  📥 Downloading PatchMon agent script...
ℹ️  📋 Agent version: 1.2.7
ℹ️  🔍 Checking if machine is already enrolled...
✅ ✅ Machine not yet enrolled - proceeding with installation
ℹ️  🧪 Testing API credentials and connectivity...
✅ API credentials are valid
✅ ✅ TEST: API credentials are valid and server is reachable
ℹ️  📊 Sending initial package data to server...
ℹ️  Verifying system datetime and timezone...
ℹ️  Collecting system information...
ℹ️  Sending update to PatchMon server...
✅ Update sent successfully (190 packages processed)
ℹ️  Checking crontab configuration...
ℹ️  Updating crontab with current policy...
ℹ️  Setting update interval to 60 minutes
✅ Crontab updated successfully (duplicates removed)
✅ Crontab updated successfully
✅ ✅ UPDATE: Initial package data sent successfully
ℹ️  ✅ Automated updates configured by agent
✅ 🎉 PatchMon Agent installation completed successfully!

📋 Installation Summary:
   • Configuration directory: /etc/patchmon
   • Agent installed: /usr/local/bin/patchmon-agent.sh
   • Dependencies installed: jq, curl, bc
   • Automated updates configured via crontab
   • API credentials configured and tested
   • Update schedule managed by agent

🔧 Management Commands:
   • Test connection: /usr/local/bin/patchmon-agent.sh test
   • Manual update: /usr/local/bin/patchmon-agent.sh update
   • Check status: /usr/local/bin/patchmon-agent.sh diagnostics

✅ ✅ Your system is now being monitored by PatchMon!

Lets go through what this script has done:

  1. Asked you if the Time is correct, this is for informational purposes and it should be correct if it isn't. It causes issues with logs etc. Set that if it isn't right
  2. The script wants to install 
    jq bc curl

    For the agent to work properly.

  3. It will install the credentials file in the location :
/etc/patchmon/credentials

It's important to note that this credentials file includes the PATCHMON_URL="https://yourinstance.domiain", Ensure this is correct if you have issues connecting to the agent after changing urls or ports

The Agent script location is in the following location:

/usr/local/bin/patchmon-agent.sh

Once the installation is done, Click off the page in PatchMon and refresh the hosts page.

You will see the information of your host now showing along with all the other information:

image.png

First time setup admin page

First time admin setup

Upon first time setup you will see this page:

image.png

Enter the details and your password (min 8 characters). 
Try not using the username "admin" as it's a common one, but something unique to you.

After pressing Create account you will be redirected to the dashboard:

image.png

First thing is please setup MFA but going to your profile on the bottom left, you will be greeted with the Profile modification page, please press the Multi-Factor authentication tab and set that up:

image.png

 

If you get errors upon trying to login or create the admin account fort he first time, then ensure you have correctly setup the CORS url setting in your .env file located (/opt/<your instance>/backend/.env) or the docker environment file.

Installing PatchMon Server on K8S with Helm

PatchMon Helm Chart Documentation

Helm chart for deploying PatchMon on Kubernetes.


Overview

PatchMon runs as a containerised application made up of four services:

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.2
Frontend ghcr.io/patchmon/patchmon-frontend 1.4.2
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.2)
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


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.yaml ships 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

Then navigate to http://localhost:3000 and complete the first-time setup to create your admin account.


Production Deployment

For production use, refer to the provided values-prod.yaml file as a starting point. It demonstrates how to:

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:

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:

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
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
  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.2
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.2
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 hostPath volumes or a storage provider that does not respect fsGroup, you may need to enable backend.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.4.2"

frontend:
  image:
    tag: "1.4.2"

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:

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

Nginx example configuration for PatchMon

This nginx configuration is for the type of installation where it's on bare-metal / native installation.

Edits the ports as required

# Example nginx config for PatchMon
# - Frontend served from disk; /bullboard and /api/ proxied to backend
# - HTTP → HTTPS redirect, WebSocket (WSS) support, static asset caching
# Replace: your-domain.com, /opt/your-domain.com/frontend, backend port
# Copy to /etc/nginx/sites-available/ and symlink from sites-enabled, then:
#   sudo nginx -t && sudo systemctl reload nginx

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
upstream patchmon {
    server 127.0.0.1:3001;
}

# Redirect all HTTP to HTTPS (so ws:// is never used; frontend uses wss://)
server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your-domain.com;

    # SSL (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;

    # Bull Board – queue UI and WebSocket (before location /)
    location /bullboard {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Cookie $http_cookie;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 75s;
        proxy_pass_header Set-Cookie;
        proxy_cookie_path / /;
        proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }

    # API – REST and WebSockets (SSH terminal, agent WS)
    location /api/ {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
        proxy_cache_bypass $http_upgrade;
        client_max_body_size 10m;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 75s;
    }

    # Health check
    location /health {
        proxy_pass http://localhost:3001/health;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Static assets caching – SPA js/css/images/fonts; exclude Bull Board and API
    location ~* ^/(?!bullboard|api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        root /opt/your-domain.com/frontend;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Custom branding assets (logos, favicons) – from frontend build
    location /assets/ {
        alias /opt/your-domain.com/frontend/assets/;
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
        add_header Access-Control-Allow-Origin *;
    }

    # Frontend SPA
    location / {
        root /opt/your-domain.com/frontend;
        try_files $uri $uri/ /index.html;
        add_header X-Frame-Options DENY always;
        add_header X-Content-Type-Options nosniff always;
        add_header X-XSS-Protection "1; mode=block" always;
    }

    # Optional: security.txt
    # location /security.txt { return 301 https://$host/.well-known/security.txt; }
    # location = /.well-known/security.txt { alias /var/www/html/.well-known/security.txt; }
}