Skip to main content
PathMon

Chapter 1 of 15

Installing PatchMon Server on Docker

Updated Read the full guide

Overview

PatchMon runs as a single container backed by three supporting services. The PatchMon server binary serves both the API and the embedded React frontend. There is no separate frontend container.

Service Image Purpose
server ghcr.io/patchmon/patchmon-server PatchMon application (API + frontend + migrations)
database postgres:17-alpine Primary data store
redis redis:7-alpine Background job queues (asynq)
guacd guacamole/guacd:1.6.0 RDP gateway (required for in-browser RDP)

All four services communicate over an isolated internal Docker network (patchmon-internal). Only the server port is exposed to the host.


Prerequisites

  • Docker Engine 24+ and Docker Compose v2
  • A reverse proxy with a valid TLS certificate (Nginx Proxy Manager, Traefik, Caddy, or similar), strongly recommended for any non-localhost deployment
  • Minimum 1 GB RAM, 2 GB recommended

Quick Start

1. Run the setup script

The fastest way to get started is the official setup script. It downloads the compose file, generates secrets, and creates your .env in one step:

mkdir patchmon && cd patchmon
bash -c "$(curl -fsSL https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/docker/setup-env.sh)"

Once it completes, skip to step 3.

2. Manual setup (alternative)

If you prefer to set things up yourself:

mkdir patchmon && cd patchmon

# Download the compose file and example env
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

# Create your .env and generate the three required secrets
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

3. Configure your access URL

Open .env and set CORS_ORIGIN to the full URL that PatchMon will be reachable at in a browser. This is the only URL-related env var the server reads from .env:

CORS_ORIGIN=https://patchmon.example.com

For a local test without a reverse proxy:

CORS_ORIGIN=http://localhost:3000

If users reach PatchMon on more than one URL (for example an external domain and an internal LAN address), comma-separate the values with no spaces:

CORS_ORIGIN=https://patchmon.example.com,https://patchmon.internal.lan

After first login, go to Settings in the PatchMon UI to configure the server URL that agents use to connect back (protocol, host, port). These are stored in the database, not in .env. The Settings page is also where you configure update intervals, auto-update behaviour, and other server-level options.

Tip: Do not edit the docker-compose.yml file to add env vars. The compose file uses env_file: .env to pass your entire .env into each container. All configuration lives in .env.

For the full list of available environment variables (rate limiting, logging, OIDC SSO, TOTP, database pool tuning, session timeouts, and more), see the PatchMon Environment Variables Reference.

4. Start PatchMon

docker compose up -d

Docker will pull the images, wait for each service to pass its health check, and start the server. The server automatically runs database migrations on startup.

Once all containers are healthy, open the URL you configured in your browser and complete the first-time setup to create your admin account.

You can check the startup logs at any time with:

docker compose logs -f server

Upgrading from 2.0.2 or earlier? ENABLE_LOGGING used to default to false, and with it off the server wrote no application logs at all, which made every "check the logs" step in this guide useless. From 2.0.3 it defaults to true. If you explicitly set ENABLE_LOGGING=false in your .env, or turned logging off in Settings > Environment, that choice is preserved and you will still see no logs.


Container Image

The PatchMon server image is published at:

ghcr.io/patchmon/patchmon-server

Available Tags

Tag Description
latest Latest stable release
x.y.z Exact version pin (e.g. 1.5.0)
x.y Latest patch in a minor series (e.g. 1.5)
x Latest minor and patch in a major series (e.g. 1)
edge Latest development build from the main branch. Unstable, for testing only.

Compose File Reference

This is the production docker-compose.yml used by PatchMon. You do not need to edit it; all configuration is controlled via your .env file.

name: patchmon

services:

  server:
    image: ghcr.io/patchmon/patchmon-server:latest
    restart: unless-stopped
    env_file: .env
    ports:
      - "${PORT:-3000}:${PORT:-3000}"
    networks:
      - patchmon-internal
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
      guacd:
        condition: service_healthy

  database:
    image: postgres:17-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - patchmon-internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    env_file: .env
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - patchmon-internal

  guacd:
    image: guacamole/guacd:1.6.0
    restart: unless-stopped
    networks:
      - patchmon-internal

volumes:
  postgres_data:
  redis_data:

networks:
  patchmon-internal:
    driver: bridge

Volumes

Volume Purpose
postgres_data PostgreSQL data directory
redis_data Redis persistence

Agent binaries and SCAP compliance content are embedded directly in the patchmon-server image. No additional volumes are required for them.

You can bind either volume to a host path by editing the compose file:

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/patchmon/postgres

Note: The server container runs as a non-root user. If you bind volumes to host paths, ensure that user has read/write access to those directories.


Updating PatchMon

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

docker compose pull
docker compose up -d

This pulls the latest image, recreates the server container, and runs any new database migrations automatically on startup. Your data is preserved in the named volumes.

Pinning to a specific version

If you prefer to control when you update, pin the image tag in your docker-compose.yml:

services:
  server:
    image: ghcr.io/patchmon/patchmon-server:1.5.0

Then pull and restart when you are ready to upgrade:

docker compose pull
docker compose up -d

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


Reverse Proxy Setup

PatchMon listens on port 3000 inside the container, mapped to port 3000 on the host by default. Point your reverse proxy at http://<host>:3000.

When deploying behind a reverse proxy, ensure:

  • WebSocket connections are proxied correctly. PatchMon uses WebSockets for agent communication, SSH terminal, and RDP sessions.
  • The X-Forwarded-For, X-Forwarded-Proto, and Host headers are forwarded so PatchMon can construct correct URLs

If you use Nginx Proxy Manager, enable "Websockets Support" on the proxy host entry.


Troubleshooting

Server fails to start: database connection refused

The server waits for the database health check before starting, but if it still fails:

# Check that the database container is healthy
docker compose ps

# Check database logs
docker compose logs database

Verify that POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB in your .env are set and consistent.

Server fails to start: Redis connection refused

docker compose logs redis

Verify that REDIS_PASSWORD in your .env is set. The Redis container uses this value in its startup command (--requirepass).

Checking server logs

# Follow live logs
docker compose logs -f server

# Last 100 lines
docker compose logs --tail=100 server

Resetting to a clean state

Warning: This deletes all PatchMon data. Only do this on a fresh install where you want to start over.

docker compose down -v
docker compose up -d

Port 3000 already in use

Change the host-side port in docker-compose.yml:

ports:
  - "8080:3000"   # Expose on host port 8080 instead

Update SERVER_PORT in your .env to match if agents need to reach the server directly on that port.