In this tutorial, we will set up PatchMon as a policeman in our stack that detects if updates are available. While from version 2, Patchmon supports applying updates, the functionality is rather limited / basic for our use case. What is missing specifically in relation to Proxmox?
mkdir patchmon && cd patchmon
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/PatchMon/PatchMon/main/docker/docker-compose.yml
curl -fsSL -o env.example https://raw.githubusercontent.com/PatchMon/PatchMon/main/docker/env.example
cp env.example .env
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
# Example for when the server runs on 192.168.6.19
SERVER_PROTOCOL=http
SERVER_HOST=192.168.6.19
SERVER_PORT=3333
CORS_ORIGIN=http://192.168.6.19:3333
nano docker-compose.yml
# Locate frontend:
# Change from:
ports:
- "3000:3000"
# To something like:
ports:
- "3333:3000"
docker compose up
docker compose up --force-recreate .jan@proxmox2:~$ curl -s "http://192.168.6.19:3333/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=patchmon_ae_XYZ&token_secret=XYZ"| sudo bash
VMs and bare-metal devices that are on FreeBSD will not work with an automated ‘master’ auto-enrollment token. We will need to enroll them manually using individual keys. While this process could be automated (by creating a token request for each device), if you just have a few, it is often faster to do it manually. Here's how to go about it:
bash but sh only and the syntax is slightly different than on Linux.pkg install curl
# Test that traffic gets through to your PatchMon's IP and port number:
nc -zvw 5 192.168.6.19 3333
# If you get 'Network is unreachable', it means that no route to that host available.
# The first IP is your PatchMon's, the second of y our primary OPNSense node:
route add 192.168.6.19 192.168.8.1
# Now run the enrollment command!
# Once done, remove the route, as during a switch-over event, we would not want to go via
# the now-master unit.
route delete 192.168.6.19
# Remove config, auth files and logs:
rm -rf /etc/patchmon/*
# Stop the agent:
service patchmon_agent stop
Notice that only LXC containers are handled by the Proxmox enrollment automation. For VMs and other bare-metal or virtualized hosts, we would need to either install them manually (via the ‘Hosts’ section) or we can leverage Ansible with AWX, Semaphore or another tool at your disposal.
For all your Linux-based devices, create an auto-enrollment token that will result in the host getting its own set of token key and secret, as those need to be unique per host. We will then use this token as part of the Ansible automation playbook.
VM & Bare-Metal Fleet.Auto-Enrollment.Assuming you already have:
Add the following simple YAML script that can then be pushed to your fleet:
---
- name: Deploy on all new hosts to maintain fleet in PatchMon
hosts: all
become: yes
vars:
patchmon_server: "http://192.168.6.19:3333"
# token_key and token_secret need to be available in Semaphore as a Variable Group (Secrets)
tasks:
- name: 1. Identify Service Name
set_fact:
patchmon_service: "{{ (ansible_os_family == 'FreeBSD') | ternary('patchmon', 'patchmon-agent') }}"
- name: 2. Check if PatchMon is already installed and running
# We use 'command' instead of 'service_facts' for speed and reliability across OS types
shell: "service {{ patchmon_service }} status"
register: agent_status
failed_when: false
changed_when: false
- name: 3. Deployment Block (Only runs if agent is missing)
block:
- name: 3a. Download and Execute enrollment script
shell: >
curl -s "{{ patchmon_server }}/api/v1/auto-enrollment/script?type=direct-host&token_key={{ lookup('env','PATCHMON_TOKEN_KEY') }}&token_secret={{ lookup('env','PATCHMON_TOKEN_SECRET') }}" | sh
register: enrollment_output
- name: 3b. Log Enrollment Result
debug:
msg: "Host successfully enrolled: {{ ansible_hostname }}. Remember to manually toggle the 'Docker' option if it runs Docker, as the API currently does not support this option."
# This condition prevents the "Endless Duplication" problem
when: true # Temporary to re-install after server wipe
#- "'running' not in agent_status.stdout"
#- "'is running' not in agent_status.stdout"
#- "agent_status.rc != 0"
- name: 4. Ensure Service is enabled
service:
name: "{{ patchmon_service }}"
state: started
enabled: yes
Firstly, ensure that the new playbook is synced into your preferred Automation Platform. Just for completion, both Semaphore UI and AWX are covered here:
Go to Variable Groups → New Group
Call it something like ‘Patchmon enrollment token’
Go to Secrets and add the two environmental variables:
PATCHMON_TOKEN_KEYPATCHMON_TOKEN_SECRETProvide the values from the Master Auto-Enrollment Token that we created earlier in Patchmon.
patching/enroll_patchmon.yml.ALL Sites & Clusters)Patchmon enrollment token PatchMon - Auto Enrollment.enroll_patchmon.yml from the dropdown.Ansible SSH Key (depending on how you named it).---
token_key: "your_key"
token_secret: "your_actual_secret"
Once your hosts have been added in, you should see them in Patchmon. In my experience, docker hosts do not get automatically spotted. I had to open each host that has docker containers and went to the ‘Integrations’ tab and enabled it, as shown below:
In case you want to utilize the Groups feature, now would be a good time to set it up as you see fit - we will not be actually using the groups for the automation, as it is not possible to update Patchmon groups via API as of version 2.0.2.
For those less experienced with Docker in Proxmox VMs, if you desire to update your Patchmon docker instance, here is how to approach it based on how we installed it. In this example, we will update from version 2.0.2 to version 2.1.1 released on August 15, 2026.
Assuming you are not using any SSO service (if yes, please read the release notes, as some services need manual changes first),
docker-compose.yml file to ensure it follows the current instance:nano /opt/patchmon/docker-compose.yml
# Under 'server:', add a hostname:
name: patchmon
services:
server:
**image: ghcr.io/patchmon/patchmon-server:2.1.0**
restart: unless-stopped
** hostname: patchmon-server**
..
guacd for higher stability....
guacd:
**image: guacamole/guacd:1.6.0**
restart: unless-stopped
read_only: true
..
cd /opt/patchmon && docker compose pull
docker compose up -d
# Watch the logs:
docker compose logs -f server
docker image ls
# Find out of date images and remove them - an example is below:
docker rmi \
ghcr.io/patchmon/patchmon-backend:latest \
ghcr.io/patchmon/patchmon-frontend:latest
# There are usually more images that are not in use and can be safely removed.