# 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.3.1} # 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= 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:-} # The last-backup check leaves out VMs with this Proxmox VE or XCP-ng # tag, and VMs named in COVERAGE_SKIP (commas; * matches any run). COVERAGE_SKIP_TAG: ${COVERAGE_SKIP_TAG:-nobackup} COVERAGE_SKIP: ${COVERAGE_SKIP:-} # 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:-} # The Renewals tab watches every https source, https up-check and # console's certificate already; CERT_WATCH adds host[:port] entries, # commas between. DOMAINS_WATCH looks domains up over RDAP once a day, # which sends each name to its registry: empty, none. CERT_WATCH: ${CERT_WATCH:-} DOMAINS_WATCH: ${DOMAINS_WATCH:-} # The Tools tab's port scan reaches private addresses only; this can # narrow it to some of them, such as 192.168.1.0/24. TOOLS_SCAN_NETWORKS: ${TOOLS_SCAN_NETWORKS:-} # 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_ENROLL: ${HUB_ENROLL:-} HUB_LINK: ${HUB_LINK:-normal} # On a collector with the Tailscale sidecar: http://tailscale:1055, # for its link to a hub on the tailnet. Nothing else goes through it. HUB_PROXY: ${HUB_PROXY:-} # The Tailscale sidecar's name, only so Settings can show it. TS_HOSTNAME: ${TS_HOSTNAME:-collimo} # 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} # Allocation forecasts that alert (alloc_cpu, alloc_mem, alloc_disk); # empty, they are planning only and what is in use alerts. CAPACITY_ALLOC_ALERTS: ${CAPACITY_ALLOC_ALERTS:-} 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_CURRENCY: ${CAPACITY_CURRENCY:-} CAPACITY_HOST: ${CAPACITY_HOST:-} # Rated endurance per drive model, for disk wear: CT1000P310SSD8=220 CAPACITY_DISK_TBW: ${CAPACITY_DISK_TBW:-} # Each WAN's download/upload in Mb/s, for network capacity: # WAN=1000/40. Empty: its best speedtest of the last 30 days. CAPACITY_WAN: ${CAPACITY_WAN:-} # 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: # Pinned, like Collimo itself, so a redeploy never pulls a release we # haven't run. It moves forward with a Collimo release, never back: # VictoriaMetrics supports upgrading its data, not downgrading it. image: victoriametrics/victoria-metrics:v1.155.0 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 # ---- Optional: Tailscale, to reach Collimo on your tailnet ------------ # # Starts only with "tailscale" in COMPOSE_PROFILES, and TS_AUTHKEY set to # an auth key from Tailscale's admin console (Settings → Keys); tag it so # the device never expires. It joins the tailnet as TS_HOSTNAME and # serves the dashboard at https://..ts.net, # with no port open on the router. On a hub, that is HUB_URL. It also # offers a proxy on :1055, which a collector's link to that hub uses # through HUB_PROXY=http://tailscale:1055. # # Userspace networking: no tun device and no added capabilities. The # dashboard is served over HTTPS when the tailnet has HTTPS certificates # turned on (Tailscale admin → DNS), and over plain HTTP on port 80 # otherwise, which WireGuard still encrypts. tailscale: profiles: ["tailscale"] image: tailscale/tailscale:stable container_name: collimo-tailscale hostname: tailscale restart: unless-stopped environment: # Under containerboot's other name for it, so the key is never # passed to Collimo itself. TS_AUTH_KEY: ${TS_AUTHKEY:-} TS_HOSTNAME: ${TS_HOSTNAME:-collimo} TS_STATE_DIR: /var/lib/tailscale TS_SOCKET: /var/run/tailscale/tailscaled.sock TS_USERSPACE: "true" TS_OUTBOUND_HTTP_PROXY_LISTEN: ":1055" # containerboot starts tailscaled and logs in; once it is up, the # dashboard is served on the tailnet, set afresh on every start. Each # try is timed: with Serve off for the tailnet, tailscale waits for # someone to turn it on, and the proxy must not wait with it. entrypoint: ["/bin/sh", "-c"] command: - | /usr/local/bin/containerboot & boot=$$! until tailscale status >/dev/null 2>&1; do kill -0 $$boot 2>/dev/null || exit 1; sleep 2; done # Status answers from the cached map before control does; HTTPS asked # for then fails at once. Wait up to a minute for the tailnet's # certificate domain, then serve afresh: HTTPS with it, HTTP without. i=0; until tailscale status --json 2>/dev/null | grep -q '"CertDomains": \['; do i=$$((i+1)); [ $$i -ge 30 ] && break; sleep 2; done tailscale serve reset >/dev/null 2>&1 if [ $$i -lt 30 ]; then timeout 30 tailscale serve --bg --https=443 http://telemetry:8080; else echo 'collimo: HTTPS certificates are off for this tailnet (Tailscale admin -> DNS); serving plain HTTP'; false; fi || timeout 30 tailscale serve --bg --http=80 http://telemetry:8080 || echo 'collimo: could not serve on the tailnet; is Serve allowed for it?' wait $$boot volumes: - tailscale-state:/var/lib/tailscale security_opt: - no-new-privileges:true volumes: vm-data: ledger-data: gotify-data: igotify-data: apprise-config: postgres-data: tailscale-state: