Files
collimo-deploy/docker-compose.yml
T
2026-10-23 17:55:07 +00:00

468 lines
20 KiB
YAML

# Single compose file for both paths.
#
# Collimo runs from its published image, a built binary: no source reaches
# the Docker host. COLLIMO_VERSION picks the release.
# 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 COLLIMO_VERSION names: 0.9.0, or
# qa for what is under test, and never latest. COLLIMO_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.
# Each release writes its own version and registry in as the defaults,
# so a blank stack runs it.
image: ${COLLIMO_IMAGE:-ghcr.io/collimohq/collimo}:${COLLIMO_VERSION:-0.1.2}
# 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: collimo
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}
PORT_SLEEPS: ${PORT_SLEEPS:-}
# 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/collimo/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:-}
# Up-checks: ping, HTTP or a TCP port for anything with an address,
# entries separated by ; such as
# CHECKS=ping 192.168.1.20 name=printer; http https://nas.lab:5001 name=nas
CHECKS: ${CHECKS:-}
# 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.
COLLIMO_LICENSE: ${COLLIMO_LICENSE:-}
# Several sites, for an MSP (ADR-0044): a collector at each client
# site reports to one hub, calling out only. Unset is one site on its
# own.
COLLIMO_ROLE: ${COLLIMO_ROLE:-standalone}
HUB_URL: ${HUB_URL:-}
HUB_ENROL: ${HUB_ENROL:-}
HUB_LINK: ${HUB_LINK:-normal}
# 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:-}
# With the bundled Gotify and no token, Collimo makes a shared
# collimo-alerts account and its own app, using the admin password.
GOTIFY_ALERTS_PASSWORD: ${GOTIFY_ALERTS_PASSWORD:-}
GOTIFY_ADMIN_PASSWORD: ${GOTIFY_ADMIN_PASSWORD:-}
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:-}
# open or all when SMTP_TO is a help desk's mailbox: one ticket per alert
SMTP_TICKETS: ${SMTP_TICKETS:-}
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}
# Capacity planning (paid): forecast thresholds in %, runway alert
# days, allocation limits, and hosts kept spare per cluster.
CAPACITY_THRESHOLDS: ${CAPACITY_THRESHOLDS:-cpu=80,mem=80,disk=85,pool=85}
CAPACITY_RUNWAY: ${CAPACITY_RUNWAY:-warn=90,bad=30}
CAPACITY_RATIOS: ${CAPACITY_RATIOS:-vcpu=4,vmem=1,disk=1.5}
CAPACITY_VM_SIZE: ${CAPACITY_VM_SIZE:-vcpu=4,mem=16,disk=100}
CAPACITY_HA: ${CAPACITY_HA:-}
# The monthly capacity summary: who gets it, whose name is on it, and
# the host "What to buy" plans in (empty: each cluster's largest).
CAPACITY_SUMMARY_TO: ${CAPACITY_SUMMARY_TO:-}
CAPACITY_SUMMARY_BY: ${CAPACITY_SUMMARY_BY:-}
CAPACITY_HOST: ${CAPACITY_HOST:-}
# Rated endurance per drive model, for disk wear: CT1000P310SSD8=220
CAPACITY_DISK_TBW: ${CAPACITY_DISK_TBW:-}
# 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 collimo_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:-collimo}
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:-collimo}
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}
COLLIMO_IMAGE: ${COLLIMO_IMAGE:-ghcr.io/collimohq/collimo}
COLLIMO_VERSION: ${COLLIMO_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/collimo
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", "/collimo", "-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: collimo-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: collimo-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: collimo-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 collimo-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
# collimo_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: collimo-postgres
restart: unless-stopped
environment:
POSTGRES_USER: collimo
POSTGRES_DB: collimo
# 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 Collimo 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 collimo -d collimo && { [ -f /tmp/collimo-password-set ] || { echo \"ALTER ROLE collimo PASSWORD :'pw';\" | psql -U collimo -d collimo -q -v ON_ERROR_STOP=1 -v pw=\"$$POSTGRES_PASSWORD\" >/dev/null && touch /tmp/collimo-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: collimo-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: