Monitoring for one Linux machine and its Docker containers. One small Go binary, no accounts, no cloud, no agents to install.
Run it on the host you want to watch and it serves a live dashboard of the machine and everything running on it. It only reports on the box it runs on, so to watch several machines, run one instance on each.
- CPU, memory, swap, load, uptime, disk usage per mount.
- NVIDIA GPU utilization, VRAM, temperature, power draw.
- Containers from the Docker API: state, health, live CPU/memory, ports.
- Per-container log tails.
- Incoming HTTP requests, read from nginx proxy manager's access logs.
- Health checks against your own URLs, with latency and uptime.
- A "curl from the browser" for hitting services from inside the network.
- A read-only Redis console: run
SCAN,INFO,CLIENT LISTand friends against any Redis on the network, from the dashboard. - Alerts to a Discord, Slack, or Telegram webhook when a check fails, a container turns unhealthy, or a disk fills up.
- OpenAI and Anthropic API spend, if you supply org admin keys.
- Optional login with a shared key or GitHub OAuth.
export PERCH_KEY=$(openssl rand -hex 24)
docker compose up -dThat starts Perch plus a read-only Docker socket proxy.
The dashboard listens on 127.0.0.1:3535, so it is reachable from the host but not from the internet.
To see it from your laptop:
ssh -L 3535:localhost:3535 you@your-serverFor permanent access, put it behind a reverse proxy over HTTPS.
Don't publish 3535:3535 on a public server; published Docker ports bypass ufw/firewalld.
Health checks are added from the dashboard and persist in the /data volume.
go run .Then open http://localhost:3535.
Auth is off unless ACCESS_KEY is set.
On WSL2 you need Docker Desktop's WSL integration enabled for the Containers tile to work.
A .env file in the working directory is loaded on startup; the real environment wins over it.
| Env var | Default | Purpose |
|---|---|---|
ACCESS_KEY |
(empty = auth off) | login key; compose wires PERCH_KEY into it |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
(empty = off) | GitHub OAuth App credentials |
GITHUB_ALLOWED_USERS |
(empty) | usernames allowed to sign in, comma-separated |
GITHUB_ALLOWED_EMAILS |
(empty) | verified emails allowed to sign in, comma-separated |
RATE_LIMIT / RATE_WINDOW_SECONDS |
50 / 10 |
unauthenticated requests per IP per window |
BAN_HOURS |
10 |
how long an over-limit IP stays banned |
BAN_FILE |
/data/banned_ips.txt |
ban persistence |
PORT |
3535 |
listen port |
CHECKS_YAML |
(empty) | health checks declared up front, instead of adding them in the UI |
NPM_LOG_DIR |
/npm-logs |
nginx proxy manager access logs |
REDIS_TARGETS |
(empty) | Redis addresses the console may reach, comma-separated (cache=redis:6379, redis-2:6379); empty means any address, with no stored credentials |
REDIS_USERNAME / REDIS_PASSWORD |
(empty) | credentials for the REDIS_TARGETS above; only ever sent to those addresses. Typed per query otherwise |
ALERT_WEBHOOK_URL |
(empty = alerts off) | Discord, Slack, or Telegram webhook; also settable in the UI |
ALERT_DISK_PCT |
90 |
percent-full at which a disk alerts |
OPENAI_ADMIN_KEY / ANTHROPIC_ADMIN_KEY |
(empty = tab off) | org admin keys for the spend tab, read server-side only |
DOCKER_HOST |
(unix socket) | tcp://socket-proxy:2375 for proxied access |
HOST_PROC / HOST_SYS / HOST_ROOT |
(the container's own /proc, /sys, /) |
where the host's proc/sys/rootfs are mounted; the compose file sets these to /host/* |
NVIDIA_SMI_PATH |
(auto-detect) | explicit nvidia-smi location |
For GitHub login, create an OAuth App with callback URL https://<your-domain>/api/auth/github/callback, then set the client ID, secret, and an allowlist.
Only usernames and verified emails are matched, never display names.
The Docker socket is never mounted into Perch.
A socket-proxy forwards read-only GET /containers/* calls and rejects everything else, so a hijacked Perch cannot start, stop, or remove containers.
The container drops all capabilities and runs with a read-only root filesystem and read-only host mounts.
Perch only reads. There is no start, stop, restart, or redeploy; every Docker call it makes is a GET.
The Redis console does not use docker exec.
Perch speaks the Redis protocol over the network, so the console needs no Docker privileges at all, and the command allowlist lives in the server.
Commands that write, reconfigure, run scripts, or stop the server are refused before a connection is opened.
Credential values in a CONFIG GET reply are redacted, matched on the shape of the parameter name so a renamed spelling does not slip through.
REDIS_USERNAME/REDIS_PASSWORD are only ever sent to an address listed in REDIS_TARGETS.
Without that list Perch never sends them at all, and you type the password per query instead.
This is deliberate: the console target is free text, so a stored credential paired with an arbitrary address would let any logged-in user point Perch at a host they control and collect it.
Setting REDIS_TARGETS also stops the console reaching anything else on your network.
It can still read your data, which is the point of it: anyone who can log in to Perch can read any Redis Perch can reach. Give Perch a read-only Redis ACL user and let Redis enforce the limit too.
The API-calls feature sends its requests from the Perch container, so a logged-in user can reach anything the container can reach on your internal network, including a cloud provider's metadata endpoint.
MIT.
