Chapter 1 of 15
Installing PatchMon Server on Docker
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.ymlfile to add env vars. The compose file usesenv_file: .envto pass your entire.envinto 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_LOGGINGused to default tofalse, 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 totrue. If you explicitly setENABLE_LOGGING=falsein 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, andHostheaders 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.