infra: full IaC state after hf-pdocker-01 rebuild (2026-03-08)
Replaces initial Nomad/Consul/Terraform skeleton with current Docker-based infrastructure across all three nodes (hf-pdocker-01, hf-pdocker-02, bart). Key changes: - CLAUDE.md: updated service distribution reflecting hf-pdocker-01 restored - docs/plans/2026-03-08-hf-pdocker-01-rebuild.md: new rebuild plan doc - compose/bart/: ai-agents, guacamole, matrix, media, openvas, smarthome, watchtower - compose/hf-pdocker-02/: traefik, monitoring, adguard, gitea, semaphore, wikijs, unifi, vaultwarden, actual-budget, mcpo, netclaw, trivy, watchtower, n8n - compose/hf-pdocker-01/: frigate, frigate-notify, farm-agent - stacks/: all Swarm stack definitions (monitoring, traefik, smarthome, matrix, guacamole, media, ai-agents, frigate, gitea, semaphore, wikijs, unifi, etc.) - .gitignore: exclude .env credential files and acme.json certs - .env.example: placeholder credential files for all services - .claude/agents/: gitea-backup and other infrastructure agent configs - scripts/: frigate media cleanup and NAS sync scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
services:
|
||||
margaret:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: margaret
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.openclaw
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.openclaw/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/margaret:/home/glyptic/.openclaw
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
spirit:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: spirit
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.spirit
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.spirit/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/spirit:/home/glyptic/.spirit
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
bob:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: bob
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.bob
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.bob/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/bob:/home/glyptic/.bob
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
clippy:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: clippy
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.clippy
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.clippy/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/clippy:/home/glyptic/.clippy
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,80 @@
|
||||
services:
|
||||
guacamole:
|
||||
image: guacamole/guacamole:latest
|
||||
container_name: guacamole
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GUACD_HOSTNAME=guacd
|
||||
- MYSQL_HOSTNAME=guacamole-db
|
||||
- MYSQL_DATABASE=guacamole_db
|
||||
- MYSQL_USER=guacamole_user
|
||||
- MYSQL_PASSWORD=guacamole_pass
|
||||
ports:
|
||||
- "8199:8080"
|
||||
networks:
|
||||
- guacamole-internal
|
||||
depends_on:
|
||||
guacamole-db:
|
||||
condition: service_healthy
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/guacamole/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
guacd:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- guacamole-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
guacamole-db:
|
||||
image: mysql:8.0
|
||||
container_name: guacamole-db
|
||||
user: "1024:100"
|
||||
restart: unless-stopped
|
||||
command: --innodb-use-native-aio=0 --innodb-buffer-pool-size=128M
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=guacamole_root
|
||||
- MYSQL_DATABASE=guacamole_db
|
||||
- MYSQL_USER=guacamole_user
|
||||
- MYSQL_PASSWORD=guacamole_pass
|
||||
volumes:
|
||||
- /var/lib/guacamole-db:/var/lib/mysql
|
||||
networks:
|
||||
- guacamole-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
guacamole-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,86 @@
|
||||
services:
|
||||
synapse:
|
||||
image: matrixdotorg/synapse:latest
|
||||
container_name: synapse
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SYNAPSE_SERVER_NAME=mess.hobbs.farm
|
||||
- SYNAPSE_REPORT_STATS=no
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/matrix/data:/data
|
||||
ports:
|
||||
- "8008:8008"
|
||||
networks:
|
||||
- matrix-internal
|
||||
depends_on:
|
||||
synapse-db:
|
||||
condition: service_healthy
|
||||
mem_limit: 1G
|
||||
memswap_limit: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
wellknown:
|
||||
image: nginx:alpine
|
||||
container_name: matrix-wellknown
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/matrix/wellknown:/usr/share/nginx/html/.well-known/matrix:ro
|
||||
ports:
|
||||
- "8010:80"
|
||||
mem_limit: 64M
|
||||
memswap_limit: 64M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:80/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
synapse-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: synapse-db
|
||||
user: "1024:100"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=synapse
|
||||
- POSTGRES_PASSWORD=synapse
|
||||
- POSTGRES_DB=synapse
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/matrix-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- matrix-internal
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
matrix-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,151 @@
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
container_name: jellyfin
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/jellyfin:/config
|
||||
- /mnt/nas-media:/media
|
||||
ports:
|
||||
- "8096:8096"
|
||||
mem_limit: 2G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8096/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
sonarr:
|
||||
image: linuxserver/sonarr:latest
|
||||
container_name: sonarr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/sonarr:/config
|
||||
- /mnt/nas-media:/media
|
||||
ports:
|
||||
- "8989:8989"
|
||||
mem_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8989/ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
radarr:
|
||||
image: linuxserver/radarr:latest
|
||||
container_name: radarr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/radarr:/config
|
||||
- /mnt/nas-media:/media
|
||||
ports:
|
||||
- "7878:7878"
|
||||
mem_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:7878/ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
sabnzbd:
|
||||
image: linuxserver/sabnzbd:latest
|
||||
container_name: sabnzbd
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/sabnzbd:/config
|
||||
- /mnt/nas-media:/media
|
||||
ports:
|
||||
- "8082:8080"
|
||||
mem_limit: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api?mode=version || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
nzbhydra2:
|
||||
image: linuxserver/nzbhydra2:latest
|
||||
container_name: nzbhydra2
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/nzbhydra:/config
|
||||
ports:
|
||||
- "5076:5076"
|
||||
mem_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:5076/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
jellyseerr:
|
||||
image: fallenbagel/jellyseerr:latest
|
||||
container_name: jellyseerr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/jellyseerr:/app/config
|
||||
ports:
|
||||
- "5055:5055"
|
||||
mem_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:5055/api/v1/status || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,2 @@
|
||||
OPENVAS_USERNAME=admin
|
||||
OPENVAS_PASSWORD=change_me
|
||||
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
openvas:
|
||||
image: immauss/openvas:latest
|
||||
container_name: openvas
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PASSWORD=${OPENVAS_PASSWORD}
|
||||
- USERNAME=${OPENVAS_USERNAME}
|
||||
volumes:
|
||||
- openvas-db:/data/database
|
||||
- openvas-var-lib:/data/var-lib
|
||||
- openvas-local-share:/data/local-share
|
||||
- openvas-local-etc:/data/local-etc
|
||||
- openvas-var-log:/data/var-log
|
||||
ports:
|
||||
- "9392:9392"
|
||||
mem_limit: 6G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
openvas-db:
|
||||
driver: local
|
||||
openvas-var-lib:
|
||||
driver: local
|
||||
openvas-local-share:
|
||||
driver: local
|
||||
openvas-local-etc:
|
||||
driver: local
|
||||
openvas-var-log:
|
||||
driver: local
|
||||
@@ -0,0 +1,65 @@
|
||||
services:
|
||||
homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:stable
|
||||
container_name: homeassistant
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/homeassistant:/config
|
||||
ports:
|
||||
- "8123:8123"
|
||||
dns:
|
||||
- 192.168.86.1
|
||||
- 8.8.8.8
|
||||
mem_limit: 4G
|
||||
memswap_limit: 4G
|
||||
depends_on:
|
||||
- mosquitto
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
esphome:
|
||||
image: esphome/esphome:latest
|
||||
container_name: esphome
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/esphome/config:/config
|
||||
ports:
|
||||
- "6052:6052"
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:6052/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
mosquitto:
|
||||
image: eclipse-mosquitto:latest
|
||||
container_name: mosquitto
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/mosquitto:/mosquitto
|
||||
ports:
|
||||
- "1883:1883"
|
||||
- "8883:8883"
|
||||
mem_limit: 128M
|
||||
memswap_limit: 128M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
container_name: watchtower
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
- WATCHTOWER_INCLUDE_RESTARTING=true
|
||||
- WATCHTOWER_POLL_INTERVAL=86400
|
||||
- WATCHTOWER_MONITOR_ONLY=false
|
||||
- TZ=America/Chicago
|
||||
- DOCKER_API_VERSION=1.44
|
||||
mem_limit: 256M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
farm-agent:
|
||||
image: farm-agent:latest
|
||||
container_name: farm-agent
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8889:8889"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
mem_limit: 128M
|
||||
memswap_limit: 128M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8889/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -1,3 +0,0 @@
|
||||
ANTHROPIC_API_KEY=<your-anthropic-api-key>
|
||||
MATRIX_ACCESS_TOKEN=<matrix-bot-access-token>
|
||||
MATRIX_ROOM_ID=<matrix-room-id>
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY app/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ .
|
||||
|
||||
EXPOSE 5199
|
||||
|
||||
CMD ["python", "-u", "main.py"]
|
||||
@@ -0,0 +1,259 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from threading import Thread
|
||||
|
||||
import aiohttp
|
||||
import anthropic
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from vision import analyze_snapshot
|
||||
from matrix_client import MatrixClient
|
||||
|
||||
# --- Configuration ---
|
||||
MQTT_HOST = os.environ.get("MQTT_HOST", "192.168.86.192")
|
||||
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
||||
FRIGATE_URL = os.environ.get("FRIGATE_URL", "http://192.168.86.192:5000")
|
||||
MATRIX_HOMESERVER = os.environ.get("MATRIX_HOMESERVER", "http://192.168.86.192:8008")
|
||||
MATRIX_ACCESS_TOKEN = os.environ["MATRIX_ACCESS_TOKEN"]
|
||||
MATRIX_ROOM_ID = os.environ["MATRIX_ROOM_ID"]
|
||||
HEALTH_PORT = int(os.environ.get("HEALTH_PORT", "5199"))
|
||||
|
||||
ALLOWED_LABELS = {"person", "car", "dog", "cat", "bird"}
|
||||
MIN_SCORE = 0.7
|
||||
RATE_LIMIT_SECONDS = 60
|
||||
DEDUP_WINDOW_SECONDS = 30
|
||||
|
||||
# --- Logging ---
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='{"time":"%(asctime)s","level":"%(levelname)s","module":"%(module)s","message":"%(message)s"}',
|
||||
stream=sys.stdout,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- State ---
|
||||
recent_events: dict[str, float] = {}
|
||||
last_notify_per_camera: defaultdict[str, float] = defaultdict(float)
|
||||
events_processed = 0
|
||||
last_event_time: str | None = None
|
||||
mqtt_connected = False
|
||||
|
||||
|
||||
# --- Health endpoint ---
|
||||
class HealthHandler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
body = json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"mqtt_connected": mqtt_connected,
|
||||
"events_processed": events_processed,
|
||||
"last_event": last_event_time,
|
||||
}
|
||||
)
|
||||
encoded = body.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # suppress access logs
|
||||
|
||||
|
||||
def start_health_server():
|
||||
server = HTTPServer(("0.0.0.0", HEALTH_PORT), HealthHandler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
logger.info("Health server started on port %d", HEALTH_PORT)
|
||||
|
||||
|
||||
# --- Snapshot fetching ---
|
||||
async def fetch_snapshot(event_id: str) -> bytes | None:
|
||||
url = f"{FRIGATE_URL}/api/events/{event_id}/snapshot.jpg"
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.read()
|
||||
logger.info(
|
||||
"Fetched snapshot for %s: %d bytes", event_id, len(data)
|
||||
)
|
||||
return data
|
||||
logger.warning(
|
||||
"Snapshot fetch failed for %s: HTTP %d", event_id, resp.status
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Snapshot fetch error for %s", event_id)
|
||||
return None
|
||||
|
||||
|
||||
# --- Event processing ---
|
||||
async def process_event(
|
||||
event_data: dict, claude: anthropic.Anthropic, matrix: MatrixClient
|
||||
):
|
||||
global events_processed, last_event_time
|
||||
|
||||
event_type = event_data.get("type")
|
||||
if event_type != "end":
|
||||
return
|
||||
|
||||
after = event_data.get("after", {})
|
||||
event_id = after.get("id", "")
|
||||
camera = after.get("camera", "unknown")
|
||||
label = after.get("label", "")
|
||||
score = after.get("top_score", after.get("score", 0))
|
||||
has_snapshot = after.get("has_snapshot", False)
|
||||
|
||||
# --- Filtering ---
|
||||
if label not in ALLOWED_LABELS:
|
||||
return
|
||||
if score < MIN_SCORE:
|
||||
return
|
||||
if not has_snapshot:
|
||||
return
|
||||
|
||||
# Dedup
|
||||
now = time.time()
|
||||
if event_id in recent_events and now - recent_events[event_id] < DEDUP_WINDOW_SECONDS:
|
||||
return
|
||||
recent_events[event_id] = now
|
||||
|
||||
# Rate limit per camera
|
||||
if now - last_notify_per_camera[camera] < RATE_LIMIT_SECONDS:
|
||||
logger.info("Rate limited: %s on %s", label, camera)
|
||||
return
|
||||
last_notify_per_camera[camera] = now
|
||||
|
||||
# Cleanup old dedup entries
|
||||
cutoff = now - DEDUP_WINDOW_SECONDS * 2
|
||||
for eid in [k for k, v in recent_events.items() if v < cutoff]:
|
||||
del recent_events[eid]
|
||||
|
||||
logger.info(
|
||||
"Processing event: %s on %s (%s, score=%.2f)", event_id, camera, label, score
|
||||
)
|
||||
|
||||
# --- Fetch snapshot ---
|
||||
snapshot = await fetch_snapshot(event_id)
|
||||
|
||||
# --- AI analysis ---
|
||||
description = None
|
||||
if snapshot:
|
||||
description = analyze_snapshot(claude, snapshot)
|
||||
if not description:
|
||||
description = f"{label.capitalize()} detected on {camera}."
|
||||
|
||||
# --- Format message ---
|
||||
score_pct = int(score * 100)
|
||||
ts = time.strftime("%I:%M:%S %p", time.localtime())
|
||||
date_str = time.strftime("%b %d, %Y", time.localtime())
|
||||
frigate_link = f"{FRIGATE_URL}/review?id={event_id}"
|
||||
|
||||
plain = (
|
||||
f"{camera} - {label.capitalize()} - {score_pct}%\n"
|
||||
f"{description}\n\n"
|
||||
f"View in Frigate: {frigate_link}\n"
|
||||
f"{ts} - {date_str}"
|
||||
)
|
||||
html = (
|
||||
f"<b>{camera}</b> · {label.capitalize()} · {score_pct}%<br/>"
|
||||
f"{description}<br/><br/>"
|
||||
f'<a href="{frigate_link}">View in Frigate</a><br/>'
|
||||
f"<small>{ts} · {date_str}</small>"
|
||||
)
|
||||
|
||||
# --- Send to Matrix ---
|
||||
if snapshot:
|
||||
filename = f"{camera}_{event_id}.jpg"
|
||||
mxc_uri = await matrix.upload_image(snapshot, filename)
|
||||
if mxc_uri:
|
||||
await matrix.send_image(mxc_uri, filename, len(snapshot))
|
||||
|
||||
await matrix.send_text(plain, html)
|
||||
|
||||
events_processed += 1
|
||||
last_event_time = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
logger.info("Notification sent for %s on %s", label, camera)
|
||||
|
||||
|
||||
# --- MQTT ---
|
||||
def create_mqtt_client(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
claude: anthropic.Anthropic,
|
||||
matrix: MatrixClient,
|
||||
):
|
||||
global mqtt_connected
|
||||
|
||||
def on_connect(client, userdata, flags, reason_code, properties):
|
||||
global mqtt_connected
|
||||
mqtt_connected = True
|
||||
client.subscribe("frigate/events")
|
||||
logger.info("MQTT connected, subscribed to frigate/events")
|
||||
|
||||
def on_disconnect(client, userdata, flags, reason_code, properties):
|
||||
global mqtt_connected
|
||||
mqtt_connected = False
|
||||
logger.warning("MQTT disconnected: %s", reason_code)
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
try:
|
||||
payload = json.loads(msg.payload)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
process_event(payload, claude, matrix), loop
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Invalid JSON on %s", msg.topic)
|
||||
except Exception:
|
||||
logger.exception("Error processing MQTT message")
|
||||
|
||||
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
client.on_connect = on_connect
|
||||
client.on_disconnect = on_disconnect
|
||||
client.on_message = on_message
|
||||
return client
|
||||
|
||||
|
||||
# --- Main ---
|
||||
def main():
|
||||
logger.info("Starting frigate-notify")
|
||||
|
||||
claude = anthropic.Anthropic()
|
||||
matrix = MatrixClient(MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop_thread = Thread(target=loop.run_forever, daemon=True)
|
||||
loop_thread.start()
|
||||
|
||||
start_health_server()
|
||||
|
||||
mqtt_client = create_mqtt_client(loop, claude, matrix)
|
||||
mqtt_client.connect(MQTT_HOST, MQTT_PORT)
|
||||
|
||||
def shutdown(signum, frame):
|
||||
logger.info("Shutting down...")
|
||||
mqtt_client.disconnect()
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
|
||||
mqtt_client.loop_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import aiohttp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RETRY_DELAYS = [1, 2, 4]
|
||||
|
||||
|
||||
class MatrixClient:
|
||||
def __init__(self, homeserver: str, token: str, room_id: str):
|
||||
self.homeserver = homeserver.rstrip("/")
|
||||
self.token = token
|
||||
self.room_id = room_id
|
||||
self.headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async def _request(self, method: str, url: str, **kwargs) -> dict | None:
|
||||
for attempt, delay in enumerate(RETRY_DELAYS):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method, url, headers=self.headers, **kwargs
|
||||
) as resp:
|
||||
if resp.status < 300:
|
||||
return await resp.json()
|
||||
body = await resp.text()
|
||||
logger.warning(
|
||||
"Matrix API %s (attempt %d): %s",
|
||||
resp.status,
|
||||
attempt + 1,
|
||||
body,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Matrix request failed (attempt %d)", attempt + 1)
|
||||
if attempt < len(RETRY_DELAYS) - 1:
|
||||
await asyncio.sleep(delay)
|
||||
logger.error("All Matrix retries exhausted for %s", url)
|
||||
return None
|
||||
|
||||
async def upload_image(self, image_bytes: bytes, filename: str) -> str | None:
|
||||
url = f"{self.homeserver}/_matrix/media/v3/upload?filename={filename}"
|
||||
headers = {**self.headers, "Content-Type": "image/jpeg"}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, headers=headers, data=image_bytes) as resp:
|
||||
if resp.status < 300:
|
||||
data = await resp.json()
|
||||
mxc_uri = data.get("content_uri")
|
||||
logger.info("Uploaded image: %s", mxc_uri)
|
||||
return mxc_uri
|
||||
body = await resp.text()
|
||||
logger.error("Image upload failed %s: %s", resp.status, body)
|
||||
except Exception:
|
||||
logger.exception("Image upload failed")
|
||||
return None
|
||||
|
||||
async def send_image(self, mxc_uri: str, filename: str, size: int) -> dict | None:
|
||||
txn_id = str(int(time.time() * 1000))
|
||||
url = (
|
||||
f"{self.homeserver}/_matrix/client/r0/rooms/"
|
||||
f"{self.room_id}/send/m.room.message/{txn_id}"
|
||||
)
|
||||
body = {
|
||||
"msgtype": "m.image",
|
||||
"body": filename,
|
||||
"url": mxc_uri,
|
||||
"info": {"mimetype": "image/jpeg", "size": size},
|
||||
}
|
||||
return await self._request("PUT", url, json=body)
|
||||
|
||||
async def send_text(self, plain: str, html: str) -> dict | None:
|
||||
txn_id = str(int(time.time() * 1000) + 1)
|
||||
url = (
|
||||
f"{self.homeserver}/_matrix/client/r0/rooms/"
|
||||
f"{self.room_id}/send/m.room.message/{txn_id}"
|
||||
)
|
||||
body = {
|
||||
"msgtype": "m.text",
|
||||
"body": plain,
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": html,
|
||||
}
|
||||
return await self._request("PUT", url, json=body)
|
||||
@@ -0,0 +1,3 @@
|
||||
paho-mqtt==2.1.0
|
||||
anthropic>=0.40.0
|
||||
aiohttp>=3.9.0
|
||||
@@ -0,0 +1,42 @@
|
||||
import base64
|
||||
import logging
|
||||
import anthropic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROMPT = (
|
||||
"Briefly describe what's happening in this security camera image in 1-2 sentences. "
|
||||
"Focus on the people, vehicles, or animals and their activity. Be concise."
|
||||
)
|
||||
|
||||
|
||||
def analyze_snapshot(client: anthropic.Anthropic, image_bytes: bytes) -> str | None:
|
||||
"""Send a snapshot to Claude Haiku for description. Returns None on failure."""
|
||||
try:
|
||||
b64 = base64.standard_b64encode(image_bytes).decode("utf-8")
|
||||
response = client.messages.create(
|
||||
model="claude-haiku-4-5-20251001",
|
||||
max_tokens=150,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": b64,
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": PROMPT},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
description = response.content[0].text.strip()
|
||||
logger.info("Vision analysis complete: %d chars", len(description))
|
||||
return description
|
||||
except Exception:
|
||||
logger.exception("Claude Vision API failed")
|
||||
return None
|
||||
@@ -0,0 +1,488 @@
|
||||
# =============================================================================
|
||||
# Frigate NVR Configuration - Optimized (v4)
|
||||
# =============================================================================
|
||||
#
|
||||
# 17 cameras total:
|
||||
#
|
||||
# Reolink (h264Preview paths, H.264):
|
||||
# back_porch 192.168.44.63 kerberos:kerberos
|
||||
# house_south_corner 192.168.44.61 admin:(blank)
|
||||
# house_south_yard 192.168.44.62 admin:(blank)
|
||||
# gate_camera 192.168.86.175 admin:(blank)
|
||||
#
|
||||
# Profile cameras (/profile1 + /profile2, H.264):
|
||||
# house_north 192.168.44.51 admin:123456
|
||||
# house_south 192.168.44.53 admin:123456
|
||||
# house_west 192.168.44.52 admin:123456
|
||||
# shop_west 192.168.44.56 admin:123456
|
||||
# shop_driveway 192.168.44.57 admin:123456
|
||||
# shop_east 192.168.44.55 admin:123456
|
||||
#
|
||||
# Stream camera (/stream1 + /stream2, HEVC):
|
||||
# house_east 192.168.44.58 admin:admin
|
||||
#
|
||||
# Dahua-style (/cam/realmonitor, codec TBD):
|
||||
# garage_2 192.168.44.64 admin:7rip8a115
|
||||
#
|
||||
# Amcrest w/ authbasic=64 (/cam/realmonitor):
|
||||
# driveway_1 192.168.86.150 admin:7rip8a115
|
||||
# driveway_2 192.168.44.68 admin:7rip8a115
|
||||
# front_door_view 192.168.44.59 admin:7rip8a115
|
||||
#
|
||||
# Amcrest ASH43-W (HEVC, /cam/realmonitor + authbasic=64):
|
||||
# pool_room 192.168.86.142 admin:bigdog01
|
||||
# front_house 192.168.44.41 admin:bigdog01
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
version: 0.17-0
|
||||
|
||||
auth:
|
||||
enabled: true
|
||||
|
||||
mqtt:
|
||||
enabled: true
|
||||
host: 192.168.86.192
|
||||
port: 1883
|
||||
|
||||
ui:
|
||||
timezone: America/Chicago
|
||||
|
||||
database:
|
||||
path: /config/frigate.db
|
||||
|
||||
# =============================================================================
|
||||
# go2rtc - Single connection per camera, restreamed locally
|
||||
# =============================================================================
|
||||
go2rtc:
|
||||
webrtc:
|
||||
candidates:
|
||||
- 192.168.86.192:8555
|
||||
- stun:8555
|
||||
streams:
|
||||
|
||||
# --- Reolink cameras (h264Preview paths) ---------------------------------
|
||||
|
||||
back_porch:
|
||||
- rtsp://kerberos:kerberos@192.168.44.63:554/h264Preview_01_main
|
||||
back_porch_sub:
|
||||
- rtsp://kerberos:kerberos@192.168.44.63:554/h264Preview_01_sub
|
||||
|
||||
house_south_corner:
|
||||
- rtsp://admin:@192.168.44.61:554/h264Preview_01_main
|
||||
house_south_corner_sub:
|
||||
- rtsp://admin:@192.168.44.61:554/h264Preview_01_sub
|
||||
|
||||
house_south_yard:
|
||||
- rtsp://admin:@192.168.44.62:554/h264Preview_01_main
|
||||
house_south_yard_sub:
|
||||
- rtsp://admin:@192.168.44.62:554/h264Preview_01_sub
|
||||
|
||||
gate_camera:
|
||||
- rtsp://admin:@192.168.86.175:554/h264Preview_01_main
|
||||
gate_camera_sub:
|
||||
- rtsp://admin:@192.168.86.175:554/h264Preview_01_sub
|
||||
|
||||
# --- Profile cameras (/profile1 + /profile2) ----------------------------
|
||||
|
||||
house_north:
|
||||
- rtsp://admin:123456@192.168.44.51:554/profile1
|
||||
house_north_sub:
|
||||
- rtsp://admin:123456@192.168.44.51:554/profile2
|
||||
|
||||
house_south:
|
||||
- rtsp://admin:123456@192.168.44.53:554/profile1
|
||||
house_south_sub:
|
||||
- rtsp://admin:123456@192.168.44.53:554/profile2
|
||||
|
||||
house_west:
|
||||
- rtsp://admin:123456@192.168.44.52:554/
|
||||
house_west_sub:
|
||||
- rtsp://admin:123456@192.168.44.52:554/profile2
|
||||
|
||||
shop_west:
|
||||
- rtsp://admin:123456@192.168.44.56:554/profile1
|
||||
shop_west_sub:
|
||||
- rtsp://admin:123456@192.168.44.56:554/profile2
|
||||
|
||||
shop_driveway:
|
||||
- rtsp://admin:123456@192.168.44.57:554/profile1
|
||||
shop_driveway_sub:
|
||||
- rtsp://admin:123456@192.168.44.57:554/profile2
|
||||
|
||||
shop_east:
|
||||
- rtsp://admin:123456@192.168.44.55:554/profile1
|
||||
shop_east_sub:
|
||||
- rtsp://admin:123456@192.168.44.55:554/profile2
|
||||
|
||||
# --- Stream camera (HEVC) ------------------------------------------------
|
||||
|
||||
house_east:
|
||||
- rtsp://admin:admin@192.168.44.58:554/stream1#video=h264
|
||||
house_east_sub:
|
||||
- rtsp://admin:admin@192.168.44.58:554/stream2
|
||||
|
||||
# --- garage_2 - Dahua-style, NOT Amcrest ---------------------------------
|
||||
|
||||
garage_2:
|
||||
- rtsp://admin:7rip8a115@192.168.44.64:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
garage_2_sub:
|
||||
- rtsp://admin:7rip8a115@192.168.44.64:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
|
||||
# --- Amcrest cameras w/ authbasic=64 -------------------------------------
|
||||
|
||||
driveway_1:
|
||||
- rtsp://admin:7rip8a115@192.168.86.150:554/cam/realmonitor?channel=1&subtype=0&authbasic=64
|
||||
driveway_1_sub:
|
||||
- rtsp://admin:7rip8a115@192.168.86.150:554/cam/realmonitor?channel=1&subtype=1&authbasic=64
|
||||
|
||||
driveway_2:
|
||||
- rtsp://admin:7rip8a115@192.168.44.68:554/cam/realmonitor?channel=1&subtype=0&authbasic=64
|
||||
driveway_2_sub:
|
||||
- rtsp://admin:7rip8a115@192.168.44.68:554/cam/realmonitor?channel=1&subtype=1&authbasic=64
|
||||
|
||||
front_door_view:
|
||||
- rtsp://admin:7rip8a115@192.168.44.59:554/cam/realmonitor?channel=1&subtype=0&authbasic=64
|
||||
front_door_view_sub:
|
||||
- rtsp://admin:7rip8a115@192.168.44.59:554/cam/realmonitor?channel=1&subtype=1&authbasic=64
|
||||
|
||||
# --- ASH43-W cameras (HEVC, authbasic=64) --------------------------------
|
||||
|
||||
pool_room:
|
||||
- rtsp://admin:bigdog01@192.168.86.142:554/cam/realmonitor?channel=1&subtype=0&authbasic=64#video=h264
|
||||
pool_room_sub:
|
||||
- rtsp://admin:bigdog01@192.168.86.142:554/cam/realmonitor?channel=1&subtype=1&authbasic=64
|
||||
|
||||
front_house:
|
||||
- rtsp://admin:bigdog01@192.168.44.41:554/cam/realmonitor?channel=1&subtype=0&authbasic=64#video=h264
|
||||
front_house_sub:
|
||||
- rtsp://admin:bigdog01@192.168.44.41:554/cam/realmonitor?channel=1&subtype=1&authbasic=64
|
||||
|
||||
# =============================================================================
|
||||
# Global ffmpeg - VAAPI hardware acceleration
|
||||
# =============================================================================
|
||||
ffmpeg:
|
||||
hwaccel_args: preset-vaapi
|
||||
|
||||
# =============================================================================
|
||||
model:
|
||||
path: /openvino-model/ssdlite_mobilenet_v2.xml
|
||||
width: 300
|
||||
height: 300
|
||||
input_tensor: nhwc
|
||||
input_pixel_format: bgr
|
||||
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
|
||||
|
||||
detectors:
|
||||
ov:
|
||||
type: openvino
|
||||
device: GPU
|
||||
detect:
|
||||
enabled: true
|
||||
fps: 3
|
||||
width: 640
|
||||
height: 480
|
||||
|
||||
# =============================================================================
|
||||
# Recording
|
||||
# =============================================================================
|
||||
record:
|
||||
enabled: true
|
||||
continuous:
|
||||
days: 2
|
||||
alerts:
|
||||
retain:
|
||||
days: 7
|
||||
detections:
|
||||
retain:
|
||||
days: 7
|
||||
|
||||
# =============================================================================
|
||||
# Snapshots
|
||||
# =============================================================================
|
||||
snapshots:
|
||||
enabled: true
|
||||
clean_copy: true
|
||||
retain:
|
||||
default: 7
|
||||
mode: motion
|
||||
|
||||
# =============================================================================
|
||||
# Object tracking
|
||||
# =============================================================================
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
threshold: 0.7
|
||||
car:
|
||||
min_area: 10000
|
||||
threshold: 0.7
|
||||
|
||||
# =============================================================================
|
||||
# Birdseye - overview of all cameras
|
||||
# =============================================================================
|
||||
birdseye:
|
||||
enabled: true
|
||||
mode: objects
|
||||
|
||||
# =============================================================================
|
||||
# Camera definitions
|
||||
# =============================================================================
|
||||
|
||||
cameras:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reolink cameras (H.264, VAAPI OK)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
back_porch:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/back_porch_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/back_porch
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
house_south_corner:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_south_corner_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_south_corner
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
house_south_yard:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_south_yard_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_south_yard
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
gate_camera:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/gate_camera_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/gate_camera
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Profile cameras (H.264, VAAPI OK)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
house_north:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_north_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_north
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
house_south:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_south_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_south
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
house_west:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_west_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_west
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
shop_west:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/shop_west_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/shop_west
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
shop_driveway:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/shop_driveway_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/shop_driveway
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
shop_east:
|
||||
enabled: false
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/shop_east_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/shop_east
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# house_east - HEVC, VAAPI disabled (pw: admin)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
house_east:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
hwaccel_args: []
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/house_east_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/house_east
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# garage_2 - Dahua-style, codec TBD, VAAPI disabled as precaution
|
||||
# If H.264 confirmed, remove hwaccel_args override
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
garage_2:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
hwaccel_args: []
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/garage_2_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/garage_2
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Amcrest cameras w/ authbasic=64
|
||||
# Codec unknown - VAAPI disabled as precaution until confirmed H.264
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
driveway_1:
|
||||
enabled: false
|
||||
ffmpeg:
|
||||
hwaccel_args: []
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/driveway_1_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/driveway_1
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
driveway_2:
|
||||
enabled: false
|
||||
ffmpeg:
|
||||
hwaccel_args: []
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/driveway_2_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/driveway_2
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
front_door_view:
|
||||
enabled: false
|
||||
ffmpeg:
|
||||
hwaccel_args: []
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/front_door_view_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/front_door_view
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ASH43-W cameras (HEVC main, H.264 sub - go2rtc transcodes main to H.264)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
pool_room:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/pool_room_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/pool_room
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
|
||||
front_house:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/front_house_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
- path: rtsp://127.0.0.1:8554/front_house
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
frigate:
|
||||
image: ghcr.io/blakeblackshear/frigate:stable
|
||||
container_name: frigate
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
shm_size: "512mb"
|
||||
network_mode: host
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
- FRIGATE_RTSP_PASSWORD=password
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/frigate/config:/config
|
||||
- /mnt/frigate-recordings:/media/frigate
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
mem_limit: 12G
|
||||
memswap_limit: 12G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:5000/api/version || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
actual-budget:
|
||||
image: actualbudget/actual-server:latest
|
||||
container_name: actual-budget
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5006:5006"
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/actual-budget:/data
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:5006/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
adguardhome:
|
||||
image: adguard/adguardhome:latest
|
||||
container_name: adguardhome
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/adguardhome/work:/opt/adguardhome/work
|
||||
- /mnt/docker-data/configs/adguardhome/conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
- "3000:3000/tcp"
|
||||
- "853:853/tcp"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
caddy-vw:
|
||||
image: caddy:alpine
|
||||
container_name: caddy-vw
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/caddy-vw/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- /mnt/docker-data/configs/caddy-vw/data:/data
|
||||
- /mnt/docker-data/configs/caddy-vw/config:/config
|
||||
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1 @@
|
||||
GITEA_DB_PASSWORD=change_me
|
||||
@@ -0,0 +1,72 @@
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3003:3000"
|
||||
- "2222:22"
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-db:5432
|
||||
- GITEA__database__NAME=gitea
|
||||
- GITEA__database__USER=gitea
|
||||
- GITEA__database__PASSWD=${GITEA_DB_PASSWORD}
|
||||
- GITEA__server__SSH_PORT=2222
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/gitea:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
gitea-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- gitea-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
gitea-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: gitea-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
environment:
|
||||
- POSTGRES_DB=gitea
|
||||
- POSTGRES_USER=gitea
|
||||
- POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/gitea-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- gitea-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
gitea-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
mcpo:
|
||||
image: ghcr.io/open-webui/mcpo:main
|
||||
container_name: mcpo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/mcpo:/app/config
|
||||
command: ["--config", "/app/config/config.json"]
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8000/openapi.json || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,158 @@
|
||||
networks:
|
||||
monitoring-net:
|
||||
name: monitoring-net
|
||||
|
||||
services:
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
user: "104"
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/grafana:/var/lib/grafana
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
dns_search: ""
|
||||
extra_hosts:
|
||||
- "blackbox-exporter:172.24.0.4"
|
||||
- "alertmanager:172.24.0.5"
|
||||
- "influxdb:172.24.0.7"
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "9095:9090"
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/prometheus:/etc/prometheus
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:9090/-/healthy || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:latest
|
||||
container_name: alertmanager
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "9093:9093"
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/alertmanager:/etc/alertmanager
|
||||
- alertmanager-data:/alertmanager
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:9093/-/healthy || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8-alpine
|
||||
container_name: influxdb
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "8086:8086"
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/influxdb:/var/lib/influxdb
|
||||
mem_limit: 1G
|
||||
memswap_limit: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:8086/ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
matrix-alertmanager-receiver:
|
||||
image: metio/matrix-alertmanager-receiver:2026.2.25
|
||||
container_name: matrix-alertmanager-receiver
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "9094:9094"
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/alertmanager/matrix-receiver-config.yml:/config/config.yml:ro
|
||||
command: ["--config-path", "/config/config.yml"]
|
||||
mem_limit: 128M
|
||||
memswap_limit: 128M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
blackbox-exporter:
|
||||
image: prom/blackbox-exporter:latest
|
||||
container_name: blackbox-exporter
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring-net
|
||||
ports:
|
||||
- "9115:9115"
|
||||
command:
|
||||
- '--config.file=/config/blackbox.yml'
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/blackbox:/config:ro
|
||||
mem_limit: 128M
|
||||
memswap_limit: 128M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
alertmanager-data:
|
||||
@@ -0,0 +1,2 @@
|
||||
N8N_DB_PASSWORD=change_me
|
||||
N8N_ENCRYPTION_KEY=change_me_32_char_random_key_here
|
||||
@@ -0,0 +1,70 @@
|
||||
services:
|
||||
n8n:
|
||||
image: n8nio/n8n:latest
|
||||
container_name: n8n
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=n8n-db
|
||||
- DB_POSTGRESDB_PORT=5432
|
||||
- DB_POSTGRESDB_DATABASE=n8n
|
||||
- DB_POSTGRESDB_USER=n8n
|
||||
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
|
||||
- N8N_PORT=5678
|
||||
- GENERIC_TIMEZONE=America/Chicago
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/n8n:/home/node/.n8n
|
||||
depends_on:
|
||||
n8n-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- n8n-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:5678/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 120s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
n8n-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: n8n-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=n8n
|
||||
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/n8n-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- n8n-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
n8n-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,2 @@
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
MATRIX_ACCESS_TOKEN=syt_...
|
||||
@@ -0,0 +1,38 @@
|
||||
FROM node:20-bookworm
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 python3-pip python3-venv python3-full \
|
||||
git curl jq openssh-client iputils-ping net-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv (Python package manager used by some MCP servers)
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Install OpenClaw globally
|
||||
RUN npm install -g openclaw@latest
|
||||
|
||||
# Clone NetClaw
|
||||
WORKDIR /opt/netclaw
|
||||
RUN git clone https://github.com/automateyournetwork/netclaw.git .
|
||||
|
||||
# Create openclaw home directory
|
||||
RUN mkdir -p /root/.openclaw/workspace
|
||||
|
||||
# Copy NetClaw workspace config into openclaw home
|
||||
RUN cp -r workspace/skills /root/.openclaw/workspace/ 2>/dev/null || true
|
||||
RUN cp config/openclaw.json /root/.openclaw/openclaw.json 2>/dev/null || true
|
||||
|
||||
# Install Python dependencies for core MCP servers
|
||||
RUN python3 -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
RUN pip install --no-cache-dir pyats genie netmiko paramiko requests pyyaml jinja2 2>/dev/null || true
|
||||
|
||||
# Create entrypoint script
|
||||
COPY entrypoint.sh /opt/entrypoint.sh
|
||||
RUN chmod +x /opt/entrypoint.sh
|
||||
|
||||
EXPOSE 1179
|
||||
|
||||
ENTRYPOINT ["/opt/entrypoint.sh"]
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
netclaw:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: netclaw-aaron
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- MATRIX_ACCESS_TOKEN=${MATRIX_ACCESS_TOKEN}
|
||||
- MATRIX_HOMESERVER=http://192.168.86.192:8008
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/netclaw:/root/.openclaw/data
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configure OpenClaw environment
|
||||
mkdir -p /root/.openclaw
|
||||
|
||||
# Write .env with API key
|
||||
cat > /root/.openclaw/.env << EOF
|
||||
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
EOF
|
||||
|
||||
# Write openclaw config for gateway mode
|
||||
cat > /root/.openclaw/openclaw.json << 'OCEOF'
|
||||
{
|
||||
"mode": "gateway",
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-6",
|
||||
"fallbackModel": "claude-haiku-4-5-20251001",
|
||||
"name": "Aaron",
|
||||
"identity": "Aaron - NetClaw AI Network Engineering Agent"
|
||||
}
|
||||
OCEOF
|
||||
|
||||
# Copy workspace skills if they exist
|
||||
if [ -d /opt/netclaw/workspace/skills ]; then
|
||||
cp -r /opt/netclaw/workspace/skills /root/.openclaw/workspace/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy personality/identity docs
|
||||
for doc in SOUL.md AGENTS.md IDENTITY.md USER.md TOOLS.md; do
|
||||
if [ -f /opt/netclaw/$doc ]; then
|
||||
cp /opt/netclaw/$doc /root/.openclaw/ 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Send Matrix DM to Jeremy to announce availability
|
||||
if [ -n "$MATRIX_ACCESS_TOKEN" ] && [ -n "$MATRIX_HOMESERVER" ]; then
|
||||
echo "Sending Matrix DM to Jeremy..."
|
||||
|
||||
# Create a direct message room with Jeremy
|
||||
ROOM_RESPONSE=$(curl -sf -X POST "${MATRIX_HOMESERVER}/_matrix/client/r0/createRoom" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||
-d "{
|
||||
\"is_direct\": true,
|
||||
\"invite\": [\"@jeremy:hobbs.farm\"],
|
||||
\"preset\": \"trusted_private_chat\"
|
||||
}" 2>/dev/null || echo "")
|
||||
|
||||
ROOM_ID=$(echo "$ROOM_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('room_id',''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$ROOM_ID" ]; then
|
||||
# Send the message
|
||||
curl -sf -X PUT "${MATRIX_HOMESERVER}/_matrix/client/r0/rooms/${ROOM_ID}/send/m.room.message/$(date +%s%N)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||
-d "{
|
||||
\"msgtype\": \"m.text\",
|
||||
\"body\": \"Hey Jeremy - Aaron here. I'm online and ready to help with network engineering tasks. I'm running NetClaw with CCIE-level networking skills. Ask me about device health checks, routing analysis, security audits, or anything network-related.\"
|
||||
}" 2>/dev/null || echo "Failed to send DM"
|
||||
echo "DM sent to @jeremy:hobbs.farm in room $ROOM_ID"
|
||||
else
|
||||
echo "Warning: Could not create DM room with Jeremy. Response: $ROOM_RESPONSE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start OpenClaw gateway
|
||||
echo "Starting NetClaw (Aaron) gateway..."
|
||||
exec openclaw gateway 2>&1 || {
|
||||
echo "OpenClaw gateway failed to start, keeping container alive for debugging..."
|
||||
tail -f /dev/null
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
SEMAPHORE_DB_PASSWORD=change_me
|
||||
SEMAPHORE_ADMIN_PASSWORD=change_me
|
||||
SEMAPHORE_ENCRYPTION_KEY=change_me_base64_encoded_32_byte_key
|
||||
SEMAPHORE_MYSQL_ROOT_PASSWORD=change_me
|
||||
@@ -0,0 +1,78 @@
|
||||
services:
|
||||
semaphore:
|
||||
image: semaphoreui/semaphore:latest
|
||||
container_name: semaphore
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3004:3000"
|
||||
environment:
|
||||
- SEMAPHORE_DB_DIALECT=mysql
|
||||
- SEMAPHORE_DB_HOST=semaphore-db
|
||||
- SEMAPHORE_DB_PORT=3306
|
||||
- SEMAPHORE_DB_USER=semaphore
|
||||
- SEMAPHORE_DB_PASS=${SEMAPHORE_DB_PASSWORD}
|
||||
- SEMAPHORE_DB_NAME=semaphore
|
||||
- SEMAPHORE_DB=semaphore
|
||||
- SEMAPHORE_PLAYBOOK_PATH=/tmp/semaphore
|
||||
- SEMAPHORE_ADMIN_PASSWORD=${SEMAPHORE_ADMIN_PASSWORD}
|
||||
- SEMAPHORE_ADMIN_NAME=admin
|
||||
- SEMAPHORE_ADMIN_EMAIL=admin@hobbs.farm
|
||||
- SEMAPHORE_ADMIN=admin
|
||||
- SEMAPHORE_ACCESS_KEY_ENCRYPTION=${SEMAPHORE_ENCRYPTION_KEY}
|
||||
- SEMAPHORE_LDAP_ACTIVATED=no
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/semaphore:/etc/semaphore
|
||||
- /mnt/docker-data/configs/semaphore/playbooks:/tmp/semaphore
|
||||
depends_on:
|
||||
semaphore-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- semaphore-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/ping || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
semaphore-db:
|
||||
image: mysql:8.0
|
||||
container_name: semaphore-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${SEMAPHORE_MYSQL_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=semaphore
|
||||
- MYSQL_USER=semaphore
|
||||
- MYSQL_PASSWORD=${SEMAPHORE_DB_PASSWORD}
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/semaphore-db:/var/lib/mysql
|
||||
networks:
|
||||
- semaphore-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
command: --innodb-buffer-pool-size=128M --innodb-log-file-size=32M
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
semaphore-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1 @@
|
||||
CF_DNS_API_TOKEN=change_me_cloudflare_api_token
|
||||
@@ -0,0 +1,39 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.6
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.file.directory=/config/dynamic"
|
||||
- "--providers.file.watch=true"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=jeremydanielhobbs@gmail.com"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
environment:
|
||||
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8090:8080"
|
||||
volumes:
|
||||
- /mnt/docker-data/traefik/letsencrypt:/letsencrypt
|
||||
- /mnt/docker-data/traefik/dynamic:/config/dynamic:ro
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/api/overview || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,88 @@
|
||||
http:
|
||||
routers:
|
||||
yoda:
|
||||
rule: "Host(`yoda.hobbs.farm`)"
|
||||
service: yoda
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
mess:
|
||||
rule: "Host(`mess.hobbs.farm`)"
|
||||
service: mess
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
mess-federation:
|
||||
rule: "Host(`mess.hobbs.farm`) && PathPrefix(`/_matrix/federation`)"
|
||||
service: mess
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
wellknown:
|
||||
rule: "Host(`hobbs.farm`) && PathPrefix(`/.well-known/matrix`)"
|
||||
service: wellknown
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
solo:
|
||||
rule: "Host(`solo.hobbs.farm`)"
|
||||
service: solo
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
vault:
|
||||
rule: "Host(`vault.hobbs.farm`)"
|
||||
service: vault
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
git:
|
||||
rule: "Host(`git.hobbs.farm`)"
|
||||
service: git
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
yoda:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.167:8123"
|
||||
|
||||
mess:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.167:8008"
|
||||
|
||||
wellknown:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.167:8010"
|
||||
|
||||
solo:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.167:8199"
|
||||
|
||||
vault:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.100:26226"
|
||||
|
||||
git:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.100:3003"
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
trivy:
|
||||
image: aquasec/trivy:latest
|
||||
container_name: trivy
|
||||
restart: unless-stopped
|
||||
command: server --listen 0.0.0.0:4954
|
||||
ports:
|
||||
- "4954:4954"
|
||||
volumes:
|
||||
- trivy-cache:/root/.cache/
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
trivy-cache:
|
||||
@@ -0,0 +1,2 @@
|
||||
UNIFI_MONGO_USER=unifi
|
||||
UNIFI_MONGO_PASS=change_me
|
||||
@@ -0,0 +1,67 @@
|
||||
services:
|
||||
unifi:
|
||||
image: lscr.io/linuxserver/unifi-network-application:latest
|
||||
container_name: unifi
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Chicago
|
||||
- MONGO_USER=${UNIFI_MONGO_USER}
|
||||
- MONGO_PASS=${UNIFI_MONGO_PASS}
|
||||
- MONGO_HOST=unifi-db
|
||||
- MONGO_PORT=27017
|
||||
- MONGO_DBNAME=unifi
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/unifi/data:/config
|
||||
ports:
|
||||
- "8443:8443"
|
||||
- "3478:3478/udp"
|
||||
- "10001:10001/udp"
|
||||
- "8080:8080"
|
||||
- "1900:1900/udp"
|
||||
- "8843:8843"
|
||||
- "8880:8880"
|
||||
- "6789:6789"
|
||||
- "5514:5514/udp"
|
||||
depends_on:
|
||||
unifi-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- unifi-internal
|
||||
mem_limit: 1G
|
||||
memswap_limit: 1G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
unifi-db:
|
||||
image: mongo:7.0
|
||||
container_name: unifi-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/unifi-db:/data/db
|
||||
networks:
|
||||
- unifi-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
mem_limit: 1G
|
||||
memswap_limit: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mongosh --eval 'db.adminCommand(\"ping\")' --quiet"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
unifi-internal:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DOMAIN=https://vault.hobbs.farm
|
||||
- SIGNUPS_ALLOWED=false
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/vaultwarden:/data
|
||||
ports:
|
||||
- "26226:80"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:80/alive || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
container_name: watchtower
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
- WATCHTOWER_INCLUDE_RESTARTING=true
|
||||
- WATCHTOWER_POLL_INTERVAL=86400
|
||||
- WATCHTOWER_MONITOR_ONLY=false
|
||||
- TZ=America/Chicago
|
||||
- DOCKER_API_VERSION=1.44
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1 @@
|
||||
WIKIJS_DB_PASSWORD=change_me
|
||||
@@ -0,0 +1,65 @@
|
||||
services:
|
||||
wikijs:
|
||||
image: ghcr.io/requarks/wiki:2
|
||||
container_name: wikijs
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
- DB_TYPE=postgres
|
||||
- DB_HOST=wikijs-db
|
||||
- DB_PORT=5432
|
||||
- DB_USER=wikijs
|
||||
- DB_PASS=${WIKIJS_DB_PASSWORD}
|
||||
- DB_NAME=wiki
|
||||
depends_on:
|
||||
wikijs-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- wikijs-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 120s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
wikijs-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: wikijs-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
environment:
|
||||
- POSTGRES_DB=wiki
|
||||
- POSTGRES_USER=wikijs
|
||||
- POSTGRES_PASSWORD=${WIKIJS_DB_PASSWORD}
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/wikijs-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- wikijs-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U wikijs -d wiki"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
wikijs-internal:
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user