> ## Content Index
> Fetch the complete content index at: https://blog.uptime.bet/llms.txt
> Use this file to discover other available public pages before exploring further.

# PatchMon: Self-Hosted Linux Patch Management for Your Homelab with Docker Compose
- URL: https://blog.uptime.bet/patchmon-self-hosted-linux-patch-management-docker/
- Published: 2026-10-27T08:12:51.000Z
- Updated: 2026-10-27T08:12:51.000Z
- Description: PatchMon shows which of your Linux hosts need updates and lets you dry-run, approve and run patches from one web UI. Here's how to deploy v2.1.6 with Docker Compose, plus the security trade-offs to keep in mind.
- Author: Alexandre Nebeling
- Tags: Trending, Self-Hosted, Open Source, Docker, Homelab, Security

Here's a question most homelabbers would rather not answer honestly: when did you last run updates on *every* box you own? Not the main server you SSH into daily. The Pi in the cupboard, the VM you spun up for one experiment, the LXC that's been quietly running Unbound since last spring.

That's the gap [PatchMon](https://github.com/PatchMon/PatchMon?ref=blog.uptime.bet) tries to close. It's an open-source, self-hosted platform that shows you which of your Linux machines have pending updates, which of those are security updates, and lets you approve and run the patching from a web UI. It was the Content Spotlight in [Self-Host Weekly on 23 October](https://selfh.st/weekly/2026-10-23/?ref=blog.uptime.bet), has passed 3,500 stars on GitHub, and shipped three releases in the past two weeks. Time for a proper look.

## What PatchMon actually does

The architecture is simple. You run one PatchMon server (a single Go binary with the React frontend embedded, plus PostgreSQL and Redis). On every machine you want to watch, you install a small agent. The agent only ever talks *outbound* to the server over HTTPS and WebSockets, so you don't need to open ports or hand out SSH keys to the monitored hosts. That's a nice property if some of your machines sit behind NAT or in a different network.

Once the agents report in, you get an inventory of every host, its OS and kernel, every installed package, and the repositories each machine pulls from. The dashboard tells you at a glance how many hosts are up to date, how many need updates and how many are waiting for a reboot.

![PatchMon dashboard showing host counts, update status, OS distribution and package trends](https://blog.uptime.bet/content/images/2026/10/patchmon-dashboard.png)

The PatchMon dashboard (demo data). Screenshot: PatchMon project, [GitHub repository](https://github.com/PatchMon/PatchMon?ref=blog.uptime.bet), AGPL-3.0.

### Patching, not just monitoring

Monitoring is only half of it. PatchMon can also run the updates for you. The flow is deliberately cautious: you pick hosts and packages, PatchMon does a dry run that shows the exact package transaction, and only after someone approves it does the agent actually install anything. You can patch everything, only specific packages, or security updates only, and policies let you push a run into a maintenance window instead of running it right away. While a run is going, the agent streams its output to your browser, and every run ends up in a searchable history with who approved it.

Package managers covered right now are apt, dnf, yum, apk and pacman on Linux plus pkg on FreeBSD. Version 2.1.4 added patching for Windows, Alpine and openSUSE as well.

![PatchMon patching overview with run status, outcomes, recent runs and pending approvals](https://blog.uptime.bet/content/images/2026/10/patchmon-patching.png)

Patch runs, outcomes and approvals in one view (demo data). Screenshot: PatchMon project, [GitHub repository](https://github.com/PatchMon/PatchMon?ref=blog.uptime.bet), AGPL-3.0.

### The extras

There's a lot more bolted on, and you can ignore most of it if you only want update tracking:

- Docker discovery of containers, images, volumes and networks on each host
- Compliance scans using OpenSCAP CIS benchmarks and Docker Bench for Security
- Alerts for hosts going offline or piling up updates, with webhook destinations
- A browser-based SSH terminal that can be proxied through the agent, and in-browser RDP for Windows hosts (beta)
- Role-based access control, TOTP two-factor and OIDC single sign-on (Authentik, Keycloak and friends)
- A REST API and integrations, including a card for the [Homepage dashboard](https://blog.uptime.bet/homepage-dashboard-homelab-docker/) and Proxmox LXC auto-enrolment

There's also an optional AI assistant inside the SSH terminal that works with OpenRouter, Anthropic, OpenAI or Google Gemini. You bring your own API key. More on that in the security section.

## Before you start

The project's README lists 2 vCPUs, 2 GB of RAM and 15 GB of disk as minimum server specs. The operator guide is a bit more relaxed (1 GB minimum, 2 GB recommended), but I'd plan for 2 GB since you're running Postgres and Redis alongside it. You'll also need Docker Engine 24 or newer with Compose v2.

For anything beyond a quick test, put a reverse proxy with a valid TLS certificate in front of it. Agents carry your package inventory and receive patch commands, so plain HTTP across your network isn't a great idea. In this guide I'll use `patchmon.example.com` as the hostname. Replace it with your own.

The current stable release at the time of writing is **v2.1.6** (17 October 2026), and that's the version pinned below.

## Installing PatchMon with Docker Compose

PatchMon offers a setup script that downloads the compose file, generates secrets and writes your `.env` interactively. It's convenient, but it fetches files from the `main` branch and the compose file defaults to the `latest` tag. I prefer knowing exactly what I'm running, so here's the manual route with everything pinned to 2.1.6.

### Step 1: Grab the compose file and env template

```bash
mkdir -p ~/patchmon && cd ~/patchmon

curl -fsSL -o docker-compose.yml \
  https://raw.githubusercontent.com/PatchMon/PatchMon/v2.1.6/docker/docker-compose.yml
curl -fsSL -o env.example \
  https://raw.githubusercontent.com/PatchMon/PatchMon/v2.1.6/docker/env.example
```

### Step 2: Pin the server image

The official compose file uses `ghcr.io/patchmon/patchmon-server:latest`. Pin it to the exact version so a `docker compose pull` never surprises you:

```bash
sed -i 's#patchmon-server:latest#patchmon-server:2.1.6#' docker-compose.yml
grep 'image:' docker-compose.yml
```

You should see `patchmon-server:2.1.6`, `postgres:17-alpine`, `redis:7-alpine` and `guacamole/guacd:1.6.0`.

For reference, this is what the file looks like afterwards. I've removed the comments and the per-service `logging` blocks (the official file rotates JSON logs at 10 MB, three files) to keep it readable, so don't copy this over the downloaded file.

```yaml
name: patchmon

services:

  server:
    image: ghcr.io/patchmon/patchmon-server:2.1.6
    restart: unless-stopped
    hostname: patchmon-server
    env_file: .env
    environment:
      GUACD_ADDRESS: "127.0.0.1:4822"
    ports:
      - "${PORT:-3000}:${PORT:-3000}"
    networks:
      - patchmon-internal
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy

  database:
    image: postgres:17-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - patchmon-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 3s
      timeout: 5s
      retries: 7

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    env_file: .env
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - patchmon-internal
    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 3s
      timeout: 5s
      retries: 7

  guacd:
    image: guacamole/guacd:1.6.0
    restart: unless-stopped
    network_mode: "service:server"
    depends_on:
      server:
        condition: service_started
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        /opt/guacamole/entrypoint.sh -b 127.0.0.1 & pid=$$!
        trap 'kill $$pid; wait $$pid; exit 0' TERM INT
        while kill -0 $$pid 2>/dev/null && ls /sys/class/net | grep -q '^eth'; do sleep 5 & wait $$!; done
        kill $$pid 2>/dev/null; exit 1
    read_only: true
    tmpfs:
      - /tmp:size=64m
      - /home/guacd:size=16m,uid=1000,gid=1000
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    mem_limit: 512m
    cpus: '1.0'
    healthcheck:
      test: ["CMD-SHELL", "nc -z 127.0.0.1 4822 && ls /sys/class/net | grep -q '^eth' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s

volumes:
  postgres_data:
  redis_data:

networks:
  patchmon-internal:
    driver: bridge
```

A few things worth knowing. Only the `server` service publishes a port (3000 by default, controlled by `PORT`). Postgres and Redis stay on the internal network. The `guacd` container is only there for in-browser RDP to Windows machines. It shares the server's network namespace because the server hands it a per-session port on loopback. The odd-looking shell loop makes it exit and restart cleanly when the server container is recreated. If you never plan to RDP into anything from PatchMon, it just idles.

### Step 3: Create the .env and generate secrets

The env template has five secrets that start out empty. The operator guide's manual setup only generates the first three (database password, Redis password, JWT secret), but the checklist at the top of `env.example` asks for all five, so let's fill them all:

```bash
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
sed -i "s/^SESSION_SECRET=$/SESSION_SECRET=$(openssl rand -hex 64)/" .env
sed -i "s/^AI_ENCRYPTION_KEY=$/AI_ENCRYPTION_KEY=$(openssl rand -hex 64)/" .env

chmod 600 .env
```

On macOS, BSD `sed` needs `sed -i ''` instead of `sed -i`. Either way, open the file afterwards and check that none of the five values is still empty.

### Step 4: Set the URL you'll use

`CORS_ORIGIN` has to match the URL you type into your browser, exactly, including the scheme. Edit `.env` and change the default line to:

```bash
CORS_ORIGIN=https://patchmon.example.com
```

If you reach PatchMon under more than one name (say, a public domain and a LAN name), list them comma-separated without spaces. For a local test with no proxy, `http://localhost:3000` works. Everything else, like the URL agents use to connect, is configured later in the web UI and stored in the database.

### Step 5: Start the stack

```bash
docker compose up -d
docker compose ps
docker compose logs -f server
```

Compose waits for Postgres and Redis to pass their health checks before starting the server, and the server runs its database migrations on start. Once the logs settle, all four containers should show as running.

### Step 6: Put a reverse proxy in front

PatchMon serves the UI, the API and all WebSocket endpoints from the one port. Your proxy needs to terminate TLS, pass WebSocket upgrades through, send `X-Forwarded-Proto`, and allow long idle connections. The last point is the one people miss: agents keep a control channel open, and a proxy's default 60-second idle timeout will keep cutting it. The docs recommend 24 hours.

With Caddy on the same host, the official example boils down to this:

```bash
patchmon.example.com {
    reverse_proxy 127.0.0.1:3000 {
        transport http {
            read_timeout 86400s
            write_timeout 86400s
        }
    }
}
```

Caddy handles the certificate and WebSocket upgrades on its own. For Nginx, set `proxy_read_timeout 86400s`, add the usual `Upgrade`/`Connection` headers and raise `client_max_body_size` to 40m, because compliance scan results can be large. The [operator guide](https://patchmon.net/docs/patchmon-operator-guide?ref=blog.uptime.bet#reverse-proxy-examples) has complete examples for Nginx, Traefik and Nginx Proxy Manager. If you use Nginx Proxy Manager, enable "Websockets Support" on the proxy host.

### Step 7: Run the first-time wizard

Open `https://patchmon.example.com`. On a fresh install you get a setup wizard instead of a login page. It creates your superadmin account, offers TOTP two-factor (say yes, and store the backup codes outside your authenticator app), and asks you to confirm the server URL. That URL is what the agents will connect to, and it gets baked into every install command PatchMon generates. With a proxy in place it's usually protocol HTTPS, your hostname and port 443.

There's a toggle for self-signed certificates. It makes the generated install command use `curl -k` and also turns off TLS verification in the agent. The docs themselves warn against it outside a lab. If you run an internal CA, add it to the trust store on your hosts instead.

### Step 8: Enrol your first host

In the UI, go to **Hosts** and add a host. PatchMon generates a one-line install command that includes a per-host API key. Run it as root (or with sudo) on the target machine, and the agent installs itself as a service and checks in. Agents exist for Linux, FreeBSD and Windows on amd64, 386, arm64 and arm, so a Raspberry Pi is fine.

If an agent doesn't show up, look at `journalctl -u patchmon-agent` on the host first. The usual suspects are the proxy (short timeouts or missing WebSocket support, which the docs' troubleshooting section covers) or a server URL that the host can't resolve.

## Practical tips

**Don't publish port 3000 to the whole LAN if you don't need to.** When Caddy or Nginx runs on the same host, you can change the server's port mapping to `"127.0.0.1:${PORT:-3000}:${PORT:-3000}"` so only the proxy can reach it. That's my own tweak, not part of the official file, but it lines up with the docs' advice. PatchMon trusts `X-Forwarded-For` from private addresses by default, so a client on your LAN that talks to port 3000 directly could fake its IP for rate limiting and the audit log. If you can't restrict the port, set `TRUSTED_PROXY_RANGES` to your proxy's exact address.

**Upgrade deliberately.** With the tag pinned, an upgrade means checking the [release notes](https://github.com/PatchMon/PatchMon/releases?ref=blog.uptime.bet), bumping the version in `docker-compose.yml`, then running `docker compose pull && docker compose up -d`. Read the notes every time. Version 2.1.4, for example, changed how `guacd` is wired in the compose file, and a plain pull doesn't touch your compose file. Upgrade the server before the agents, too.

**Back up the Postgres volume.** Everything that matters (hosts, history, settings, users) lives in `postgres_data`. A nightly `pg_dump` from the database container is enough for a homelab. Redis only holds job queues.

**Turn on agent auto-update.** It lives under Settings > Agent Updates. The 2.1.4 notes mention that hosts added since 2.0.0 were created with it switched off, so check older enrolments.

**Use groups and start with security-only runs.** Put your "boring" hosts into a group and try a security-only patch run with a dry run first. You'll see quickly how much you trust it before pointing it at anything important.

## Limits and security considerations

Let's be clear about what you're deploying. PatchMon is a central server that can tell every enrolled machine to install packages, and the agent needs root to do that. That's the whole point, and it's also why the server becomes one of the most sensitive services in your homelab. Whoever controls the admin account controls patching on every host, plus a browser SSH terminal into them. Enable two-factor, keep the UI off the public internet (or behind SSO and a VPN), and don't give out admin roles casually.

The project's own track record is reassuring and a reminder at the same time. The 2.1.4 release fixed a script injection through uploaded SVG logos, a privilege escalation through roles and SSO settings, and several sign-in weaknesses, all found in an internal audit. Good that they look. But it also means you want to stay current, which is a bit ironic for a patching tool you have to patch.

The AI terminal assistant is opt-in and needs an API key from one of the supported providers. If you enable it, assume that whatever you ask it about, including command output from your servers, is sent to that provider. For a homelab that may be fine. For anything with customer data, think twice.

The admin guide describes an optional anonymous telemetry heartbeat (server version, host count, rough OS distribution, once a day). You control it under Settings > Metrics.

Some other honest notes:

- Windows patching and in-browser RDP are new. RDP is still labelled beta.
- The project is moving fast, with three releases between 16 and 17 October alone. That's great for fixes, less great if you want a quiet tool you can forget about.
- The repository publishes an AI declaration: AI models help with design, code, tests and documentation, every change is supposed to get human review before it lands in `main`, and outside contributors have to disclose AI use. Whether that matters to you is your call, but at least it's documented openly.
- There's a commercial side: a hosted PatchMon Cloud and PatchMon PRO for enterprise support. The self-hosted server is AGPL-3.0 and free to run.

## How it compares

The closest thing most homelabbers already use is **Ansible**, maybe with a playbook that runs `apt upgrade` across an inventory. It's far more flexible and has no agent, but it gives you no dashboard of pending updates, no approval step and no history unless you build it yourself (or add a UI like AWX or Semaphore).

**unattended-upgrades** on Debian/Ubuntu or **dnf-automatic** on Fedora/RHEL are the zero-infrastructure answer. They patch automatically, but each host does its own thing and you have no central view of who's behind.

On the heavyweight end there's **Foreman with Katello** and **Uyuni**, the open-source upstream of SUSE Multi-Linux Manager. They do content management, provisioning and patching for large fleets and are proven tools, but they're a lot to run for ten machines in a rack in the basement.

PatchMon sits in the middle: a single compose stack, a friendly UI, real approval-based patching, and multi-distro support. For a homelab or a small team, that's a sweet spot that was oddly empty for a long time.

## Conclusion

PatchMon solves a boring problem well, and boring problems are the ones that bite you. The outbound-only agent makes enrolment painless, the dry-run-then-approve flow is the right default, and the Docker setup is quick if you pin the version and get the proxy timeouts right. Just remember what you've built: a control plane with root on every host. Treat the PatchMon server with the same care as your password manager, keep it updated, and it'll probably earn its place.

Already using it, or still happy with your Ansible playbook? I'd like to hear how you keep your fleet patched.

## Sources

- [PatchMon on GitHub](https://github.com/PatchMon/PatchMon?ref=blog.uptime.bet) (README, features, minimum specs)
- [PatchMon license (AGPL-3.0)](https://github.com/PatchMon/PatchMon/blob/main/LICENSE?ref=blog.uptime.bet)
- [PatchMon v2.1.6 release notes](https://github.com/PatchMon/PatchMon/releases/tag/v2.1.6?ref=blog.uptime.bet)
- [PatchMon v2.1.4 release notes](https://github.com/PatchMon/PatchMon/releases/tag/v2.1.4?ref=blog.uptime.bet) (security fixes, Windows/Alpine/openSUSE, guacd change)
- [Official docker-compose.yml at v2.1.6](https://github.com/PatchMon/PatchMon/blob/v2.1.6/docker/docker-compose.yml?ref=blog.uptime.bet)
- [Official env.example at v2.1.6](https://github.com/PatchMon/PatchMon/blob/v2.1.6/docker/env.example?ref=blog.uptime.bet)
- [PatchMon Operator Guide](https://patchmon.net/docs/patchmon-operator-guide?ref=blog.uptime.bet) (Docker install, reverse proxy examples, first-time setup)
- [PatchMon Admin Guide](https://patchmon.net/docs/patchmon-admin-guide?ref=blog.uptime.bet) (settings, telemetry)
- [PatchMon AI Declaration](https://github.com/PatchMon/PatchMon/blob/main/AI-DECLARATION.md?ref=blog.uptime.bet)
- [Self-Host Weekly, 23 October 2026](https://selfh.st/weekly/2026-10-23/?ref=blog.uptime.bet) (Content Spotlight)
- [The Foreman](https://theforeman.org/?ref=blog.uptime.bet), [Uyuni](https://www.uyuni-project.org/?ref=blog.uptime.bet), [Debian Wiki: UnattendedUpgrades](https://wiki.debian.org/UnattendedUpgrades?ref=blog.uptime.bet), [Ansible](https://github.com/ansible/ansible?ref=blog.uptime.bet)