Release v0.8.0 — Portainer-first install, in-app guide

- Install from Portainer with no command line: paste the compose, set a
  folder and a password, deploy. Inspector creates its own config and
  detects Docker socket permissions by itself.
- Illustrated how-to guide inside the app (Help tab) and in the repo.
- Containers added in the UI are watched right away, no restart.
- maintenance_window switch on the container form.
- README with logo and screenshots; INSTALL rewritten for Portainer.
This commit is contained in:
Europium 63 committed 2026-10-27 15:45:33 -07:00
1 parent 4c05a4d565
commit c603e86e75
21 files changed
+788 -201

No files matched your search

+41
View File
@@ -7,6 +7,47 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
---
## [0.8.0] — 2026-10-27
### Added
- **Install from Portainer with no command line.** Mount one folder per purpose
(`/app/config`, `/app/logs`, `/app/data`) and deploy. If there's no config yet,
Inspector writes a starter `config.yml` (dashboard on, event history on, no
containers). Dashboard login comes from `INSPECTOR_USERNAME` / `INSPECTOR_PASSWORD`;
without a password, one is generated and printed once in the container logs.
- **Docker socket access is automatic.** A new entrypoint starts as root only long
enough to read the socket's group and fix ownership of the mounted folders, then
runs Inspector as `PUID:PGID` (default `1000:1000`) with that group added.
`DOCKER_GID` / `group_add` are no longer needed.
- **In-app guide** at `/guide/` with screenshots, linked from a new **Help** tab
in the header. Also in the repo at `src/inspector/assets/guide/guide.html`.
- **`maintenance_window` switch** on the container form, so containers can opt in
to the daily maintenance window without editing YAML.
### Fixed
- Containers added from the UI are now watched on the next check. Before, the
loop only logged that a restart was needed, despite what the UI said.
Containers removed or disabled from the UI are dropped the same way.
- Status page could fail to render while a container was being added or removed
(it iterated the live state dict and assumed every entry was still in config).
### Changed
- Config path: `--config`, then `$INSPECTOR_CONFIG`, then `/app/config.yml` if
present (existing single-file setups keep working unchanged), then
`/app/config/config.yml`.
- Public `compose.yml`, `INSTALL.md`, and `README.md` rewritten for Portainer
stacks; README gains the logo and screenshots.
### Upgrade notes
- Existing deployments need no changes. To adopt the simpler layout, move
`config.yml` into the new config folder and switch to the new compose file.
---
## [0.7.4] — 2026-10-27
### Changed
+9 -4
View File
@@ -19,9 +19,14 @@ ARG INSPECTOR_UID=1000
ARG INSPECTOR_GID=1000
RUN groupadd --gid ${INSPECTOR_GID} inspector \
&& useradd --uid ${INSPECTOR_UID} --gid inspector --home-dir /app --shell /usr/sbin/nologin inspector \
&& mkdir -p /app/logs /app/data \
&& chown -R inspector:inspector /app/logs /app/data
&& mkdir -p /app/config /app/logs /app/data \
&& chown -R inspector:inspector /app/config /app/logs /app/data
USER inspector
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh
CMD ["python", "-m", "inspector.main", "--config", "/app/config.yml"]
# Starts as root only to detect the Docker socket's group and fix mounted
# folder ownership, then drops to PUID:PGID (default 1000:1000).
# See docker-entrypoint.sh.
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["python", "-m", "inspector.main"]
+115 -50
View File
@@ -1,70 +1,135 @@
# Install
# Installing Inspector with Portainer
Inspector watches Docker containers, restarts the ones you tell it to,
and notifies you via Gotify and/or Pushover. All configuration lives in
a single `config.yml` that you can edit by hand or from the built-in web
settings page.
This guide uses only the Portainer web UI. You don't need SSH, a terminal, or a
config file: Inspector creates its own settings on first start and everything
else is done in its dashboard.
## Requirements
**You need:** a Docker host managed by Portainer (CE or BE, 2.x), and a few
minutes. Gotify or Pushover are optional, for phone alerts.
- Docker Engine 24+ and `docker compose` v2.
- A machine that can reach `/var/run/docker.sock` (any Docker host).
- Optional: a Gotify server and/or a Pushover account for notifications.
---
## Quickstart
## 1. Create the stack
```bash
# 1. Grab the compose file and example config
mkdir -p ~/docker/inspector && cd ~/docker/inspector
curl -L -o compose.yml https://gitea.euronvault.com/euronvault/inspector/raw/branch/main/compose.yml
curl -L -o config.yml https://gitea.euronvault.com/euronvault/inspector/raw/branch/main/config.example.yml
1. In Portainer, open your environment (usually **local**) and click
**Stacks → + Add stack**.
2. **Name:** `inspector`
3. **Build method:** *Web editor*. Paste the contents of
[`compose.yml`](./compose.yml):
# 2. Edit config.yml — at minimum, list the containers you want watched
# under the `containers:` key.
$EDITOR config.yml
```yaml
services:
inspector:
image: gitea.euronvault.com/euronvault/inspector:${INSPECTOR_TAG:-latest}
container_name: inspector
restart: unless-stopped
ports:
- "${INSPECTOR_PORT:-8080}:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${INSPECTOR_DATA:-/opt/inspector}/config:/app/config
- ${INSPECTOR_DATA:-/opt/inspector}/logs:/app/logs
- ${INSPECTOR_DATA:-/opt/inspector}/data:/app/data
environment:
- TZ=${TZ:-UTC}
- INSPECTOR_PASSWORD=${INSPECTOR_PASSWORD:-}
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
```
# 3. Set your Docker group GID so the container can read the socket.
echo "DOCKER_GID=$(getent group docker | cut -d: -f3)" > .env
## 2. Set the environment variables
# 4. Pull and run
docker compose pull
docker compose up -d
docker compose logs -f inspector
Scroll down to **Environment variables**. Click **Advanced mode** and paste,
then edit the values:
```
INSPECTOR_DATA=/mnt/user/appdata/inspector
INSPECTOR_PASSWORD=pick-a-password
TZ=America/Los_Angeles
```
If `dashboard.enabled: true` in your config, visit
`http://<host>:8080/` after startup to see the dashboard, and
`http://<host>:8080/settings` to edit the config from the browser.
| Variable | What to put | Required |
|---|---|---|
| `INSPECTOR_DATA` | Folder on your server where Inspector keeps its settings, logs, and history. **Unraid:** `/mnt/user/appdata/inspector`. **Other Linux:** `/opt/inspector`. Created automatically. | Yes |
| `INSPECTOR_PASSWORD` | Password for the dashboard (username `admin`). Used only on first start; change it later in the dashboard. Leave it out and Inspector generates one and prints it in the container logs. | Recommended |
| `TZ` | Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), so times on the dashboard are local. | Recommended |
| `INSPECTOR_PORT` | Port on your server for the dashboard. Change it if `8080` is taken. | No (default `8080`) |
| `INSPECTOR_TAG` | Version to run, e.g. `0.8.0`. See [Updating](#4-updating). | No (default `latest`) |
| `PUID` / `PGID` | User and group that own Inspector's files. Unraid users often use `99` / `100` so the files match other appdata. | No (default `1000` / `1000`) |
## Pinning a version
## 3. Deploy and sign in
Prefer an immutable tag over `latest` in production:
1. Click **Deploy the stack**.
2. Open `http://<your-server-ip>:8080` (or the port you chose).
3. Sign in as `admin` with your `INSPECTOR_PASSWORD`.
- No password set? Go to **Containers → inspector → Logs** in Portainer. Near
the top is a box labeled *Inspector dashboard login* with the generated
password.
4. Click **Help** in the dashboard header for the full how-to guide. The short
version:
- **Settings → + Add container**: add each container you want watched, using
its exact name from Portainer, and click **Recommended** for restart rules.
- **Settings → Notifications**: turn on Gotify and/or Pushover, click
**Send test**, then **Save**.
- Restart Inspector once after changing anything marked
**requires restart** (Portainer → Containers → tick `inspector` → Restart).
```bash
# .env
INSPECTOR_TAG=0.7.0
```
That's it. Inspector picks up new containers on its next check (within a minute).
Images are published at `gitea.euronvault.com/euronvault/inspector`.
---
## Upgrading
## 4. Updating
```bash
docker compose pull
docker compose up -d
```
1. **Stacks → inspector → Editor**.
2. Click **Update the stack** and turn on **Re-pull image and redeploy**.
3. Confirm.
Your `config.yml` is persisted outside the container, so upgrades don't
touch it. The settings page keeps a `.bak` of the previous file next to
`config.yml` on every save (when the directory is bind-mounted; see the
README for the Docker caveat).
Your settings and history live in `INSPECTOR_DATA`, so updates never touch them.
## Uninstall
**Pinning a version.** `latest` always gets the newest release. To stay on a
specific version, set `INSPECTOR_TAG` (e.g. `0.8.0`) in the stack's environment
variables, then update the stack. Available versions are on the
[releases page](https://gitea.euronvault.com/euronvault/inspector/releases)
and in the [CHANGELOG](./CHANGELOG.md).
```bash
docker compose down
rm -rf logs data config.yml compose.yml .env
```
**Rolling back.** Set `INSPECTOR_TAG` to the previous version and update the
stack with **Re-pull image and redeploy** on.
Inspector doesn't store anything outside the directory it was started
from.
## 5. Uninstalling
1. **Stacks → inspector → Delete this stack**.
2. Delete the `INSPECTOR_DATA` folder (on Unraid, through the appdata share) if
you don't want to keep the settings and history.
Inspector never changes your other containers' files or settings. It only
starts and restarts the ones you add to it.
---
## Troubleshooting
| What you see | What to do |
|---|---|
| Stack fails with `pull access denied` or `not found` | Check the image line is exactly `gitea.euronvault.com/euronvault/inspector:...` and that `INSPECTOR_TAG` (if set) is a real version. |
| Port already in use | Set `INSPECTOR_PORT` to a free port (e.g. `8095`) and update the stack. |
| Logs say *Permission denied on /var/run/docker.sock* | Make sure the stack has no `user:` line, then update the stack. Inspector needs to start normally to set up socket access. |
| Red **READ-ONLY** badge at the bottom of Settings | The config folder mount ends in `:ro` or the folder isn't writable. Remove `:ro` and update the stack. |
| Can't sign in | Look for the generated password in the container logs. If you've lost it, open `INSPECTOR_DATA/config/config.yml` (e.g. via your Unraid share), set a new `password:` under `dashboard:`, and restart Inspector. |
| A container shows **CONTAINER NOT FOUND** | The name in Inspector doesn't match Docker. Copy the exact name from Portainer → Containers. |
## Security notes
- Inspector needs the Docker socket to restart containers. Anything with access
to that socket effectively has full control of the host, so keep the
dashboard on your home network (or behind a VPN / reverse proxy with auth)
and use a real password.
- Inspector's process runs as an unprivileged user. It starts as root only long
enough to detect the Docker socket's group and fix ownership of its own
folders.
## Moving from an older (0.7.x) setup
Older setups mounted a single file at `/app/config.yml` and set `group_add` /
`DOCKER_GID`. That still works unchanged. To switch to the simpler layout:
move your `config.yml` into `<INSPECTOR_DATA>/config/`, replace the stack with
the compose above, and update the stack.
+77 -112
View File
@@ -1,139 +1,104 @@
# Inspector
<p align="center">
<img src="docs/logo.png" alt="Inspector logo" width="140">
</p>
Inspector is a small, opinionated Docker-watchdog. It polls the Docker
daemon, applies per-container restart rules, pings Gotify and/or
Pushover when things go wrong, and gives you a dashboard that shows
what's happening right now and what happened earlier today.
<h1 align="center">Inspector</h1>
Built for home-lab and small-fleet operators who want "it just works"
container recovery without standing up a full observability stack.
<p align="center"><b>Monitor. Restart. Report.</b><br>
A small Docker watchdog for home servers: it watches the containers you pick,
restarts them when they break, and tells your phone about it.</p>
## What Inspector does
<p align="center">
<a href="./INSTALL.md">Install with Portainer</a> ·
<a href="./src/inspector/assets/guide/guide.html">How-to guide</a> ·
<a href="./CHANGELOG.md">Changelog</a>
</p>
- Polls each configured container on a fixed interval and classifies it:
`HEALTHY`, `UNHEALTHY`, `STARTING`, `NOT RUNNING`, `ERROR`,
`UNREACHABLE`, `CONTAINER NOT FOUND`, `NO HEALTH CHECK`, `UNKNOWN`.
- Restarts a container when it crosses a configured error threshold —
with cooldowns, grace periods, and an hourly restart cap so a flapping
container doesn't DDoS your host.
- Opt-in HTTP reachability checks per container (useful for VPN-routed
containers whose tunnel drops while Docker still reports "healthy").
- Opt-in Uptime Kuma integration as a second reachability source.
- Advanced recovery via Portainer stack redeploy for containers whose
network namespace is tied to a VPN sidecar.
- Sends notifications on transitions, not on every poll — one line when
something breaks, one line when it recovers.
- Optional SQLite event history + a daily/weekly digest.
- Scheduled maintenance windows (don't act during 2–6 AM).
- Manual notification mute per container, from the dashboard.
- Embedded web dashboard + events timeline + **web settings page** for
editing `config.yml` without SSH.
![Inspector status page](src/inspector/assets/guide/status.png)
## What Inspector is not
## What it does
- Not a metrics system. It doesn't scrape Prometheus or push
dashboards; use Grafana / Netdata / your real monitoring stack for
that.
- Not an orchestrator. It only acts on containers you've explicitly
listed in `config.yml`.
- Not multi-host. One Inspector per Docker socket.
- **Watches the containers you choose.** Each check sorts a container into
healthy, unhealthy, not running, starting, and so on.
- **Restarts them when they break**, with sensible brakes: a container has to fail
several checks in a row first, there's a cooldown between restarts, a grace
period while it boots, and an hourly cap so a crash-looping app can't take over
your server.
- **Alerts you** through [Gotify](https://gotify.net) and/or
[Pushover](https://pushover.net): once when something breaks, once when it's
restarted, once when it recovers. Not on every check.
- **Is set up entirely in the browser.** Add, edit, and remove containers, set up
notifications (with a **Send test** button), and change every setting from the
dashboard. No YAML editing needed.
- **Keeps a history.** The Events page shows a day-by-day timeline of failures,
restarts, and recoveries, and an optional digest summarizes it for you.
- **Stays out of the way when you're working.** Put a container in maintenance or
mute its alerts with one click, or set a nightly maintenance window for backups
and updates.
## Install
See [`INSTALL.md`](./INSTALL.md). TL;DR:
Inspector is built to be installed from **Portainer**: paste one compose file into
**Stacks → Add stack**, set a folder and a password, deploy. Inspector creates its
own config and sets up Docker access by itself.
```bash
mkdir -p ~/docker/inspector && cd ~/docker/inspector
curl -L -o compose.yml https://gitea.euronvault.com/euronvault/inspector/raw/branch/main/compose.yml
curl -L -o config.yml https://gitea.euronvault.com/euronvault/inspector/raw/branch/main/config.example.yml
$EDITOR config.yml
echo "DOCKER_GID=$(getent group docker | cut -d: -f3)" > .env
docker compose up -d
```
**→ [Step-by-step Portainer install](./INSTALL.md)**
## Configuration
## Using it
Everything lives in `config.yml` next to the compose file. Start from
[`config.example.yml`](./config.example.yml) — every key is commented.
Highlights:
Once it's running, click **Help** in the dashboard header for the illustrated
guide (the same guide is in this repo at
[`src/inspector/assets/guide/guide.html`](./src/inspector/assets/guide/guide.html);
download it and open it in a browser to read it here).
- `inspector.interval_seconds` — poll cadence.
- `inspector.dry_run` — classify + log, never restart.
- `notifications.gotify` / `notifications.pushover` — enable either,
both, or neither.
- `containers.<name>.max_error_count` / `cooldown_seconds` /
`grace_period_seconds` / `max_restarts_per_hour` — the restart knobs.
- `containers.<name>.restart_on` — which statuses trigger a restart
(leave empty for observe-only).
- `containers.<name>.http_check` — opt-in HTTP ping.
- `containers.<name>.recovery_action` — `restart_container` (default)
or the advanced Portainer-based options.
| Add a container | Restart rules |
|---|---|
| ![New container form](src/inspector/assets/guide/container-new.png) | ![Container settings](src/inspector/assets/guide/container-edit.png) |
### Dashboard and settings page
| Notifications | Events history |
|---|---|
| ![Notification settings](src/inspector/assets/guide/settings-notifications.png) | ![Events timeline](src/inspector/assets/guide/events.png) |
If `dashboard.enabled: true`, Inspector exposes a small web UI on
`:8080`:
## What it isn't
- `/` — container cards with current status, error bar, phase
(cooldown / grace / failing / observe only / maintenance), and
maintenance / mute toggles.
- `/events` — per-day timeline of status changes, restarts, recoveries,
and failures (requires `database.enabled: true`).
- `/settings` — forms for every top-level config section plus one card
per container. Saves preserve comments and key order in `config.yml`
and keep a `.bak`. Fields that take effect on the next loop iteration
(per-container thresholds, `dry_run`, `heartbeat`, `interval_seconds`)
hot-apply; fields bound at startup are labeled **requires restart**.
- **Not a monitoring/metrics system.** No graphs of CPU or memory; use Grafana,
Netdata, or Uptime Kuma for that.
- **Not an orchestrator.** It only touches the containers you add, and only to
restart them.
- **One Docker host per Inspector.** Run one copy on each server.
Note on `.bak` inside Docker: when `config.yml` is bind-mounted as a
single file (the compose default), the `.bak` lives in the container's
overlay fs — the live file still updates cleanly, but the backup is
lost when the container is removed. To keep the `.bak` on the host,
bind-mount the directory instead of the single file.
## Advanced options
### Notifications
Most people never need these. They live in `config.yml` (in your
`INSPECTOR_DATA/config` folder) rather than the dashboard; see
[`config.example.yml`](./config.example.yml) for every option with comments.
Both channels are independent. If both are enabled, every alert goes to
both. Inspector sends on transitions:
- **HTTP checks**: mark a container `UNREACHABLE` when a URL stops answering,
even if Docker says it's healthy (useful for apps behind a VPN container).
- **Uptime Kuma**: use a Kuma status page as an extra reachability signal.
- **Dependencies** (`depends_on`): don't restart an app while the container it
relies on (e.g. a VPN) is down.
- **Portainer stack redeploy**: for containers that share a VPN container's
network, recover by redeploying the whole stack instead of restarting one
container.
```
🚨 first failure threshold crossed
🔁 container restart triggered
✅ restart / start completed
✅ container recovered
🛑 restart failed / hourly limit exceeded
❓ configured container not found
```
## Security
Gotify messages are Markdown; Pushover doesn't render Markdown so the
`**bold**` passthrough is cosmetic — bodies are identical.
Inspector needs the Docker socket (`/var/run/docker.sock`) to restart containers,
and that socket is effectively root on your server. So:
Pushover priorities map from Inspector's Gotify-style scale (default 5)
to Pushover's `-2..2` range: `<=2 → -1`, `3..4 → 0`, `5..6 → 1`,
`>=7 → 2`. Priority 2 (emergency) ships default `retry` / `expire` and
your Pushover client prompts for acknowledgement.
- Keep the dashboard on your home network, or put it behind a VPN or a reverse
proxy with authentication. Don't expose port 8080 to the internet.
- Use a real dashboard password.
- Inspector itself runs as an unprivileged user; it starts as root only to detect
the socket's permissions and fix ownership of its own folders.
## Docker socket security
## Versions
Inspector talks to `/var/run/docker.sock`. That socket is effectively
root on your host: anything that can write to it can start, stop,
inspect, and (through bind mounts) read any file as root. Inspector
mounts it read-write because it needs to restart containers.
Follow the usual home-lab precautions:
- Don't publish the dashboard port on an untrusted network. Use a
reverse proxy with auth, a tailnet, or SSH tunneling.
- Set a strong `dashboard.password`.
- Store Portainer / Gotify / Pushover tokens in the config file on a
host-only path with permissions `600`.
## Versioning
Semantic versioning (`MAJOR.MINOR.PATCH`). Image tags are published for
every release; `latest` tracks the newest tag. Pin a specific version
in production — see `INSTALL.md`.
Releases follow `MAJOR.MINOR.PATCH`. Every release is published as an image tag;
`latest` is the newest. See the [CHANGELOG](./CHANGELOG.md) for what changed, and
[INSTALL.md](./INSTALL.md#4-updating) for updating, pinning, and rolling back.
## License
[MIT](./LICENSE).
[MIT](./LICENSE) © 2026 Europium 63
+26 -17
View File
@@ -1,29 +1,38 @@
# Inspector compose file for buddies / self-hosters.
# Pulls the published image from gitea.euronvault.com.
# Inspector — Portainer stack
#
# Before `docker compose up -d`:
# 1. Copy config.example.yml -> config.yml and edit it.
# 2. Set DOCKER_GID to your host's docker group GID:
# getent group docker | cut -d: -f3
# Export it in your shell or put it in a .env file next to this file.
# 3. Optionally set INSPECTOR_TAG to pin a version
# (default: latest). Prefer a pinned X.Y.Z for production.
# Paste this into Portainer: Stacks -> Add stack -> Web editor.
# Then set these under "Environment variables" (Advanced mode lets you paste them):
#
# INSPECTOR_DATA Folder on your server for Inspector's files.
# Unraid: /mnt/user/appdata/inspector Other: /opt/inspector
# INSPECTOR_PASSWORD Dashboard password (username is "admin").
# Only used on first start; change it later in Settings.
# TZ Your timezone, e.g. America/Los_Angeles
#
# Optional:
# INSPECTOR_PORT Port for the dashboard on your server (default 8080).
# INSPECTOR_TAG Image version to run (default "latest"; e.g. 0.8.0 to pin).
# PUID / PGID User/group Inspector runs as (default 1000/1000).
# Unraid users often prefer 99 / 100.
#
# No config file to create, no docker group ID to look up: Inspector creates
# its config on first start and detects Docker socket permissions by itself.
services:
inspector:
image: gitea.euronvault.com/euronvault/inspector:${INSPECTOR_TAG:-latest}
container_name: inspector
restart: unless-stopped
# GID of the docker group on the host so the non-root user inside the
# container can read /var/run/docker.sock. Required.
group_add:
- "${DOCKER_GID:-999}"
ports:
# Only published if dashboard.enabled is true in config.yml.
- "${INSPECTOR_PORT:-8080}:8080"
volumes:
# Lets Inspector see and restart your containers. Required.
- /var/run/docker.sock:/var/run/docker.sock
- ./config.yml:/app/config.yml
- ./logs:/app/logs
- ./data:/app/data
# Settings (config.yml), logs, and event history. Kept across updates.
- ${INSPECTOR_DATA:-/opt/inspector}/config:/app/config
- ${INSPECTOR_DATA:-/opt/inspector}/logs:/app/logs
- ${INSPECTOR_DATA:-/opt/inspector}/data:/app/data
environment:
- TZ=${TZ:-UTC}
- INSPECTOR_PASSWORD=${INSPECTOR_PASSWORD:-}
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
+6 -2
View File
@@ -1,5 +1,9 @@
# Inspector example configuration.
# Copy to config.yml, edit, and mount into the container at /app/config.yml.
# Inspector example configuration: every option, with comments.
#
# You normally don't need this file. Inspector writes its own config.yml to
# INSPECTOR_DATA/config/ on first start, and the dashboard edits it for you.
# Use this as a reference for the advanced options that are YAML-only
# (http_check, uptime_kuma, depends_on, Portainer stack redeploy).
inspector:
interval_seconds: 60
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# Inspector container entrypoint.
#
# Started as root (the default): find the group that owns the Docker socket,
# make the mounted folders writable, then drop to an unprivileged user with
# that group added. No DOCKER_GID / group_add needed.
#
# Started as non-root (compose `user:` / older group_add setups): run as-is.
set -e
if [ "$(id -u)" != "0" ]; then
exec "$@"
fi
PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
SOCK=/var/run/docker.sock
groups="$PGID"
if [ -S "$SOCK" ]; then
sock_gid="$(stat -c %g "$SOCK")"
groups="$groups,$sock_gid"
else
echo "warning: $SOCK is not mounted; Inspector cannot see any containers" >&2
fi
# Mounted folders are often created by Docker as root. Hand them (not their
# contents) to the runtime user so Inspector can write config, logs and data.
for d in /app/config /app/logs /app/data; do
if [ -d "$d" ] && [ "$(stat -c %u "$d")" != "$PUID" ]; then
chown "$PUID:$PGID" "$d" 2>/dev/null || echo "warning: could not chown $d" >&2
fi
done
if [ -f /app/config/config.yml ] && [ "$(stat -c %u /app/config/config.yml)" != "$PUID" ]; then
chown "$PUID:$PGID" /app/config/config.yml 2>/dev/null || true
fi
exec setpriv --reuid="$PUID" --regid="$PGID" --groups="$groups" "$@"
Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+1 -1
View File
@@ -1 +1 @@
__version__ = "0.7.4"
__version__ = "0.8.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+272
View File
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Inspector — How to use</title>
<style>
:root {
--bg: #0b1220; --card: #111c30; --card-2: #0f1829; --border: #1e293b;
--ink: #e2e8f0; --ink-2: #cbd5e1; --muted: #8494a9;
--blue: #60a5fa; --green: #34d399; --amber: #fbbf24; --red: #f87171; --violet: #a78bfa;
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--ink);
font-family: Inter, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
font-size: 16px; line-height: 1.6; }
a { color: var(--blue); }
.top { position: sticky; top: 0; z-index: 5; background: #060b16; border-bottom: 1px solid var(--border); }
.top .in { max-width: 1100px; margin: 0 auto; padding: 0.8rem 1rem; display: flex; align-items: center; gap: 1rem; }
.top .brand { font-weight: 800; font-size: 1.05rem; }
.top .brand span { color: var(--muted); font-weight: 400; margin-left: 0.4rem; }
.top a.back { margin-left: auto; font-size: 0.9rem; text-decoration: none; border: 1px solid var(--border);
padding: 0.35rem 0.8rem; border-radius: 8px; color: var(--ink-2); }
.wrap { max-width: 1100px; margin: 0 auto; padding: 2rem 1rem 4rem; display: grid;
grid-template-columns: 220px minmax(0, 1fr); gap: 2.5rem; }
nav.toc { position: sticky; top: 4.5rem; align-self: start; font-size: 0.88rem; }
nav.toc b { display: block; color: var(--muted); text-transform: uppercase; font-size: 0.7rem;
letter-spacing: 0.08em; margin-bottom: 0.5rem; }
nav.toc a { display: block; color: var(--ink-2); text-decoration: none; padding: 0.25rem 0; }
nav.toc a:hover { color: var(--blue); }
main { min-width: 0; }
h1 { font-size: 1.9rem; margin: 0 0 0.4rem; line-height: 1.2; }
.lede { color: var(--ink-2); font-size: 1.05rem; margin: 0 0 2rem; }
section { margin-bottom: 3rem; scroll-margin-top: 4.5rem; }
h2 { font-size: 1.35rem; margin: 0 0 0.6rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
h3 { font-size: 1.02rem; margin: 1.4rem 0 0.4rem; }
p, li { color: var(--ink-2); }
code { background: var(--card-2); border: 1px solid var(--border); padding: 0.05em 0.35em;
border-radius: 5px; font-size: 0.88em; color: var(--ink); }
figure { margin: 1rem 0 1.4rem; }
figure img { width: 100%; height: auto; display: block; border-radius: 12px; border: 1px solid var(--border); }
figcaption { color: var(--muted); font-size: 0.82rem; margin-top: 0.4rem; }
.steps { counter-reset: s; list-style: none; padding: 0; }
.steps li { counter-increment: s; position: relative; padding-left: 2.3rem; margin-bottom: 0.7rem; }
.steps li::before { content: counter(s); position: absolute; left: 0; top: 0.05rem; width: 1.6rem; height: 1.6rem;
border-radius: 50%; background: rgba(96,165,250,0.15); color: var(--blue); font-weight: 700;
font-size: 0.85rem; display: flex; align-items: center; justify-content: center; }
.note { background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--blue);
border-radius: 10px; padding: 0.75rem 1rem; margin: 1rem 0; color: var(--ink-2); }
.note.warn { border-left-color: var(--amber); }
table { width: 100%; border-collapse: collapse; margin: 0.8rem 0 1.2rem; font-size: 0.92rem; }
th, td { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
td:first-child { white-space: nowrap; }
.tag { display: inline-block; font-size: 0.72rem; font-weight: 700; padding: 0.05rem 0.45rem; border-radius: 999px; }
.t-green { background: rgba(52,211,153,0.15); color: var(--green); }
.t-red { background: rgba(248,113,113,0.15); color: var(--red); }
.t-amber { background: rgba(251,191,36,0.15); color: var(--amber); }
.t-blue { background: rgba(96,165,250,0.15); color: var(--blue); }
.t-violet { background: rgba(167,139,250,0.15); color: var(--violet); }
@media (max-width: 820px) {
.wrap { grid-template-columns: 1fr; gap: 1rem; padding-top: 1.2rem; }
nav.toc { position: static; }
}
</style>
</head>
<body>
<div class="top"><div class="in">
<div class="brand">Inspector<span>How to use</span></div>
<a class="back" href="/">Open dashboard</a>
</div></div>
<div class="wrap">
<nav class="toc">
<b>On this page</b>
<a href="#first-login">1. First login</a>
<a href="#add">2. Add your containers</a>
<a href="#status">3. The Status page</a>
<a href="#notifications">4. Notifications</a>
<a href="#containers">5. Managing containers</a>
<a href="#events">6. Events history</a>
<a href="#settings">7. Other settings</a>
<a href="#restart">8. "Requires restart" fields</a>
<a href="#trouble">9. Troubleshooting</a>
</nav>
<main>
<h1>Using Inspector</h1>
<p class="lede">Inspector watches the Docker containers you pick, restarts them when they break,
and pings your phone when something goes wrong or recovers. Everything is set from this web
dashboard; you never need to touch a config file.</p>
<section id="first-login">
<h2>1. First login</h2>
<p>When Inspector starts for the first time it creates its config file and a dashboard login.</p>
<ol class="steps">
<li>If you set <code>INSPECTOR_PASSWORD</code> in the Portainer stack, sign in with
<code>admin</code> (or your <code>INSPECTOR_USERNAME</code>) and that password.</li>
<li>If you didn't, Inspector generated a password. In Portainer open
<b>Containers → inspector → Logs</b>. Near the top you'll see a box labeled
<i>Inspector dashboard login</i> with the username and password.</li>
<li>Open <code>http://&lt;your-server&gt;:8080</code> and sign in.</li>
<li>Change the password under <b>Settings → Dashboard</b> if you like.</li>
</ol>
</section>
<section id="add">
<h2>2. Add your containers</h2>
<p>Inspector only watches containers you add. Go to <b>Settings</b> and click
<b>+ Add container</b> at the bottom of the left menu (or <b>+ New container</b> on the
All containers page).</p>
<figure><img src="container-new.png" alt="New container form">
<figcaption>The New container form.</figcaption></figure>
<ol class="steps">
<li><b>name</b>: the container's name exactly as Portainer shows it
(for example <code>sonarr</code>). If it doesn't match, the card shows
<span class="tag t-amber">CONTAINER NOT FOUND</span>.</li>
<li>Leave <b>enabled</b> on.</li>
<li>Under <b>Restart when status is…</b> click <b>Recommended</b>
(restarts on <code>UNHEALTHY</code> and <code>NOT RUNNING</code>), or
<b>Observe only</b> if you just want alerts without restarts.</li>
<li>Click <b>Create container</b>. Inspector starts watching it on its next check
(within a minute by default). No restart needed.</li>
</ol>
<h3>What the numbers mean</h3>
<table>
<tr><th>Field</th><th>Meaning</th><th>Default</th></tr>
<tr><td>max_error_count</td><td>How many bad checks in a row before Inspector restarts the container. Stops one blip from causing a restart.</td><td>5</td></tr>
<tr><td>cooldown_seconds</td><td>Minimum time between two restarts of the same container.</td><td>300</td></tr>
<tr><td>grace_period_seconds</td><td>After a restart, ignore failures for this long so the app can boot.</td><td>120</td></tr>
<tr><td>max_restarts_per_hour</td><td>Safety cap. After this many restarts in an hour Inspector stops restarting and alerts you instead.</td><td>3</td></tr>
<tr><td>maintenance_window</td><td>Follow the daily maintenance window from Settings → Maintenance.</td><td>off</td></tr>
<tr><td>color</td><td>Card color on the dashboard. Pick a swatch or leave blank for automatic.</td><td>auto</td></tr>
</table>
<div class="note">With the default 60-second check interval and <code>max_error_count</code> 5, a
container has to be broken for about five minutes before Inspector restarts it. Lower the count
for faster recovery.</div>
</section>
<section id="status">
<h2>3. The Status page</h2>
<figure><img src="status.png" alt="Status page">
<figcaption>Status: one card per watched container.</figcaption></figure>
<p>The top row counts containers, healthy ones, ones with issues, and ones in maintenance.
Each card shows:</p>
<ul>
<li><b>Status badge</b>: what Docker reports right now, e.g.
<span class="tag t-green">HEALTHY</span> <span class="tag t-red">NOT RUNNING</span>
<span class="tag t-violet">NO HEALTH CHECK</span> (the image has no health check; that's normal
for many apps and only means Inspector can tell running from stopped, not healthy from unhealthy).</li>
<li><b>Errors bar</b>: bad checks in a row out of <code>max_error_count</code>. When it fills,
Inspector restarts the container.</li>
<li><b>Cooldown until / Last restart</b>: when Inspector last restarted it and when it's allowed to
again.</li>
<li><b>Observe only</b>: shown when the container has no restart triggers; Inspector alerts but
never restarts it.</li>
<li><b>Enter maintenance</b>: pause Inspector for that container while you work on it
(updates, migrations). Click again to exit.</li>
<li><b>Mute notifications</b>: keep watching and restarting, but stop alerts for a while
(12 hours by default).</li>
</ul>
<p>The page refreshes itself every 30 seconds.</p>
</section>
<section id="notifications">
<h2>4. Notifications</h2>
<p>Inspector can alert you through <a href="https://gotify.net">Gotify</a>,
<a href="https://pushover.net">Pushover</a>, or both. Set them up under
<b>Settings → Notifications</b>.</p>
<figure><img src="settings-notifications.png" alt="Notifications settings">
<figcaption>Each channel has its own priority reference, a Send test button, and a Save button.</figcaption></figure>
<ol class="steps">
<li>Turn on <b>enabled</b> for the channel you use.</li>
<li><b>Gotify</b>: enter your server URL and an <i>application</i> token (create one in Gotify
under Apps). <b>Pushover</b>: enter your application API token and your user key.</li>
<li>Click <b>Send test</b>. You should get a message on your phone within seconds. If it fails,
the reason appears next to the button.</li>
<li>Click <b>Save</b>, then restart Inspector once (see <a href="#restart">section 8</a>),
because turning a channel on or off is a "requires restart" field.</li>
</ol>
<p>You'll get one alert when a container first fails, when Inspector restarts it, and when it
recovers. You won't get one on every check.</p>
</section>
<section id="containers">
<h2>5. Managing containers</h2>
<p><b>Settings → All containers</b> shows every watched container as a tile. Click a tile (or a
name in the left menu) to edit it.</p>
<figure><img src="containers.png" alt="All containers grid">
<figcaption>All containers.</figcaption></figure>
<figure><img src="container-edit.png" alt="Container settings">
<figcaption>Editing one container. Each restart trigger is a card you click on or off.</figcaption></figure>
<p>Changes apply on the next check. <b>Delete container</b> stops Inspector watching it; it
does <i>not</i> touch the container itself.</p>
<h3>Restart triggers</h3>
<table>
<tr><th>Status</th><th>When it happens</th></tr>
<tr><td>UNHEALTHY</td><td>The container's own health check is failing.</td></tr>
<tr><td>NOT RUNNING</td><td>Stopped, exited, or crashed.</td></tr>
<tr><td>UNREACHABLE</td><td>Running, but an HTTP check failed (advanced, set in config.yml).</td></tr>
<tr><td>ERROR</td><td>Inspector couldn't check it (Docker API error).</td></tr>
<tr><td>STARTING</td><td>Still in its health-check start period. Usually leave this off.</td></tr>
<tr><td>HEALTHY / NO HEALTH CHECK</td><td>Normal states; almost never a reason to restart.</td></tr>
</table>
</section>
<section id="events">
<h2>6. Events history</h2>
<p>The <b>Events</b> tab is a day-by-day timeline of failures, restarts, recoveries, mutes, and
maintenance. Use <b>Prev day</b> / <b>Next day</b> to look back.</p>
<figure><img src="events.png" alt="Events timeline">
<figcaption>Here a container stopped, Inspector restarted it, and it recovered a few seconds later.</figcaption></figure>
</section>
<section id="settings">
<h2>7. Other settings</h2>
<figure><img src="settings-inspector.png" alt="Inspector loop settings">
<figcaption>Settings → Inspector loop.</figcaption></figure>
<table>
<tr><th>Section</th><th>What it's for</th></tr>
<tr><td>Inspector loop</td><td><b>interval_seconds</b>: how often to check (60 is fine).
<b>dry_run</b>: log and alert but never actually restart; handy while you're trying it out.</td></tr>
<tr><td>Dashboard</td><td>Login username and password for this web UI.</td></tr>
<tr><td>Event history</td><td>How many days of events to keep.</td></tr>
<tr><td>Health summary</td><td>A periodic digest (e.g. daily) of what happened, sent through your notification channels.</td></tr>
<tr><td>Maintenance</td><td>A daily quiet window (e.g. 02:00–05:00) so nightly backups or updates don't trigger restarts. Turn on <b>maintenance_window</b> on each container that should follow it.</td></tr>
<tr><td>Notification mute</td><td>Turns the per-container <b>Mute notifications</b> buttons on or off, and sets how long a mute lasts.</td></tr>
</table>
<p>At the bottom of every settings page is a strip showing where the config file lives and
whether Inspector can write to it.</p>
</section>
<section id="restart">
<h2>8. "Requires restart" fields</h2>
<p>Most changes apply on the next check. Fields marked
<span class="tag t-violet">REQUIRES RESTART</span> (like turning a notification channel or the
dashboard on or off) only take effect after Inspector restarts:</p>
<ol class="steps">
<li>In Portainer go to <b>Containers</b>.</li>
<li>Tick <b>inspector</b> and click <b>Restart</b>.</li>
</ol>
<p>Your settings and history are kept; they live in the folders mounted into the container.</p>
</section>
<section id="trouble">
<h2>9. Troubleshooting</h2>
<table>
<tr><th>Problem</th><th>Fix</th></tr>
<tr><td>Red <b>READ-ONLY</b> badge at the bottom of Settings</td>
<td>Inspector can't save changes. In the stack, make sure the config folder mount does not end in
<code>:ro</code>, then update the stack.</td></tr>
<tr><td>A card says <b>CONTAINER NOT FOUND</b></td>
<td>The name doesn't match a container. Check the exact name in Portainer → Containers, delete
the entry and add it again with the right name.</td></tr>
<tr><td>Logs say <i>Permission denied on /var/run/docker.sock</i></td>
<td>Remove any <code>user:</code> line from the stack so Inspector starts normally (it detects the
socket permissions itself), then update the stack.</td></tr>
<tr><td>Send test fails</td>
<td>The message next to the button says why: wrong URL, wrong token (HTTP 401), or the server can't
be reached from your Docker host.</td></tr>
<tr><td>Forgot the dashboard password</td>
<td>Open <code>config.yml</code> in the stack's config folder (e.g. through your Unraid share),
set <code>dashboard: password:</code> to something new, and restart Inspector.</td></tr>
<tr><td>No alerts arrive</td>
<td>Check the channel is enabled, Send test works, Inspector was restarted after enabling it,
and the container isn't muted or in maintenance.</td></tr>
</table>
</section>
</main>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+56
View File
@@ -0,0 +1,56 @@
"""First-run helpers: find config.yml and write a starter one if it's missing.
Lets Inspector be installed from Portainer with nothing on the host but an
empty folder mounted at /app/config.
"""
from __future__ import annotations
import json
import os
import secrets
from importlib import resources
from pathlib import Path
LEGACY_CONFIG_PATH = Path("/app/config.yml")
DEFAULT_CONFIG_PATH = Path("/app/config/config.yml")
def resolve_config_path(cli_value: str | None, env: dict | None = None) -> Path:
"""--config wins, then $INSPECTOR_CONFIG, then the legacy single-file
mount if present, then the folder mount."""
env = os.environ if env is None else env
if cli_value:
return Path(cli_value)
if env.get("INSPECTOR_CONFIG"):
return Path(env["INSPECTOR_CONFIG"])
if LEGACY_CONFIG_PATH.exists():
return LEGACY_CONFIG_PATH
return DEFAULT_CONFIG_PATH
def write_starter_config(path: Path, env: dict | None = None) -> str | None:
"""Write the starter config to `path`.
Dashboard credentials come from INSPECTOR_USERNAME / INSPECTOR_PASSWORD.
Returns the generated password when none was supplied (so the caller can
log it once), otherwise None.
"""
env = os.environ if env is None else env
username = env.get("INSPECTOR_USERNAME") or "admin"
password = env.get("INSPECTOR_PASSWORD") or ""
generated = None
if not password:
generated = password = secrets.token_urlsafe(12)
template = (resources.files(__package__) / "starter_config.yml").read_text(encoding="utf-8")
# json.dumps gives a valid YAML double-quoted scalar for any input.
text = template.replace("__USERNAME__", json.dumps(username)).replace(
"__PASSWORD__", json.dumps(password)
)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
try:
path.chmod(0o600)
except OSError:
pass
return generated
+41 -2
View File
@@ -124,8 +124,11 @@ def _status_snapshot(
) -> list[dict]:
now = utcnow()
rows = []
for i, (name, state) in enumerate(states.items()):
cfg = containers[name]
# list() snapshot: the inspector loop adds/removes entries from another thread.
for i, (name, state) in enumerate(list(states.items())):
cfg = containers.get(name)
if cfg is None: # removed from config since the last loop iteration
continue
cooldown_until = state.cooldown_until
last_restart_at = state.last_restart_at
muted_until = notification_mute.muted_until(name)
@@ -285,6 +288,20 @@ def _load_asset(name: str) -> bytes:
_ICON_PNG = _load_asset("inspector_icon_outline.png")
_GUIDE_NAME_RE = re.compile(r"^[a-z0-9-]+\.(html|png)$")
def _load_guide_file(name: str) -> tuple[bytes | None, str]:
"""Read a file from assets/guide/. Names are whitelisted by pattern, so no
path traversal is possible."""
if not _GUIDE_NAME_RE.match(name):
return None, ""
data = _load_asset(f"guide/{name}")
if not data:
return None, ""
ctype = "text/html; charset=utf-8" if name.endswith(".html") else "image/png"
return data, ctype
# Filename -> Content-Type, for files in src/inspector/assets/ served at /static/<name>.
_STATIC_FILES = {
"icon.png": "image/png",
@@ -821,6 +838,7 @@ def _page_head(refresh: bool, active: str) -> str:
f' <a href="/" class="{status_class}">Status</a>\n'
f' <a href="/events" class="{events_class}">Events</a>\n'
f' <a href="/settings" class="{settings_class}">Settings</a>\n'
' <a href="/guide/">Help</a>\n'
" </div>\n"
f' <div class="meta"><span class="meta-text">{meta_text}</span><a class="signout" href="/logout">Sign out</a></div>\n'
"</header>\n"
@@ -1254,6 +1272,8 @@ def _apply_settings_update(config_ref: ConfigRef, section: str, form: dict) -> N
cfg["grace_period_seconds"] = _form_int(form, "grace_period_seconds", 120)
cfg["max_restarts_per_hour"] = _form_int(form, "max_restarts_per_hour", 3)
cfg["color"] = _form_str(form, "color", "")
if form.get("restart_on_present"): # only our own form carries this field
cfg["maintenance_window"] = _form_bool(form, "maintenance_window")
restart_on = form.get("restart_on", [])
if restart_on or form.get("restart_on_present"):
cfg["restart_on"] = [s.upper() for s in restart_on]
@@ -1300,6 +1320,7 @@ def _apply_container_create(config_ref: ConfigRef, form: dict) -> str:
"grace_period_seconds": _form_int(form, "grace_period_seconds", 120),
"max_restarts_per_hour": _form_int(form, "max_restarts_per_hour", 3),
"color": _form_str(form, "color", ""),
"maintenance_window": _form_bool(form, "maintenance_window"),
}
restart_on = form.get("restart_on", [])
if restart_on or form.get("restart_on_present"):
@@ -1994,6 +2015,10 @@ def _render_container_form(
+ _settings_field("max_restarts_per_hour", "max_restarts_per_hour",
str(getattr(cfg, "max_restarts_per_hour", 3)), kind="number",
hint="Hourly cap — Inspector pauses restarts after this many")
+ _settings_field("maintenance_window", "maintenance_window",
"on" if getattr(cfg, "maintenance_window", False) else "", kind="switch",
hint="Skip this container during the daily maintenance window (Settings → Maintenance)",
requires_restart=True)
+ _render_color_input("color", getattr(cfg, "color", ""))
+ restart_on_html
)
@@ -2052,6 +2077,7 @@ def _render_container_edit_page(app: AppConfig, config_ref: ConfigRef, name: str
class _NewContainerDefaults:
name = ""
enabled = True
maintenance_window = False
max_error_count = 5
cooldown_seconds = 300
grace_period_seconds = 120
@@ -2250,6 +2276,19 @@ class _Handler(BaseHTTPRequestHandler):
events = []
body = _render_events_page(events, app.database.enabled, selected_day, colors)
self._send_bytes(HTTPStatus.OK, "text/html; charset=utf-8", body)
elif path == "/guide":
# Trailing slash so the guide's relative image paths resolve under /guide/.
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
self.send_header("Location", "/guide/")
self.send_header("Content-Length", "0")
self.end_headers()
elif path.startswith("/guide/"):
name = path[len("/guide/"):] or "guide.html"
data, ctype = _load_guide_file(name)
if data is None:
self._send_bytes(HTTPStatus.NOT_FOUND, "text/plain; charset=utf-8", b"not found")
return
self._send_bytes(HTTPStatus.OK, ctype, data)
elif path == "/api/events":
selected_day = _parse_date_param(query.get("date", [None])[0])
if app.database.enabled:
+42 -13
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import argparse
import logging
import os
import signal
import sys
import time
@@ -9,6 +10,7 @@ from datetime import datetime
from pathlib import Path
from . import __version__
from .bootstrap import resolve_config_path, write_starter_config
from .config import AppConfig, ConfigRef, ContainerConfig, load_config, load_raw
from .dashboard import DashboardServer
from .docker_client import build_client, get_container_status
@@ -398,8 +400,9 @@ def run_loop(config_ref: ConfigRef) -> int:
except Exception as exc:
if "Permission denied" in str(exc):
log.critical(
"Permission denied on /var/run/docker.sock. Set DOCKER_GID to "
"the host docker group GID: stat -c '%g' /var/run/docker.sock"
"Permission denied on /var/run/docker.sock. Start the container "
"as root (the default) so it can detect the socket's group, or set "
"group_add to the socket's GID: stat -c '%g' /var/run/docker.sock"
)
else:
log.critical("Cannot reach Docker daemon: %s", exc)
@@ -437,17 +440,20 @@ def run_loop(config_ref: ConfigRef) -> int:
notifier = _build_notifier(app)
# Rebuild enabled_configs / check_order from current config so edits
# to per-container settings apply next cycle. Containers added via
# the settings page are logged as 'requires restart' because the
# states dict is built once at startup.
# to per-container settings apply next cycle. Containers added (or
# re-enabled) from the settings page get a fresh state here; `states`
# is shared with the dashboard, so it is updated in place.
current_enabled = {c.name: c for c in app.containers if c.enabled}
new_names = current_enabled.keys() - states.keys()
if new_names:
log.warning(
"Containers added in config require Inspector restart to begin monitoring: %s",
sorted(new_names),
)
enabled_configs = {n: c for n, c in current_enabled.items() if n in states}
for new_name in new_names:
states[new_name] = ContainerState(name=new_name)
name_width = max(name_width, *(len(n) for n in new_names))
log.info("Now watching newly added container(s): %s", sorted(new_names))
for gone in states.keys() - current_enabled.keys():
states.pop(gone, None)
log.info("Stopped watching container (removed or disabled): %s", gone)
enabled_configs = current_enabled
check_order = _topological_order(enabled_configs)
uptime_kuma_snapshot = uptime_kuma_client.fetch()
@@ -495,14 +501,37 @@ def run_loop(config_ref: ConfigRef) -> int:
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="inspector", description="Docker container health watcher")
parser.add_argument("--config", "-c", default="/app/config.yml", help="Path to config.yml")
parser.add_argument(
"--config", "-c", default=None,
help="Path to config.yml (default: $INSPECTOR_CONFIG, /app/config.yml if present, "
"else /app/config/config.yml)",
)
parser.add_argument("--version", action="version", version=f"inspector {__version__}")
args = parser.parse_args(argv)
config_path = Path(args.config)
config_path = resolve_config_path(args.config)
if not config_path.exists():
print(f"config file not found: {config_path}", file=sys.stderr)
# First run with an empty config folder: write a starter config so the
# dashboard comes up and everything else can be set from the web UI.
try:
generated_password = write_starter_config(config_path)
except OSError as exc:
print(
f"config file not found: {config_path} (and could not create a starter one: {exc})",
file=sys.stderr,
)
return 2
print(f"Created starter config at {config_path}", file=sys.stderr)
if generated_password:
print(
"=" * 64 + "\n"
" Inspector dashboard login (generated on first start)\n"
f" username: {os.environ.get('INSPECTOR_USERNAME') or 'admin'}\n"
f" password: {generated_password}\n"
" Change it under Settings -> Dashboard.\n"
+ "=" * 64,
file=sys.stderr,
)
app = load_config(config_path)
raw = load_raw(config_path)
+64
View File
@@ -0,0 +1,64 @@
# Inspector configuration.
#
# Inspector wrote this starter file on first start. You can edit everything
# from the web UI (Settings), or by hand. Containers are added from
# Settings -> Containers -> Add container.
inspector:
interval_seconds: 60
log_level: INFO
dry_run: false
log_dir: /app/logs
log_filename: inspector.log
log_retention_days: 30
heartbeat: false
notifications:
# Gotify: https://gotify.net
gotify:
enabled: false
url: ""
token: ""
priority: 5
# Pushover: https://pushover.net
pushover:
enabled: false
token: ""
user_key: ""
priority: 5
device: ""
sound: ""
# Event history (powers the Events page and the health summary digest).
database:
enabled: true
path: /app/data/events.db
retention_days: 30
health_summary:
enabled: false
interval_hours: 24
priority: 3
maintenance:
enabled: false
start: "00:00"
end: "06:00"
dir: /app/data/maintenance
max_hours: 8
notification_mute:
enabled: true
dir: /app/data/muted
default_hours: 12
dashboard:
enabled: true
host: 0.0.0.0
port: 8080
username: __USERNAME__
password: __PASSWORD__
# Containers Inspector watches. Add them from the web UI.
containers: