Archived
Ferrule 0.01
This commit is contained in:
1 parent
aae9874dec
commit
5684699242
8 files changed
+1843
-2
No files matched your search
@@ -0,0 +1,15 @@
|
||||
Ferrule
|
||||
Copyright (c) 2026 the Ferrule authors. All rights reserved.
|
||||
|
||||
This software and its source code are proprietary and confidential. No
|
||||
licence to use, copy, modify, merge, publish, distribute, sublicense or sell
|
||||
them, in whole or in part, is granted except by a written agreement with
|
||||
the copyright holders.
|
||||
|
||||
Ferrule is distributed to customers as a built image. Running it is
|
||||
governed by the terms of the licence under which that image was obtained.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT, EXCEPT AS A WRITTEN
|
||||
AGREEMENT WITH THE COPYRIGHT HOLDERS PROVIDES.
|
||||
+1044
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,80 @@
|
||||
# ferrule-deploy
|
||||
# Ferrule
|
||||
|
||||
Ferrule deployment files: the compose files and setup guide Portainer reads. No source.
|
||||
Ferrule is a monitoring dashboard for UniFi networks, hypervisors and
|
||||
storage. It shows what the vendors' own screens mostly don't: fan speeds,
|
||||
per-port error counters, SFP optics, PoE draw, temperatures, NVR disk
|
||||
health, WAN failover and cable modem health. It also keeps its own record
|
||||
of outages, so the record survives the controller.
|
||||
|
||||
This repository is how you deploy it: a Docker Compose stack, run from
|
||||
Portainer or from the command line. Ferrule runs from a published image,
|
||||
so nothing is built on your host.
|
||||
|
||||
**Full documentation: <https://docs.ferrulehq.dev>**
|
||||
|
||||
## What you need
|
||||
|
||||
- A Docker host with **Portainer** or **Docker Compose**, and outbound
|
||||
internet access to download the images.
|
||||
- At least one thing to watch:
|
||||
- a UniFi console (gateway, and a UNVR for Protect);
|
||||
- or Proxmox VE, Proxmox Backup Server, vCenter/ESXi;
|
||||
- or a Synology, QNAP or TrueNAS.
|
||||
- **A license key**, if you're a beta tester; you'll get it from us. Without
|
||||
one, Ferrule starts a 30-day trial of the paid features. When the trial
|
||||
ends, it keeps monitoring, with the paid features paused.
|
||||
|
||||
## Install
|
||||
|
||||
1. **Create read-only accounts** on what you'll watch, such as a UniFi API
|
||||
key and a local read-only admin. Each system's exact steps are under
|
||||
[Connect your gear](https://docs.ferrulehq.dev/connect/unifi-network/).
|
||||
2. **Get the stack:**
|
||||
- **Portainer:** Stacks → Add stack → name `ferrule` → **Repository**,
|
||||
with URL `https://git.uglyhousesneedlove.com/steve/ferrule-deploy`,
|
||||
reference `refs/heads/main`, compose path `docker-compose.yml`, and
|
||||
authentication **off**.
|
||||
- **Docker Compose:**
|
||||
```sh
|
||||
git clone https://git.uglyhousesneedlove.com/steve/ferrule-deploy.git ferrule
|
||||
cd ferrule
|
||||
cp stack.env.example .env
|
||||
```
|
||||
3. **Fill in the settings** from [`stack.env.example`](stack.env.example):
|
||||
your hosts and credentials. Keep `FERRULE_VERSION` as it is: it's the
|
||||
current release. Set `UNIFI_ENABLED=false` if you have no UniFi console.
|
||||
- **Portainer:** paste them under **Environment variables** (Advanced
|
||||
mode).
|
||||
- **Docker Compose:** edit `.env`.
|
||||
4. **Start it:** **Deploy the stack** in Portainer, or `docker compose up -d`.
|
||||
Then open `http://<docker host>:8090`.
|
||||
5. **Settings → License:** paste your key and press **Apply**. It shows
|
||||
"Waiting for the first check-in" for up to about two hours, then
|
||||
"Confirmed online".
|
||||
|
||||
Once it's running, **Settings → Stack environment** writes the lines for
|
||||
everything else: hypervisors, storage, notifications and sign-in.
|
||||
|
||||
## Updating
|
||||
|
||||
When a new release comes out, set `FERRULE_VERSION` to it, then click
|
||||
**Pull and redeploy** in Portainer, or run `docker compose up -d`. Nothing
|
||||
changes until you do.
|
||||
|
||||
## Help and feedback
|
||||
|
||||
Everything is at <https://docs.ferrulehq.dev>, including
|
||||
[Troubleshooting](https://docs.ferrulehq.dev/troubleshooting/).
|
||||
[PORTAINER.md](PORTAINER.md) has the full reference too.
|
||||
|
||||
Something unclear, broken or missing? Tell us at
|
||||
<[email protected]>.
|
||||
|
||||
Your license key is checked once a day against a signed list at
|
||||
`https://license.ferrulehq.dev/revoked`. That's a plain download: nothing
|
||||
about you, your network or your install is sent.
|
||||
|
||||
---
|
||||
|
||||
Ferrule is an independent project, not affiliated with Ubiquiti or the
|
||||
other vendors it names. See [LICENSE](LICENSE) for the terms.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Backups to an NFS export. Add this file under the Git stack's
|
||||
# "Additional paths" in Portainer, beside docker-compose.yml, and set:
|
||||
#
|
||||
# BACKUP_NFS_SERVER=192.168.1.20 the NAS
|
||||
# BACKUP_NFS_PATH=/volume1/backups the export
|
||||
# BACKUP_NFS_OPTIONS=nfsvers=4.1 optional; the default suits most NAS
|
||||
#
|
||||
# Docker mounts the export as a named volume, so there is no bind mount and
|
||||
# no host path: the stack stays Portainer-safe. The Docker host needs the
|
||||
# NFS client (nfs-common on Debian and Ubuntu); most have it.
|
||||
#
|
||||
# The collector writes as UID/GID 65534. The export must let it: on a
|
||||
# Synology, "Map all users to admin" in the NFS rule; elsewhere, all_squash
|
||||
# with anonuid=65534,anongid=65534, or a directory owned by 65534.
|
||||
#
|
||||
# Docker reads these options only when it creates the volume. To change
|
||||
# them later, remove the stack's backup-target volume and redeploy.
|
||||
|
||||
services:
|
||||
telemetry:
|
||||
environment:
|
||||
BACKUP_DIR: /backup
|
||||
# Shown on Settings -> Stack environment; the collector ignores them.
|
||||
BACKUP_NFS_SERVER: ${BACKUP_NFS_SERVER:-}
|
||||
BACKUP_NFS_PATH: ${BACKUP_NFS_PATH:-}
|
||||
BACKUP_NFS_OPTIONS: ${BACKUP_NFS_OPTIONS:-nfsvers=4.1,rw,soft,timeo=150}
|
||||
volumes:
|
||||
- backup-target:/backup
|
||||
|
||||
volumes:
|
||||
backup-target:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: "addr=${BACKUP_NFS_SERVER:?set BACKUP_NFS_SERVER to the NAS},${BACKUP_NFS_OPTIONS:-nfsvers=4.1,rw,soft,timeo=150}"
|
||||
device: ":${BACKUP_NFS_PATH:?set BACKUP_NFS_PATH to the export}"
|
||||
@@ -0,0 +1,41 @@
|
||||
# Backups to an SMB (Windows or NAS) share. Add this file under the Git
|
||||
# stack's "Additional paths" in Portainer, beside docker-compose.yml, and
|
||||
# set:
|
||||
#
|
||||
# BACKUP_SMB_SERVER=192.168.1.20 the server
|
||||
# BACKUP_SMB_SHARE=backups the share
|
||||
# BACKUP_SMB_USERNAME=ferrule an account that can write to it
|
||||
# BACKUP_SMB_PASSWORD=... its password; no commas
|
||||
# BACKUP_SMB_VERSION=3.0 optional
|
||||
#
|
||||
# Docker mounts the share as a named volume, so there is no bind mount and
|
||||
# no host path: the stack stays Portainer-safe. The Docker host needs the
|
||||
# CIFS client (cifs-utils on Debian and Ubuntu).
|
||||
#
|
||||
# The share is mounted as UID/GID 65534, which the collector runs as, with
|
||||
# files readable only by it.
|
||||
#
|
||||
# Docker reads these options only when it creates the volume. To change
|
||||
# them later, such as a new password, remove the stack's backup-target
|
||||
# volume and redeploy.
|
||||
|
||||
services:
|
||||
telemetry:
|
||||
environment:
|
||||
BACKUP_DIR: /backup
|
||||
# Shown on Settings -> Stack environment; the collector ignores them.
|
||||
# Never the password.
|
||||
BACKUP_SMB_SERVER: ${BACKUP_SMB_SERVER:-}
|
||||
BACKUP_SMB_SHARE: ${BACKUP_SMB_SHARE:-}
|
||||
BACKUP_SMB_USERNAME: ${BACKUP_SMB_USERNAME:-}
|
||||
BACKUP_SMB_VERSION: ${BACKUP_SMB_VERSION:-3.0}
|
||||
volumes:
|
||||
- backup-target:/backup
|
||||
|
||||
volumes:
|
||||
backup-target:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
o: "username=${BACKUP_SMB_USERNAME:?set BACKUP_SMB_USERNAME},password=${BACKUP_SMB_PASSWORD:?set BACKUP_SMB_PASSWORD},uid=65534,gid=65534,file_mode=0600,dir_mode=0700,vers=${BACKUP_SMB_VERSION:-3.0}"
|
||||
device: "//${BACKUP_SMB_SERVER:?set BACKUP_SMB_SERVER}/${BACKUP_SMB_SHARE:?set BACKUP_SMB_SHARE}"
|
||||
@@ -0,0 +1,65 @@
|
||||
# Optional overlay for Docker hosts running HMS-Docker, whose Traefik and
|
||||
# hms-docker_proxy_net network already exist.
|
||||
#
|
||||
# Add it in Portainer under the Git stack's "Additional paths", after
|
||||
# docker-compose.yml. It routes the dashboard, and Gotify and iGotify when
|
||||
# their profiles are on, through Traefik by hostname, behind the same
|
||||
# internal allow-list middleware HMS-Docker uses for its own apps.
|
||||
#
|
||||
# Set the hostnames as stack variables; each must resolve to your Traefik:
|
||||
# TELEMETRY_HOSTNAME=telemetry.example.com
|
||||
# GOTIFY_HOSTNAME=gotify.example.com (only with COMPOSE_PROFILES gotify)
|
||||
# IGOTIFY_HOSTNAME=igotify.example.com (only with COMPOSE_PROFILES igotify)
|
||||
# TRAEFIK_MIDDLEWARES=internal-ipallowlist@file (the default)
|
||||
#
|
||||
# Router names carry a ferrule- prefix so they never collide with routers an
|
||||
# existing Gotify on the same Traefik already uses.
|
||||
#
|
||||
# Each container sits on two networks, its stack's own and the proxy one, so
|
||||
# Traefik has to pick an address. Left to traefik.docker.network, Traefik 3.7
|
||||
# on Docker 29 has been seen failing to match the name, falling back to the
|
||||
# stack network it is not on, and timing out every request (issue #1). So
|
||||
# each service names its server by container name instead: Docker's DNS on
|
||||
# the proxy network answers that with the proxy address, and Traefik never
|
||||
# chooses. The names are the container_name values in docker-compose.yml.
|
||||
|
||||
services:
|
||||
telemetry:
|
||||
networks:
|
||||
- default
|
||||
- hms-docker_proxy_net
|
||||
environment:
|
||||
# Shown on Settings -> Stack environment; the collector ignores them.
|
||||
TELEMETRY_HOSTNAME: ${TELEMETRY_HOSTNAME:-telemetry.localhost}
|
||||
GOTIFY_HOSTNAME: ${GOTIFY_HOSTNAME:-gotify.localhost}
|
||||
IGOTIFY_HOSTNAME: ${IGOTIFY_HOSTNAME:-igotify.localhost}
|
||||
TRAEFIK_MIDDLEWARES: ${TRAEFIK_MIDDLEWARES:-internal-ipallowlist@file}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.services.ferrule.loadbalancer.server.url=http://ferrule:8080
|
||||
- traefik.http.routers.ferrule.rule=Host(`${TELEMETRY_HOSTNAME:-telemetry.localhost}`)
|
||||
- traefik.http.routers.ferrule.middlewares=${TRAEFIK_MIDDLEWARES:-internal-ipallowlist@file}
|
||||
|
||||
gotify:
|
||||
networks:
|
||||
- default
|
||||
- hms-docker_proxy_net
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.services.ferrule-gotify.loadbalancer.server.url=http://ferrule-gotify:80
|
||||
- traefik.http.routers.ferrule-gotify.rule=Host(`${GOTIFY_HOSTNAME:-gotify.localhost}`)
|
||||
- traefik.http.routers.ferrule-gotify.middlewares=${TRAEFIK_MIDDLEWARES:-internal-ipallowlist@file}
|
||||
|
||||
igotify:
|
||||
networks:
|
||||
- default
|
||||
- hms-docker_proxy_net
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.services.ferrule-igotify.loadbalancer.server.url=http://ferrule-igotify:8080
|
||||
- traefik.http.routers.ferrule-igotify.rule=Host(`${IGOTIFY_HOSTNAME:-igotify.localhost}`)
|
||||
- traefik.http.routers.ferrule-igotify.middlewares=${TRAEFIK_MIDDLEWARES:-internal-ipallowlist@file}
|
||||
|
||||
networks:
|
||||
hms-docker_proxy_net:
|
||||
external: true
|
||||
@@ -0,0 +1,431 @@
|
||||
# Single compose file for both paths.
|
||||
#
|
||||
# Ferrule runs from its published image, a built binary: no source reaches
|
||||
# the Docker host. FERRULE_VERSION picks the release and is required.
|
||||
# docker-compose.build.yml, added beside this file, builds from source
|
||||
# instead, for development.
|
||||
#
|
||||
# Portainer GitOps: Portainer reads this file from the repository. Stack
|
||||
# variables come from Portainer's Environment variables panel.
|
||||
#
|
||||
# Plain CLI: `docker compose up -d` reads the same variables out of a local
|
||||
# .env automatically — Compose interpolates ${VAR} from .env without needing
|
||||
# an `env_file:` key, which is why there isn't one. An `env_file:` would
|
||||
# break the Portainer path, since .env is deliberately not in the repo.
|
||||
#
|
||||
# There is no top-level `name:` — Portainer sets the compose project name
|
||||
# from the stack name, and a hardcoded one here would fight it.
|
||||
|
||||
services:
|
||||
telemetry:
|
||||
# The published image, at the release FERRULE_VERSION names: 0.9.0, or
|
||||
# qa for what is under test. Required, so a stack never drifts onto a
|
||||
# version nobody chose, and never latest. FERRULE_IMAGE is the registry
|
||||
# path, one line to change when it moves. Its own container names keep
|
||||
# it apart from an earlier stack of this project on the same host.
|
||||
image: ${FERRULE_IMAGE:-git.uglyhousesneedlove.com/steve/ferrule}:${FERRULE_VERSION:?set FERRULE_VERSION to the release to run, such as 0.9.0}
|
||||
# Ask the registry on every deploy. A fixed release costs one quick
|
||||
# request; qa moves, and this is how a redeploy picks it up.
|
||||
pull_policy: always
|
||||
container_name: ferrule
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# false turns off Network, Protect, WAN and the probe, for a stack that
|
||||
# watches only hypervisors and storage — or one tried out beside an
|
||||
# existing stack, so the controller is not polled twice.
|
||||
UNIFI_ENABLED: ${UNIFI_ENABLED:-true}
|
||||
UNIFI_HOST: ${UNIFI_HOST:-}
|
||||
UNIFI_API_KEY: ${UNIFI_API_KEY:-}
|
||||
UNIFI_USERNAME: ${UNIFI_USERNAME:-}
|
||||
UNIFI_PASSWORD: ${UNIFI_PASSWORD:-}
|
||||
UNIFI_SITE: ${UNIFI_SITE:-default}
|
||||
# Protect on separate hardware (a UNVR) needs its own host and its own
|
||||
# credentials — the gateway's do not work on it. Leave these unset when
|
||||
# one console runs both applications.
|
||||
PROTECT_HOST: ${PROTECT_HOST:-}
|
||||
PROTECT_API_KEY: ${PROTECT_API_KEY:-}
|
||||
PROTECT_USERNAME: ${PROTECT_USERNAME:-}
|
||||
PROTECT_PASSWORD: ${PROTECT_PASSWORD:-}
|
||||
UNIFI_OS: ${UNIFI_OS:-true}
|
||||
UNIFI_VERIFY_TLS: ${UNIFI_VERIFY_TLS:-false}
|
||||
POLL_INTERVAL: ${POLL_INTERVAL:-30s}
|
||||
REQUEST_TIMEOUT: ${REQUEST_TIMEOUT:-20s}
|
||||
PROTECT_ENABLED: ${PROTECT_ENABLED:-true}
|
||||
PROTECT_BOOTSTRAP: ${PROTECT_BOOTSTRAP:-true}
|
||||
LISTEN_ADDR: ":8080"
|
||||
METRICS_URL: "http://victoria-metrics:8428"
|
||||
METRICS_PUSH: "true"
|
||||
|
||||
# WAN health and ISP accountability. The gateway WAN objects and the
|
||||
# cable modem DOCSIS state ride along with the device poll and cost no
|
||||
# extra requests; WAN_POLL_INTERVAL only paces the aggregated dashboard
|
||||
# and stat/health.
|
||||
WAN_ENABLED: ${WAN_ENABLED:-true}
|
||||
WAN_POLL_INTERVAL: ${WAN_POLL_INTERVAL:-60s}
|
||||
WAN_DASHBOARD_SCALE: ${WAN_DASHBOARD_SCALE:-24h}
|
||||
SPEEDTEST_INTERVAL: ${SPEEDTEST_INTERVAL:-15m}
|
||||
BACKFILL_ON_START: ${BACKFILL_ON_START:-true}
|
||||
|
||||
# The outage archive. Fixed path because it is a named volume, not a
|
||||
# host path — Portainer stacks cannot resolve relative host paths, and a
|
||||
# named volume needs none.
|
||||
LEDGER_PATH: /var/lib/ferrule/ledger.jsonl
|
||||
LEDGER_RETENTION: ${LEDGER_RETENTION:-8760h}
|
||||
|
||||
# Demarcation probe. Unset PROBE_TARGETS leaves it off. Point
|
||||
# wan_next_hop at your own WAN1 next hop, by its literal address.
|
||||
PROBE_TARGETS: ${PROBE_TARGETS:-}
|
||||
PROBE_INTERVAL: ${PROBE_INTERVAL:-60s}
|
||||
PROBE_COUNT: ${PROBE_COUNT:-10}
|
||||
|
||||
# Hypervisors and storage, one slot each. Every slot is listed because
|
||||
# Portainer only passes a variable into the container when this file
|
||||
# names it. The Settings page generates the lines for a new source:
|
||||
# SOURCE_1=proxmox https://pve1:8006 name=pve-lab user=telemetry@pve!ro
|
||||
# SOURCE_1_SECRET=<token secret>
|
||||
SOURCE_1: ${SOURCE_1:-}
|
||||
SOURCE_1_SECRET: ${SOURCE_1_SECRET:-}
|
||||
SOURCE_2: ${SOURCE_2:-}
|
||||
SOURCE_2_SECRET: ${SOURCE_2_SECRET:-}
|
||||
SOURCE_3: ${SOURCE_3:-}
|
||||
SOURCE_3_SECRET: ${SOURCE_3_SECRET:-}
|
||||
SOURCE_4: ${SOURCE_4:-}
|
||||
SOURCE_4_SECRET: ${SOURCE_4_SECRET:-}
|
||||
SOURCE_5: ${SOURCE_5:-}
|
||||
SOURCE_5_SECRET: ${SOURCE_5_SECRET:-}
|
||||
SOURCE_6: ${SOURCE_6:-}
|
||||
SOURCE_6_SECRET: ${SOURCE_6_SECRET:-}
|
||||
SOURCE_7: ${SOURCE_7:-}
|
||||
SOURCE_7_SECRET: ${SOURCE_7_SECRET:-}
|
||||
SOURCE_8: ${SOURCE_8:-}
|
||||
SOURCE_8_SECRET: ${SOURCE_8_SECRET:-}
|
||||
|
||||
# Sign-in. Off until ADMIN_PASSWORD (free) or OIDC_* / LDAP_* (paid)
|
||||
# are set. See PORTAINER.md.
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
|
||||
SESSION_TTL: ${SESSION_TTL:-168h}
|
||||
METRICS_TOKEN: ${METRICS_TOKEN:-}
|
||||
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
||||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||||
OIDC_ROLES: ${OIDC_ROLES:-}
|
||||
OIDC_NAME: ${OIDC_NAME:-}
|
||||
OIDC_SCOPES: ${OIDC_SCOPES:-}
|
||||
OIDC_GROUPS_CLAIM: ${OIDC_GROUPS_CLAIM:-}
|
||||
OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL:-}
|
||||
LDAP_URL: ${LDAP_URL:-}
|
||||
LDAP_STARTTLS: ${LDAP_STARTTLS:-false}
|
||||
LDAP_VERIFY_TLS: ${LDAP_VERIFY_TLS:-true}
|
||||
LDAP_CA_CERT: ${LDAP_CA_CERT:-}
|
||||
LDAP_BIND_DN: ${LDAP_BIND_DN:-}
|
||||
LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-}
|
||||
LDAP_BASE_DN: ${LDAP_BASE_DN:-}
|
||||
LDAP_USER_FILTER: ${LDAP_USER_FILTER:-}
|
||||
LDAP_GROUP_ATTR: ${LDAP_GROUP_ATTR:-}
|
||||
LDAP_ROLES: ${LDAP_ROLES:-}
|
||||
|
||||
# The license key, when there is one. Without it every install runs a
|
||||
# 30-day trial of the paid features, then carries on as Community.
|
||||
FERRULE_LICENSE: ${FERRULE_LICENSE:-}
|
||||
|
||||
# Notification channels. Each is off until its variables are set; any
|
||||
# number can be on. Slack, Teams, PagerDuty and the webhook are paid.
|
||||
# Settings -> Notifications shows each one's state and the rules in
|
||||
# force; see PORTAINER.md for what each variable means.
|
||||
GOTIFY_URL: ${GOTIFY_URL:-}
|
||||
GOTIFY_TOKEN: ${GOTIFY_TOKEN:-}
|
||||
NTFY_URL: ${NTFY_URL:-}
|
||||
NTFY_TOKEN: ${NTFY_TOKEN:-}
|
||||
PUSHOVER_TOKEN: ${PUSHOVER_TOKEN:-}
|
||||
PUSHOVER_USER: ${PUSHOVER_USER:-}
|
||||
DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL:-}
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_TLS: ${SMTP_TLS:-}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_FROM: ${SMTP_FROM:-}
|
||||
SMTP_TO: ${SMTP_TO:-}
|
||||
SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL:-}
|
||||
TEAMS_WEBHOOK_URL: ${TEAMS_WEBHOOK_URL:-}
|
||||
PAGERDUTY_ROUTING_KEY: ${PAGERDUTY_ROUTING_KEY:-}
|
||||
WEBHOOK_URL: ${WEBHOOK_URL:-}
|
||||
WEBHOOK_SECRET: ${WEBHOOK_SECRET:-}
|
||||
APPRISE_URL: ${APPRISE_URL:-}
|
||||
APPRISE_URLS: ${APPRISE_URLS:-}
|
||||
APPRISE_KEY: ${APPRISE_KEY:-}
|
||||
|
||||
# Inventory of the vCenter and Proxmox VE sources, as Excel, CSV or
|
||||
# HTML from the Virtualization tab. Taking one by hand is free; the
|
||||
# daily run, history and the Changes sheet are paid. See PORTAINER.md.
|
||||
# Encrypts local users and groups on the volume (paid). 32 random bytes
|
||||
# in base64: openssl rand -base64 32, or Settings → Users and groups.
|
||||
SETTINGS_KEY: ${SETTINGS_KEY:-}
|
||||
|
||||
# The assistant (experimental, paid): Explain on alerts, Review on
|
||||
# sources, answered by a language model you choose. Up to four, one
|
||||
# per slot: the provider (claude, openai, gemini, xai, or llamacpp,
|
||||
# lmstudio, ollama or local), then the address of a model you run, then
|
||||
# optional name=, model=, context= and timeout=. The key goes in
|
||||
# AI_n_KEY. Settings -> Add a model writes them. See PORTAINER.md.
|
||||
AI_1: ${AI_1:-}
|
||||
AI_1_KEY: ${AI_1_KEY:-}
|
||||
AI_2: ${AI_2:-}
|
||||
AI_2_KEY: ${AI_2_KEY:-}
|
||||
AI_3: ${AI_3:-}
|
||||
AI_3_KEY: ${AI_3_KEY:-}
|
||||
AI_4: ${AI_4:-}
|
||||
AI_4_KEY: ${AI_4_KEY:-}
|
||||
# One model, as before there were slots: slot 1 when AI_1 is not set.
|
||||
AI_PROVIDER: ${AI_PROVIDER:-}
|
||||
AI_URL: ${AI_URL:-}
|
||||
AI_MODEL: ${AI_MODEL:-}
|
||||
AI_API_KEY: ${AI_API_KEY:-}
|
||||
AI_CONTEXT: ${AI_CONTEXT:-}
|
||||
AI_TIMEOUT: ${AI_TIMEOUT:-5m}
|
||||
|
||||
INVENTORY: ${INVENTORY:-on}
|
||||
INVENTORY_AT: ${INVENTORY_AT:-02:30}
|
||||
# Days of inventory history: a year by default; 2557 keeps seven years.
|
||||
INVENTORY_KEEP: ${INVENTORY_KEEP:-365}
|
||||
# A copy of every inventory in PostgreSQL, for Grafana and Power BI
|
||||
# (paid). For the stack's own, add "postgres" to COMPOSE_PROFILES and
|
||||
# set INVENTORY_DB_PASSWORD; for one you run, set INVENTORY_DB_URL. The
|
||||
# reader password makes a read-only ferrule_reader role for reports.
|
||||
INVENTORY_DB_URL: ${INVENTORY_DB_URL:-}
|
||||
INVENTORY_DB_PASSWORD: ${INVENTORY_DB_PASSWORD:-}
|
||||
INVENTORY_DB_READER_PASSWORD: ${INVENTORY_DB_READER_PASSWORD:-}
|
||||
INVENTORY_DB_CA: ${INVENTORY_DB_CA:-}
|
||||
|
||||
# Backups. Off until a target is set: BACKUP_DIR comes from an overlay
|
||||
# that mounts an NFS or SMB share (docker-compose.backup-nfs.yml,
|
||||
# docker-compose.backup-smb.yml); BACKUP_S3_* is object storage, paid.
|
||||
# See PORTAINER.md.
|
||||
BACKUP_NAME: ${BACKUP_NAME:-ferrule}
|
||||
BACKUP_KEY: ${BACKUP_KEY:-}
|
||||
BACKUP_AT: ${BACKUP_AT:-03:30}
|
||||
BACKUP_HISTORY: ${BACKUP_HISTORY:-true}
|
||||
BACKUP_HISTORY_DAY: ${BACKUP_HISTORY_DAY:-sunday}
|
||||
BACKUP_KEEP: ${BACKUP_KEEP:-14}
|
||||
BACKUP_HISTORY_KEEP: ${BACKUP_HISTORY_KEEP:-4}
|
||||
BACKUP_RESTORE: ${BACKUP_RESTORE:-auto}
|
||||
BACKUP_S3_ENDPOINT: ${BACKUP_S3_ENDPOINT:-}
|
||||
BACKUP_S3_REGION: ${BACKUP_S3_REGION:-us-east-1}
|
||||
BACKUP_S3_BUCKET: ${BACKUP_S3_BUCKET:-}
|
||||
BACKUP_S3_PREFIX: ${BACKUP_S3_PREFIX:-ferrule}
|
||||
BACKUP_S3_ACCESS_KEY: ${BACKUP_S3_ACCESS_KEY:-}
|
||||
BACKUP_S3_SECRET_KEY: ${BACKUP_S3_SECRET_KEY:-}
|
||||
DASHBOARD_URL: ${DASHBOARD_URL:-}
|
||||
NOTIFY_DEFAULT: ${NOTIFY_DEFAULT:-bad}
|
||||
NOTIFY_RULES: ${NOTIFY_RULES:-}
|
||||
NOTIFY_DELAY: ${NOTIFY_DELAY:-2m}
|
||||
NOTIFY_ON_CLEAR: ${NOTIFY_ON_CLEAR:-true}
|
||||
NOTIFY_REPEAT: ${NOTIFY_REPEAT:-off}
|
||||
NOTIFY_QUIET: ${NOTIFY_QUIET:-}
|
||||
TZ: ${TZ:-}
|
||||
PROBE_TIMEOUT: ${PROBE_TIMEOUT:-2s}
|
||||
|
||||
# Read by Compose, not the collector: passed in only so Settings ->
|
||||
# Stack environment can start from what is set. Never a secret.
|
||||
COMPOSE_PROFILES: ${COMPOSE_PROFILES:-}
|
||||
WEB_PORT: ${WEB_PORT:-8090}
|
||||
FERRULE_IMAGE: ${FERRULE_IMAGE:-git.uglyhousesneedlove.com/steve/ferrule}
|
||||
FERRULE_VERSION: ${FERRULE_VERSION:-}
|
||||
RETENTION: ${RETENTION:-13w}
|
||||
GOTIFY_ADMIN_USER: ${GOTIFY_ADMIN_USER:-admin}
|
||||
GOTIFY_PORT: ${GOTIFY_PORT:-8091}
|
||||
IGOTIFY_PORT: ${IGOTIFY_PORT:-8092}
|
||||
IGOTIFY_GOTIFY_URLS: ${IGOTIFY_GOTIFY_URLS:-http://gotify}
|
||||
APPRISE_PORT: ${APPRISE_PORT:-8093}
|
||||
INVENTORY_DB_BIND: ${INVENTORY_DB_BIND:-127.0.0.1}
|
||||
INVENTORY_DB_PORT: ${INVENTORY_DB_PORT:-5432}
|
||||
ports:
|
||||
# 8090 rather than 8080, so it does not collide with an existing
|
||||
# earlier deployment on the same host.
|
||||
- "${WEB_PORT:-8090}:8080"
|
||||
volumes:
|
||||
# A named volume, so there is no host path for Portainer to fail to
|
||||
# resolve. The root filesystem stays read-only; this is the one writable
|
||||
# mount, and it is what makes the outage archive outlive the controller
|
||||
# 21-hour window.
|
||||
- ledger-data:/var/lib/ferrule
|
||||
sysctls:
|
||||
# Lets the container open an unprivileged ICMP datagram socket for the
|
||||
# demarcation probe, with cap_drop: ALL still in force. The range must
|
||||
# include the UID/GID the image runs as, which is 65534. Without this the
|
||||
# probe reports itself unavailable and nothing else is affected.
|
||||
net.ipv4.ping_group_range: "65534 65534"
|
||||
depends_on:
|
||||
- victoria-metrics
|
||||
healthcheck:
|
||||
# The image is scratch — no shell, no curl. The binary probes itself.
|
||||
test: ["CMD", "/ferrule", "-healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 45s
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
victoria-metrics:
|
||||
# Pin to a release tag once you have one you are happy with.
|
||||
image: victoriametrics/victoria-metrics:latest
|
||||
container_name: ferrule-victoria-metrics
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "-storageDataPath=/victoria-metrics-data"
|
||||
- "-retentionPeriod=${RETENTION:-13w}"
|
||||
- "-httpListenAddr=:8428"
|
||||
- "-memory.allowedPercent=60"
|
||||
volumes:
|
||||
- vm-data:/victoria-metrics-data
|
||||
# Not published. The dashboard reaches it over the stack network and
|
||||
# proxies queries, so it never needs to be on your LAN.
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ---- Optional: Gotify and iGotify --------------------------------------
|
||||
#
|
||||
# Neither starts unless its profile is enabled on the stack:
|
||||
# COMPOSE_PROFILES=gotify,igotify both, for a host with neither
|
||||
# COMPOSE_PROFILES=igotify iGotify beside a Gotify you already run
|
||||
# Leave COMPOSE_PROFILES unset if you run both already, and point
|
||||
# GOTIFY_URL at your server.
|
||||
#
|
||||
# Inside the stack the collector reaches Gotify as http://gotify, so set
|
||||
# GOTIFY_URL=http://gotify when using the bundled one. First deploy, then
|
||||
# create an application in Gotify's web UI, put its token in GOTIFY_TOKEN,
|
||||
# and update the stack.
|
||||
#
|
||||
# For HMS-Docker hosts, docker-compose.hms-docker.yml adds the Traefik
|
||||
# routes and proxy network; see PORTAINER.md.
|
||||
gotify:
|
||||
profiles: ["gotify"]
|
||||
image: gotify/server:latest
|
||||
container_name: ferrule-gotify
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GOTIFY_DEFAULTUSER_NAME: ${GOTIFY_ADMIN_USER:-admin}
|
||||
# Set this. Unset, Gotify creates its admin with the password "admin".
|
||||
GOTIFY_DEFAULTUSER_PASS: ${GOTIFY_ADMIN_PASSWORD:-}
|
||||
TZ: ${TZ:-}
|
||||
volumes:
|
||||
# A named volume, like the outage archive: no host path to resolve.
|
||||
- gotify-data:/app/data
|
||||
ports:
|
||||
- "${GOTIFY_PORT:-8091}:80"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# Relays Gotify messages to the iGotify iPhone app through Apple's push
|
||||
# service. The phone must reach both Gotify and iGotify, so away from home
|
||||
# they need a tunnel or reverse proxy in front of them.
|
||||
igotify:
|
||||
profiles: ["igotify"]
|
||||
image: ghcr.io/androidseb25/igotify-notification-assist:latest
|
||||
container_name: ferrule-igotify
|
||||
hostname: igotify
|
||||
restart: unless-stopped
|
||||
pull_policy: always
|
||||
environment:
|
||||
# http://gotify for the bundled Gotify; your server's URL otherwise.
|
||||
GOTIFY_URLS: ${IGOTIFY_GOTIFY_URLS:-http://gotify}
|
||||
# Only needed when Gotify and iGotify are not reachable over public
|
||||
# domains. Set all three or none: GOTIFY_URLS, these two.
|
||||
GOTIFY_CLIENT_TOKENS: ${IGOTIFY_CLIENT_TOKENS:-}
|
||||
SECNTFY_TOKENS: ${IGOTIFY_SECNTFY_TOKENS:-}
|
||||
volumes:
|
||||
- igotify-data:/app/data
|
||||
ports:
|
||||
- "${IGOTIFY_PORT:-8092}:8080"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# ---- Optional: PostgreSQL, for the inventory's database copy ----------
|
||||
#
|
||||
# Starts only with "postgres" in COMPOSE_PROFILES. Set
|
||||
# INVENTORY_DB_PASSWORD and the collector finds it at ferrule-postgres on
|
||||
# its own; nothing else to set. It holds a copy: the inventory files on
|
||||
# ledger-data stay the master and travel with the backups, and the copy is
|
||||
# rebuilt from them, so this volume needs no backup of its own.
|
||||
#
|
||||
# Grafana and Power BI connect with their own PostgreSQL connectors, as
|
||||
# ferrule_reader with INVENTORY_DB_READER_PASSWORD, never as the writer.
|
||||
# The port listens on the Docker host's loopback only; set
|
||||
# INVENTORY_DB_BIND=0.0.0.0 for a reporting tool on another machine.
|
||||
postgres:
|
||||
profiles: ["postgres"]
|
||||
image: postgres:18-alpine
|
||||
container_name: ferrule-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ferrule
|
||||
POSTGRES_DB: ferrule
|
||||
# The image refuses to start without one, so a missing password shows
|
||||
# here rather than as an open database.
|
||||
POSTGRES_PASSWORD: ${INVENTORY_DB_PASSWORD:-}
|
||||
TZ: ${TZ:-}
|
||||
volumes:
|
||||
# PostgreSQL 18 keeps its data under a versioned directory here.
|
||||
- postgres-data:/var/lib/postgresql
|
||||
ports:
|
||||
- "${INVENTORY_DB_BIND:-127.0.0.1}:${INVENTORY_DB_PORT:-5432}:5432"
|
||||
# Postgres sets POSTGRES_PASSWORD only when its volume is first made, so
|
||||
# changing INVENTORY_DB_PASSWORD later left Ferrule refused. The first
|
||||
# check in each container sets the password from the environment, over
|
||||
# the container's own socket, which needs none, and marks /tmp so it is
|
||||
# done once: a redeploy with a new password is a new container with an
|
||||
# empty /tmp. (A password cannot be tested from in here: Postgres trusts
|
||||
# every connection from inside its container, 127.0.0.1 included.) The
|
||||
# password reaches SQL as a psql variable, so no quote in it can break
|
||||
# the statement.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ferrule -d ferrule && { [ -f /tmp/ferrule-password-set ] || { echo \"ALTER ROLE ferrule PASSWORD :'pw';\" | psql -U ferrule -d ferrule -q -v ON_ERROR_STOP=1 -v pw=\"$$POSTGRES_PASSWORD\" >/dev/null && touch /tmp/ferrule-password-set; }; }"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# Apprise: one paid channel that reaches more than a hundred services.
|
||||
# Starts only with "apprise" in COMPOSE_PROFILES; then set
|
||||
# APPRISE_URL=http://apprise:8000 and APPRISE_URLS to the services to
|
||||
# notify. With APPRISE_URLS it keeps nothing, so it needs no backup.
|
||||
apprise:
|
||||
profiles: ["apprise"]
|
||||
image: caronc/apprise:latest
|
||||
container_name: ferrule-apprise
|
||||
hostname: apprise
|
||||
restart: unless-stopped
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- apprise-config:/config
|
||||
ports:
|
||||
- "${APPRISE_PORT:-8093}:8000"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
volumes:
|
||||
vm-data:
|
||||
ledger-data:
|
||||
gotify-data:
|
||||
igotify-data:
|
||||
apprise-config:
|
||||
postgres-data:
|
||||
@@ -0,0 +1,132 @@
|
||||
# Once the dashboard is up, Settings -> Stack environment writes every
|
||||
# variable, this file's and the rest, from what the stack is running with.
|
||||
|
||||
# Required: the release to run, such as 0.9.0, or qa for the build under test.
|
||||
FERRULE_VERSION=0.01
|
||||
|
||||
# false watches only hypervisors and storage, with no UniFi console at all,
|
||||
# and is what to set while trying this stack beside an existing one.
|
||||
UNIFI_ENABLED=true
|
||||
UNIFI_HOST=https://192.168.1.1
|
||||
UNIFI_API_KEY=
|
||||
UNIFI_USERNAME=
|
||||
UNIFI_PASSWORD=
|
||||
|
||||
# Protect runs on separate hardware (a UNVR). It is its own UniFi OS console
|
||||
# with its own API key and its own local admin — the gateway's credentials
|
||||
# will not authenticate against it. Leave these blank if one console runs
|
||||
# both Network and Protect.
|
||||
PROTECT_HOST=https://192.168.1.4
|
||||
PROTECT_API_KEY=
|
||||
PROTECT_USERNAME=
|
||||
PROTECT_PASSWORD=
|
||||
|
||||
UNIFI_SITE=default
|
||||
UNIFI_OS=true
|
||||
UNIFI_VERIFY_TLS=false
|
||||
POLL_INTERVAL=30s
|
||||
PROTECT_ENABLED=true
|
||||
PROTECT_BOOTSTRAP=true
|
||||
WEB_PORT=8090
|
||||
RETENTION=13w
|
||||
|
||||
# ---- WAN health and ISP accountability -------------------------------
|
||||
WAN_ENABLED=true
|
||||
WAN_POLL_INTERVAL=60s
|
||||
WAN_DASHBOARD_SCALE=24h
|
||||
SPEEDTEST_INTERVAL=15m
|
||||
BACKFILL_ON_START=true
|
||||
LEDGER_RETENTION=8760h
|
||||
|
||||
# Demarcation probe. Leave PROBE_TARGETS blank to disable it. The
|
||||
# wan_next_hop address must be your ISP first router on the WAN1 subnet,
|
||||
# written literally: it stays reachable through a failover because it is
|
||||
# directly connected, which a default route would not be.
|
||||
#
|
||||
# Do not use 192.168.100.1 here. It is both the DOCSIS modem management
|
||||
# convention and the Starlink dish address, so what it reaches depends on
|
||||
# your network. Modem state comes from the controller instead.
|
||||
PROBE_TARGETS=
|
||||
PROBE_INTERVAL=60s
|
||||
PROBE_COUNT=10
|
||||
PROBE_TIMEOUT=2s
|
||||
|
||||
# ---- License -------------------------------------------------------------
|
||||
# Empty runs a 30-day trial of the paid features, then Community.
|
||||
FERRULE_LICENSE=
|
||||
|
||||
# ---- Notifications ------------------------------------------------------
|
||||
# Each channel is off until its variables are set. Settings -> Notifications
|
||||
# shows them all; PORTAINER.md says what each one is.
|
||||
GOTIFY_URL=
|
||||
GOTIFY_TOKEN=
|
||||
NTFY_URL=
|
||||
NTFY_TOKEN=
|
||||
PUSHOVER_TOKEN=
|
||||
PUSHOVER_USER=
|
||||
DISCORD_WEBHOOK_URL=
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_TLS=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
SMTP_TO=
|
||||
# Paid: send during the trial or with a license.
|
||||
SLACK_WEBHOOK_URL=
|
||||
TEAMS_WEBHOOK_URL=
|
||||
PAGERDUTY_ROUTING_KEY=
|
||||
WEBHOOK_URL=
|
||||
WEBHOOK_SECRET=
|
||||
APPRISE_URL=
|
||||
APPRISE_URLS=
|
||||
APPRISE_KEY=
|
||||
DASHBOARD_URL=
|
||||
|
||||
# ---- Backups -------------------------------------------------------------
|
||||
# A share needs an overlay: docker-compose.backup-nfs.yml or -smb.yml under
|
||||
# the stack's Additional paths. S3 is paid. See PORTAINER.md.
|
||||
BACKUP_NAME=ferrule
|
||||
BACKUP_KEY=
|
||||
BACKUP_AT=03:30
|
||||
BACKUP_HISTORY=true
|
||||
BACKUP_HISTORY_DAY=sunday
|
||||
BACKUP_KEEP=14
|
||||
BACKUP_HISTORY_KEEP=4
|
||||
BACKUP_RESTORE=auto
|
||||
# NFS overlay
|
||||
BACKUP_NFS_SERVER=
|
||||
BACKUP_NFS_PATH=
|
||||
# SMB overlay
|
||||
BACKUP_SMB_SERVER=
|
||||
BACKUP_SMB_SHARE=
|
||||
BACKUP_SMB_USERNAME=
|
||||
BACKUP_SMB_PASSWORD=
|
||||
# S3-compatible storage (paid)
|
||||
BACKUP_S3_ENDPOINT=
|
||||
BACKUP_S3_REGION=us-east-1
|
||||
BACKUP_S3_BUCKET=
|
||||
BACKUP_S3_PREFIX=ferrule
|
||||
BACKUP_S3_ACCESS_KEY=
|
||||
BACKUP_S3_SECRET_KEY=
|
||||
|
||||
# ---- Inventory ---------------------------------------------------------
|
||||
# Days of history: 365 by default, 2557 for seven years. The database copy
|
||||
# is paid: add postgres to COMPOSE_PROFILES and set INVENTORY_DB_PASSWORD
|
||||
# for the stack's own, or INVENTORY_DB_URL for yours. See PORTAINER.md.
|
||||
INVENTORY_KEEP=365
|
||||
INVENTORY_DB_URL=
|
||||
INVENTORY_DB_PASSWORD=
|
||||
# At least 12 characters; shorter and the read-only login is not made.
|
||||
INVENTORY_DB_READER_PASSWORD=
|
||||
INVENTORY_DB_BIND=127.0.0.1
|
||||
INVENTORY_DB_PORT=5432
|
||||
|
||||
# ---- Hypervisors and storage ------------------------------------------
|
||||
# One system per slot: kind, address, then optional name=, user=, every=,
|
||||
# tls=verify. The credential goes in the matching _SECRET. Kinds: proxmox,
|
||||
# pbs, vmware, synology, qnap, truenas. Settings -> Add a source writes these.
|
||||
SOURCE_1=
|
||||
SOURCE_1_SECRET=
|
||||
SOURCE_2=
|
||||
SOURCE_2_SECRET=
|
||||
Reference in new issue
Block a user