Ferrule 0.01

This commit is contained in:
Ferrule release committed 2026-10-17 19:09:28 +00:00
1 parent aae9874dec
commit 5684699242
8 files changed
+1843 -2

No files matched your search

+15
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+79 -2
View File
@@ -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.
+36
View File
@@ -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}"
+41
View File
@@ -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}"
+65
View File
@@ -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
+431
View File
@@ -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:
+132
View File
@@ -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=