Installation
This chapter is dedicated to the Installation of PatchMon Server and the Agent
- Installing PatchMon Server on Docker
- Installing PatchMon Server on Ubuntu 24
- PatchMon Environment Variables Reference
- Installation of the PatchMon Agent
- First time setup admin page
- Installing PatchMon Server on K8S with Helm
- Nginx example configuration for PatchMon
Installing PatchMon Server on Docker
Overview
PatchMon runs as a containerised application made up of four services:
- Database - PostgreSQL 17
- Redis - Redis 7 (used for BullMQ job queues and caching)
- Backend - Node.js API server
- Frontend - React application served via NGINX
Container Images
- Backend: ghcr.io/patchmon/patchmon-backend
- Frontend: ghcr.io/patchmon/patchmon-frontend
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, andCORS_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_filesorbranding_assetsvolumes 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):
- Keep all PatchMon services on
patchmon-internalfor internal communication - Optionally add the frontend service to the reverse proxy network
- 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:
- All services are on the same internal network
- Service health status with
docker psordocker service ps - Network connectivity with
docker exec <container> ping backend
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:
- Builds images locally from source
- Enables hot reload via Docker Compose watch
- Exposes all service ports for debugging
- Mounts source code directly into containers
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
- Frontend/backend source changes are synced automatically in watch mode
- package.json changes trigger an automatic service rebuild
- Prisma schema changes cause the backend to restart automatically
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 |
| 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:
- Checks timezone - confirms (or lets you change) the server timezone
- Installs prerequisites -
curl,jq,git,wget,netcat-openbsd,sudo - Installs Node.js 20.x - via NodeSource
- Installs PostgreSQL - creates an isolated database and user for this instance
- Installs Redis - configures ACL-based authentication with a dedicated Redis user and database
- Installs Nginx - sets up a reverse proxy with security headers
- Installs Certbot (if SSL enabled) - obtains and configures a Let's Encrypt certificate
- Creates a dedicated system user - PatchMon runs as a non-login, locked-down user
- Clones the repository to
/opt/<your-domain>/ - Installs npm dependencies in an isolated environment
- Creates
.envfiles - generates secrets and writesbackend/.envandfrontend/.env - Runs database migrations - with self-healing for failed migrations
- Creates a systemd service - with
NoNewPrivileges,PrivateTmp, andProtectSystem=strict - Configures Nginx - reverse proxy with HTTP/2, WebSocket support, and security headers
- Populates server settings in the database (server URL, protocol, port)
- Writes
deployment-info.txt- all credentials and commands in one file
After Installation
- Visit
http(s)://<your-domain>in your browser - Complete the first-time admin setup (create your admin account)
- 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:
- Authentication -
JWT_EXPIRES_IN,JWT_REFRESH_EXPIRES_IN,SESSION_INACTIVITY_TIMEOUT_MINUTES - Password policy -
PASSWORD_MIN_LENGTH,PASSWORD_REQUIRE_UPPERCASE, etc. - Account lockout -
MAX_LOGIN_ATTEMPTS,LOCKOUT_DURATION_MINUTES - Two-factor authentication -
MAX_TFA_ATTEMPTS,TFA_REMEMBER_ME_EXPIRES_IN, etc. - OIDC / SSO -
OIDC_ENABLED,OIDC_ISSUER_URL,OIDC_CLIENT_ID, etc. - Rate limiting -
RATE_LIMIT_WINDOW_MS,RATE_LIMIT_MAX,AUTH_RATE_LIMIT_*,AGENT_RATE_LIMIT_* - Database pool -
DB_CONNECTION_LIMIT,DB_POOL_TIMEOUT,DB_IDLE_TIMEOUT, etc. - Logging -
LOG_LEVEL,ENABLE_LOGGING - Network -
ENABLE_HSTS,TRUST_PROXY,CORS_ORIGINS - Encryption -
AI_ENCRYPTION_KEY,SESSION_SECRET - Timezone -
TZ - Body limits -
JSON_BODY_LIMIT,AGENT_UPDATE_BODY_LIMIT
After any
.envchange, 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:
- Detects all existing PatchMon installations under
/opt/ - Lets you select which instance to update
- Backs up the current code and database before making changes
- Pulls the latest code from the selected branch/tag
- Installs updated dependencies and rebuilds the frontend
- Runs any new database migrations (with self-healing)
- Adds any missing environment variables to
backend/.env(preserves your existing values) - Updates the Nginx configuration with latest security improvements
- 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
- The
DATABASE_URLmust be a valid PostgreSQL connection string - Connection retry logic helps handle database startup delays in containerized environments
- Format:
postgresql://[user]:[password]@[host]:[port]/[database] - In Docker deployments,
DATABASE_URLis constructed automatically in the compose file - you do not set it in.env
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
- Each backend instance maintains its own connection pool
- Running multiple backend instances requires considering total connections to PostgreSQL
- PostgreSQL default
max_connectionsis 100 (ensure your pool size doesn't exceed this) - Connections are reused efficiently - you don't need one connection per host
- Increase pool size if experiencing timeout errors during high load
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';
Recommended Actions:
- If
current_connectionsfrequently approachesDB_CONNECTION_LIMIT, increase the pool size - Monitor during peak usage (when multiple users are active, agents checking in)
- Leave 20-30% headroom for burst traffic
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
- These prevent runaway queries from holding database locks indefinitely
- Increase
DB_TRANSACTION_LONG_TIMEOUTif bulk import or migration operations are timing out - All values are in milliseconds
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:
s: secondsm: minutesh: hoursd: days
Examples: 30s, 15m, 2h, 7d
Recommended Settings
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 |
Recommended Settings
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
- These rules apply to local accounts only - OIDC users authenticate against their identity provider
- Password changes and new account creation both enforce these rules
PASSWORD_RATE_LIMIT_*prevents brute-force password change attempts
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
- Lockout is per-account, not per-IP
- The failed attempts counter has a 15-minute rolling window - if no further failed attempts occur within that window, the counter resets on its own
- A successful login clears the failed attempts counter (before lockout is triggered)
- Once locked out, the account stays locked for the full
LOCKOUT_DURATION_MINUTES- there is no way to bypass this except waiting - Setting
MAX_LOGIN_ATTEMPTStoo low may lock out legitimate users who mistype passwords
Recommended Settings
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
- Sessions are tracked in the database with activity timestamps
- Each authenticated request updates the session activity
- Expired sessions are automatically invalidated
- Users must log in again after timeout period
- Lower values provide better security but may impact user experience
Recommended Settings
- High Security Environment:
15minutes - Standard Security:
30minutes (default) - User-Friendly:
60minutes
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
- These variables only apply when users have TFA enabled on their account
- "Remember this device" allows users to skip TFA on trusted devices
MAX_TFA_ATTEMPTSandTFA_LOCKOUT_DURATION_MINUTESprevent brute-force attacks on TOTP codes- Suspicious activity detection can trigger additional security measures
- Remembered sessions can be revoked by users or admins
Recommended Settings
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
OIDC_REDIRECT_URImust be registered as an allowed redirect URI in your identity provider- When
OIDC_DISABLE_LOCAL_AUTH=true, users can only log in via OIDC - useful for enforcing SSO across the organisation - When
OIDC_SYNC_ROLES=true, the user's role is updated on every login based on their OIDC group membership - If a user is in both
OIDC_ADMIN_GROUPandOIDC_USER_GROUP, the admin role takes precedence - The
groupsscope must be supported by your identity provider and included inOIDC_SCOPESfor group mapping to work
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
SERVER_PROTOCOL,SERVER_HOST, andSERVER_PORTare used to generate agent installation scriptsCORS_ORIGINmust match the URL you use to access PatchMon in your browserCORS_ORIGINS(plural, comma-separated) overridesCORS_ORIGINwhen set - only needed if PatchMon is accessed from multiple domains- Set
TRUST_PROXYtotruewhen behind nginx, Apache, or other reverse proxies ENABLE_HSTSshould betruein production with HTTPS
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:
- General API: Dashboard, hosts, packages, user management
- Authentication: Login, logout, token refresh
- Agent API: Agent check-ins, updates, package reports
Calculating Windows
The window is a sliding time frame. Examples:
900000ms = 15 minutes600000ms = 10 minutes60000ms = 1 minute
Recommended Settings
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
- Redis authentication is highly recommended for security
- Redis 6.0+ supports ACL with usernames; earlier versions use password-only auth
- If no password is set, Redis will be accessible without authentication (not recommended)
- Database number allows multiple applications to use the same Redis instance
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:
debug: All logs including database queries, internal operationsinfo: General information, startup messages, normal operationswarn: Warning messages, deprecated features, non-critical issueserror: Error messages only, critical issues
Recommended Settings
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
- The
TZenvironment variable controls timezone handling across all components:- Backend (Node.js): Timestamps in API responses, database records, logs
- Agent (Go): Agent logs, integration data timestamps
- If
TZis not set, the application defaults to UTC - Database timestamps are always stored in UTC for consistency
- Display timestamps can be converted to the configured timezone
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
JSON_BODY_LIMITapplies to all standard API endpoints (dashboard actions, user management, etc.)AGENT_UPDATE_BODY_LIMITapplies specifically to agent check-in and package report payloads- Increase these if agents are managing a very large number of packages and the payload exceeds the limit
- Keep these as low as practical to limit memory usage and reduce the impact of oversized requests
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:
AI_ENCRYPTION_KEY- used directly if set (64 hex chars = 32 bytes, or any string which gets SHA-256 hashed)SESSION_SECRET- ifAI_ENCRYPTION_KEYis not set, SHA-256 hashed to derive the keyDATABASE_URL- if neither above is set, derives a key from the database URL (logs a security warning)- Ephemeral - last resort, generates a random key (data encrypted with this key will be unreadable after a restart)
What Gets Encrypted
- AI API keys - API keys for AI providers (e.g. OpenAI) are AES-256-GCM encrypted before being stored in the database
- Bootstrap tokens - Agent auto-enrollment API keys are encrypted before temporary storage in Redis
Usage Notes
- For most deployments, you do not need to set either variable - the key is derived from
DATABASE_URLwhich is stable - Set
AI_ENCRYPTION_KEYif you need encryption stability across database URL changes or multi-replica deployments - These do not affect user password storage (passwords are bcrypt hashed, not encrypted)
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
admin: Full system access, can manage users and settingsuser: Standard access, can manage hosts and packagesviewer: Read-only access, cannot make changes
Usage Notes
- Only applies to newly created users
- Existing users are not affected by changes to this variable
- First user created through setup is always an admin
- Can be changed per-user through the user management interface
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
- Frontend variables are prefixed with
VITE_for the Vite build system VITE_*variables are embedded at build time - they cannot be changed at runtimeVITE_API_URLcan be relative (/api/v1) or absoluteBACKEND_HOSTandBACKEND_PORTare used by the Docker frontend container's Nginx proxy config
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:
- Increase
DB_CONNECTION_LIMIT(try 30 or higher) - Increase
DB_POOL_TIMEOUT(try 20 or 30) - Check backend logs for "DATABASE CONNECTION POOL EXHAUSTED" messages
Database connection failures on startup:
- Increase
PM_DB_CONN_MAX_ATTEMPTS - Increase
PM_DB_CONN_WAIT_INTERVAL - Verify
DATABASE_URLis correct
"Invalid or expired session" errors:
- Check
JWT_SECREThasn't changed between restarts - Verify
SESSION_INACTIVITY_TIMEOUT_MINUTESisn't too low - Ensure
JWT_EXPIRES_INis reasonable
Rate limit errors (429 Too Many Requests):
- Increase
RATE_LIMIT_MAXvalues - Increase window duration (
*_WINDOW_MSvariables)
CORS errors:
- Verify
CORS_ORIGINmatches your frontend URL exactly (protocol + domain + port) - For multiple domains, use
CORS_ORIGINS(plural, comma-separated)
OIDC login fails:
- Verify
OIDC_REDIRECT_URIis registered in your identity provider - Check
OIDC_ISSUER_URLis reachable from the PatchMon server - Ensure
OIDC_CLIENT_SECRETmatches the value in your IdP
Encrypted data unreadable after restart:
- Set
AI_ENCRYPTION_KEYto a stable value so the key persists across restarts - Re-enter AI provider API keys if the encryption key has changed
Security Best Practices
-
Always set strong secrets:
- Use
openssl rand -hex 64forJWT_SECRET - Use
openssl rand -hex 32for database and Redis passwords
- Use
-
Enable HTTPS in production:
- Set
SERVER_PROTOCOL=https - Enable
ENABLE_HSTS=true - Use proper SSL certificates
- Set
-
Configure appropriate rate limits:
- Adjust based on expected traffic
- Lower limits for public-facing deployments
-
Use session timeouts:
- Don't set
SESSION_INACTIVITY_TIMEOUT_MINUTEStoo high - Balance security with user experience
- Don't set
-
Secure Redis:
- Always set
REDIS_PASSWORD - Use Redis ACLs in Redis 6+ for additional security
- Don't expose Redis port publicly
- Always set
-
Enable account lockout:
- Keep
MAX_LOGIN_ATTEMPTSandLOCKOUT_DURATION_MINUTESat defaults or stricter
- Keep
-
Enforce password policy:
- Keep all
PASSWORD_REQUIRE_*options enabled - Consider increasing
PASSWORD_MIN_LENGTHto 12+
- Keep all
Version Information
- Last Updated: February 2026
- Applicable to PatchMon: v1.4.0+
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:
You will be greeted by this screen where you enter in a friendly name for the server:
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:
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:
- 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
- The script wants to install
jq bc curlFor the agent to work properly.
- 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:
First time setup admin page
First time admin setup
Upon first time setup you will see this page:
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:
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:
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.
- 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.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
- 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
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
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.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:
registry.example.com/postgres:18-alpineregistry.example.com/redis:8-alpineregistry.example.com/patchmon/patchmon-backend:1.4.2registry.example.com/patchmon/patchmon-frontend:1.4.2registry.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
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; }
}