feat: add utilities + cameras batches (v0.9.11)

Utilities (8 modules):
- mealie: recipe manager, port 9925
- actualbudget: personal finance, port 5006
- traccar: GPS tracking, ports 8082 + 5000-5150 device protocols
- fmd: Android FindMyDevice server, random admin password, port 8084
- ddclient: dynamic DNS, config template, default start=n
- wg-easy: WireGuard VPN+UI, auto-detects public IP, random password
- meshcentral: remote device management, hostname prompt, ports 4430+4433
- magicmirror: smart mirror, 1-3 instances, MMM-* module auto-clone

Cameras (2 modules):
- frigate: AI NVR, auto-enables /dev/dri, starter config.yml, default start=n
- frigate-notify: Frigate push alerts, auto-detects local Frigate+ntfy, no web UI

https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
This commit is contained in:
Claude
2026-06-03 18:30:58 +00:00
parent a36f058a51
commit 2210dd4bed
12 changed files with 1256 additions and 1 deletions
+35
View File
@@ -4,6 +4,41 @@ All notable changes to this project. Versions follow `MAJOR.MINOR.PATCH`.
The project is pre-1.0 while the modular system reaches parity with the
monolithic `ubuntu-post-install-*.sh` scripts.
## [0.9.11] - 2026-06-03
### Added
- **Utilities batch** — 8 service modules migrated from the monolith:
- `services/mealie.sh` *(utilities)* — Recipe manager & meal planner. PUID/PGID baked;
default creds noted (change immediately). Port 9925 → internal 9000.
- `services/actualbudget.sh` *(utilities)* — Open-source personal finance (Actual Budget).
Minimal container; bank sync via SimpleFIN optional. Port 5006.
- `services/traccar.sh` *(utilities)* — GPS tracking server for phones, vehicles, assets.
Ships a starter `config/traccar.xml` with H2 embedded DB. Port 8082 + 5000-5150 device
protocols (TCP+UDP).
- `services/fmd.sh` *(utilities)* — FindMyDevice server for Android. Generates a random
admin password; mobile app from F-Droid (not Play Store). Port 8084.
- `services/ddclient.sh` *(utilities)* — Dynamic DNS updater; no web UI. Ships a
`config/ddclient.conf` template covering Cloudflare, DuckDNS, No-IP. Default start
prompt is "n" — edit config first.
- `services/wg-easy.sh` *(utilities)* — WireGuard VPN with web UI. Auto-detects public
IP for `WG_HOST`; generates random password; requires `NET_ADMIN` + `SYS_MODULE` caps
and `ip_forward` sysctl. Ports 51820/udp (VPN) + 51821/tcp (web).
- `services/meshcentral.sh` *(utilities)* — Self-hosted remote device management server.
Prompts for hostname (domain/IP for agent connections). Ports 4430 (HTTPS) + 4433 (agent).
- `services/magicmirror.sh` *(utilities)* — Modular smart mirror / info dashboard.
Multi-instance (1-3, ports 8081-8083); each instance in `~/docker/magicmirror/<N>/`.
Optionally copies existing `config.js` and auto-clones `MMM-*` third-party modules
from GitHub (tries MichMich → bugsounet → MagicMirrorOrg org order).
- **Cameras batch** — 2 service modules:
- `services/frigate.sh` *(cameras)* — AI-powered NVR with object detection. Auto-enables
`/dev/dri/renderD128` for hardware-accelerated detection when present; ships a starter
`config/config.yml` with camera examples. `privileged: true` + 1 GB tmpfs cache.
Ports 5000 (web), 8554 (RTSP restream), 8555 (WebRTC). Default start prompt is "n" —
edit config first.
- `services/frigate-notify.sh` *(cameras)* — Push notification sidecar for Frigate events.
Auto-detects local Frigate and ntfy installs to pre-fill config defaults. Supports ntfy,
Pushover, Discord, Gotify, Telegram, and more. No web UI.
## [0.9.10] - 2026-06-03
### Added
+1 -1
View File
@@ -1 +1 @@
0.9.10
0.9.11
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# services/actualbudget.sh — Open-source personal finance / budgeting (Actual Budget).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- ACTUALBUDGET ----).
# Own ~/docker/actualbudget/ with a standalone docker-compose.yml.
register_service actualbudget utilities "Open-source personal finance & budgeting (Actual Budget)" 5006
install_actualbudget() {
require_docker || return 1
local AB_DIR="$DOCKER_DIR/actualbudget"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Actual Budget would:"
echo " - Create $AB_DIR with docker-compose.yml (data/)"
echo " - Expose port 5006"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
mkdir -p "$AB_DIR/data"
ensure_docker_dir_ownership "$AB_DIR"
cd "$AB_DIR" || return 1
local TZ_VAL; TZ_VAL=$(cat /etc/timezone 2>/dev/null || echo "UTC")
cat > docker-compose.yml << 'AB_COMPOSE'
name: actualbudget
services:
actualbudget:
image: actualbudget/actual-server:latest
container_name: actualbudget
restart: unless-stopped
ports:
- "5006:5006"
volumes:
- ./data:/data
env_file:
- .env
AB_COMPOSE
cat > .env << AB_ENV
TZ=$TZ_VAL
AB_ENV
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
log_success "Actual Budget configured at $AB_DIR"
configure_caddy_for_service "ActualBudget" "5006" "budget"
write_readme "$AB_DIR" << MD
# Actual Budget
Open-source personal finance and budgeting tool. Supports bank sync via
SimpleFIN (requires a SimpleFIN account at simplefin.org).
- Web UI: http://localhost:5006
- App data: \`data/\`
## Manage
\`\`\`bash
cd $AB_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Notes
- First launch: create a budget file or import an existing one.
- Bank sync requires a SimpleFIN bridge subscription (simplefin.org).
MD
local START_AB=""
prompt_yn "Start Actual Budget now? (y/n):" "y" START_AB
if [ "$START_AB" = "y" ] || [ "$START_AB" = "Y" ]; then
docker compose up -d && log_success "Actual Budget started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:5006"
echo " Bank sync: simplefin.org (optional, paid)"
echo ""
}
+131
View File
@@ -0,0 +1,131 @@
#!/bin/bash
# services/ddclient.sh — Dynamic DNS updater (ddclient).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- DDCLIENT ----).
# Own ~/docker/ddclient/ with a standalone docker-compose.yml + config.
# Supports Cloudflare, DuckDNS, No-IP, and many other providers.
# Edit config/ddclient.conf before starting — no web UI.
register_service ddclient utilities "Dynamic DNS updater — keep your domain pointing at your home IP (ddclient)"
install_ddclient() {
require_docker || return 1
local DDCLIENT_DIR="$DOCKER_DIR/ddclient"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] ddclient would:"
echo " - Create $DDCLIENT_DIR with docker-compose.yml + config/ddclient.conf template"
echo " - No web UI — edit config/ddclient.conf for your DNS provider before starting"
return 0
fi
mkdir -p "$DDCLIENT_DIR"
ensure_docker_dir_ownership "$DDCLIENT_DIR"
cd "$DDCLIENT_DIR" || return 1
local TZ_VAL; TZ_VAL=$(cat /etc/timezone 2>/dev/null || echo "UTC")
cat > docker-compose.yml << 'DDCLIENT_COMPOSE'
name: ddclient
services:
ddclient:
image: lscr.io/linuxserver/ddclient:latest
container_name: ddclient
hostname: ddclient
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
volumes:
- ./config:/config
DDCLIENT_COMPOSE
cat > .env << DDCLIENT_ENV
TZ=$TZ_VAL
DDCLIENT_ENV
mkdir -p config
cat > config/ddclient.conf << 'DDCLIENT_CONF'
# ddclient configuration
# Docs: https://ddclient.net/
#
# ⚠️ YOU MUST EDIT THIS FILE before starting ddclient.
# Uncomment and fill in the block for your DNS provider.
daemon=300
syslog=yes
pid=/var/run/ddclient/ddclient.pid
ssl=yes
# Cloudflare example:
# use=web, web=cloudflare
# protocol=cloudflare
# zone=example.com
# login=token
# password=your-api-token
# example.com
# DuckDNS example:
# use=web
# protocol=duckdns
# password=your-duckdns-token
# yourdomain.duckdns.org
# No-IP example:
# use=web
# protocol=noip
# login=your@email.com
# password=your-password
# yourhostname.ddns.net
DDCLIENT_CONF
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DDCLIENT_DIR"
log_success "ddclient configured at $DDCLIENT_DIR"
write_readme "$DDCLIENT_DIR" << MD
# ddclient
Dynamic DNS client — keeps your domain pointing at your home IP address
even when your ISP changes it. No web UI; runs as a background daemon.
- Config: \`config/ddclient.conf\` — **edit before starting**
- Supported providers: Cloudflare, DuckDNS, No-IP, FreeDNS, and more
## Setup
1. Edit \`config/ddclient.conf\` for your DNS provider.
2. Start: \`docker compose up -d\`
3. Check logs: \`docker compose logs -f\`
## Manage
\`\`\`bash
cd $DDCLIENT_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Docs
- https://ddclient.net/
- Cloudflare setup: https://ddclient.net/protocols/cloudflare.html
MD
echo ""
log_warning "Edit config/ddclient.conf for your DNS provider before starting."
echo ""
local START_DDC=""
prompt_yn "Start ddclient now? (y/n):" "n" START_DDC
if [ "$START_DDC" = "y" ] || [ "$START_DDC" = "Y" ]; then
docker compose up -d && log_success "ddclient started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Config: $DDCLIENT_DIR/config/ddclient.conf"
echo " Docs: https://ddclient.net/"
echo ""
}
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
# services/fmd.sh — FindMyDevice server for Android device tracking (FMD).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- FINDMYDEVICE ----).
# Own ~/docker/fmd/ with a standalone docker-compose.yml + .env.
# Mobile app: "FindMyDevice" on F-Droid — not the Play Store version.
register_service fmd utilities "Android device tracking — alternative to Google Find My Device (FMD)" 8084
install_fmd() {
require_docker || return 1
local FMD_DIR="$DOCKER_DIR/fmd"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] FindMyDevice would:"
echo " - Create $FMD_DIR with docker-compose.yml + .env (data/)"
echo " - Generate a random admin password"
echo " - Expose port 8084"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
mkdir -p "$FMD_DIR"
ensure_docker_dir_ownership "$FMD_DIR"
cd "$FMD_DIR" || return 1
local FMD_PASS
FMD_PASS=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)
cat > docker-compose.yml << 'FMD_COMPOSE'
name: fmd
services:
fmd:
image: nulide/findmydevice
container_name: fmd
hostname: fmd
restart: unless-stopped
environment:
- FMD_ADMIN_PASSWORD=${FMD_ADMIN_PASSWORD}
volumes:
- ./data:/fmd/data
ports:
- "8084:8080"
FMD_COMPOSE
cat > .env << FMD_ENV
FMD_ADMIN_PASSWORD=$FMD_PASS
FMD_ENV
mkdir -p data
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR"
log_success "FindMyDevice configured at $FMD_DIR"
configure_caddy_for_service "FindMyDevice" "8084" "fmd"
write_readme "$FMD_DIR" << MD
# FindMyDevice (FMD)
Self-hosted Android device tracking — locate, lock, or wipe your device
from the web UI. Alternative to Google's Find My Device.
- Web UI: http://localhost:8084
- Admin password: stored in \`.env\` (\`FMD_ADMIN_PASSWORD\`)
- App data: \`data/\`
## Manage
\`\`\`bash
cd $FMD_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Mobile app
Install **FindMyDevice** from **F-Droid** (not the Play Store version):
1. Open the app → Settings → Server URL → \`http://YOUR-SERVER-IP:8084\`
2. Enter your admin password from \`.env\`
3. Grant location and accessibility permissions
MD
local START_FMD=""
prompt_yn "Start FindMyDevice now? (y/n):" "y" START_FMD
if [ "$START_FMD" = "y" ] || [ "$START_FMD" = "Y" ]; then
docker compose up -d && log_success "FindMyDevice started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:8084"
echo " Password: $FMD_PASS (saved in .env)"
echo " Mobile app: FindMyDevice on F-Droid"
echo ""
}
+135
View File
@@ -0,0 +1,135 @@
#!/bin/bash
# services/frigate-notify.sh — Push notification sidecar for Frigate events.
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- FRIGATE-NOTIFY ----).
# Own ~/docker/frigate-notify/ with a standalone docker-compose.yml + config.yml.
# Supports ntfy, Pushover, Discord, Gotify, Telegram, and more. No web UI.
# Auto-detects local Frigate and ntfy installs to pre-fill config defaults.
register_service frigate-notify cameras "Push alerts for Frigate detection events (Frigate-Notify)"
install_frigate-notify() {
require_docker || return 1
local FN_DIR="$DOCKER_DIR/frigate-notify"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Frigate-Notify would:"
echo " - Create $FN_DIR with docker-compose.yml + config.yml"
echo " - Auto-detect local Frigate and ntfy installs"
echo " - No web UI — configure via config.yml"
return 0
fi
mkdir -p "$FN_DIR"
ensure_docker_dir_ownership "$FN_DIR"
cd "$FN_DIR" || return 1
cat > docker-compose.yml << 'FN_COMPOSE'
name: frigate-notify
services:
frigate-notify:
image: ghcr.io/0x2142/frigate-notify:latest
container_name: frigate-notify
hostname: frigate-notify
restart: unless-stopped
volumes:
- ./config.yml:/app/config.yml:ro
FN_COMPOSE
# Smart defaults based on what's installed
local FRIGATE_URL="http://frigate:5000"
local NTFY_URL="https://ntfy.sh"
local NTFY_TOPIC="frigate-alerts"
if [ -d "$DOCKER_DIR/frigate" ]; then
log_success "Local Frigate detected — using http://frigate:5000"
else
log_warning "Frigate not found locally — using default URL (update config.yml if needed)"
fi
if [ -d "$DOCKER_DIR/ntfy" ]; then
NTFY_URL="http://ntfy:80"
log_success "Local ntfy detected — using http://ntfy:80"
else
log_warning "Local ntfy not found — using ntfy.sh (update config.yml for self-hosted)"
fi
echo ""
prompt_text "Frigate URL [$FRIGATE_URL]:" "$FRIGATE_URL" FRIGATE_URL
prompt_text "ntfy server URL [$NTFY_URL]:" "$NTFY_URL" NTFY_URL
prompt_text "ntfy topic [frigate-alerts]:" "frigate-alerts" NTFY_TOPIC
cat > config.yml << FN_CONFIG
# Frigate-Notify Configuration
# Docs: https://frigate-notify.0x2142.com
#
# Edit this file if notifications don't arrive — check Frigate URL,
# ntfy server, and that containers share a Docker network.
frigate:
server: $FRIGATE_URL
webapi:
enabled: true
interval: 30
alerts:
general:
send_startup_message: true
labels:
- person
- car
# - dog
# - package
notifiers:
- name: ntfy
enabled: true
provider: ntfy
config:
server: $NTFY_URL
topic: $NTFY_TOPIC
FN_CONFIG
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FN_DIR"
log_success "Frigate-Notify configured at $FN_DIR"
write_readme "$FN_DIR" << MD
# Frigate-Notify
Push notification sidecar for Frigate — sends alerts when Frigate detects
people, cars, animals, or custom objects. Supports ntfy, Pushover, Discord,
Gotify, Telegram, and more. No web UI.
- Config: \`config.yml\` — edit notification targets here
- Frigate events polled every 30 seconds by default
- Docs: https://frigate-notify.0x2142.com
## Manage
\`\`\`bash
cd $FN_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # check for delivery errors
docker compose pull && docker compose up -d # update
\`\`\`
## Adding more notifiers
Edit \`config.yml\` and add entries under \`notifiers:\`. Supported providers:
ntfy, Pushover, Discord (webhook), Gotify, Telegram, SMTP, and more.
See: https://frigate-notify.0x2142.com/configuration/alerts/
MD
local START_FN=""
prompt_yn "Start Frigate-Notify now? (y/n):" "y" START_FN
if [ "$START_FN" = "y" ] || [ "$START_FN" = "Y" ]; then
docker compose up -d && log_success "Frigate-Notify started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Config: $FN_DIR/config.yml"
echo " Docs: https://frigate-notify.0x2142.com"
echo ""
}
+170
View File
@@ -0,0 +1,170 @@
#!/bin/bash
# services/frigate.sh — AI-powered NVR for security cameras (Frigate).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- FRIGATE NVR ----).
# Own ~/docker/frigate/ with a standalone docker-compose.yml + .env + config.yml.
# Auto-enables /dev/dri/renderD128 for hardware detection (Intel/AMD) when present.
# YOU MUST edit config/config.yml to add your camera RTSP streams before starting.
register_service frigate cameras "AI-powered NVR — object detection on security cameras (Frigate)" 5000
install_frigate() {
require_docker || return 1
local FRIGATE_DIR="$DOCKER_DIR/frigate"
local DEFAULT_MEDIA="$ACTUAL_HOME/frigate"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Frigate would:"
echo " - Create $FRIGATE_DIR with docker-compose.yml + .env + config/config.yml"
echo " - Auto-enable /dev/dri/renderD128 for GPU-assisted detection if present"
echo " - Expose ports 5000 (web), 8554 (RTSP restream), 8555 (WebRTC)"
echo " - Write a starter config.yml — edit to add camera streams before starting"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
local FRIGATE_MEDIA=""
prompt_text "Path for recordings/snapshots [$DEFAULT_MEDIA]:" "$DEFAULT_MEDIA" FRIGATE_MEDIA
FRIGATE_MEDIA="${FRIGATE_MEDIA/#\~/$ACTUAL_HOME}"; FRIGATE_MEDIA="${FRIGATE_MEDIA%/}"
mkdir -p "$FRIGATE_DIR"
ensure_docker_dir_ownership "$FRIGATE_DIR"
cd "$FRIGATE_DIR" || return 1
local TZ_VAL; TZ_VAL=$(cat /etc/timezone 2>/dev/null || echo "UTC")
# Hardware detection: include /dev/dri only when a render node exists
local DEVICE_BLOCK=""
if [ -e /dev/dri/renderD128 ]; then
DEVICE_BLOCK=" devices:
- /dev/dri/renderD128:/dev/dri/renderD128"
log_success "Render node found — enabling hardware-accelerated detection"
else
log_warning "No /dev/dri/renderD128 — Frigate will use CPU detection."
fi
cat > docker-compose.yml << FRIGATE_COMPOSE
name: frigate
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:stable
container_name: frigate
hostname: frigate
restart: unless-stopped
privileged: true
shm_size: "256mb"
environment:
- TZ=$TZ_VAL
$DEVICE_BLOCK
volumes:
- ./config:/config
- \${FRIGATE_MEDIA}:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
ports:
- "5000:5000"
- "8554:8554"
- "8555:8555/tcp"
- "8555:8555/udp"
FRIGATE_COMPOSE
cat > .env << FRIGATE_ENV
FRIGATE_MEDIA=$FRIGATE_MEDIA
FRIGATE_ENV
mkdir -p config
mkdir -p "$FRIGATE_MEDIA"
cat > config/config.yml << 'FRIGATE_CONFIG'
# Frigate Configuration — Docs: https://docs.frigate.video
#
# ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate.
mqtt:
enabled: false # Set to true and configure if you use Home Assistant
cameras:
# Example — replace with your camera details:
# front_door:
# ffmpeg:
# inputs:
# - path: rtsp://user:pass@192.168.1.100:554/stream
# roles: [detect, record]
# detect:
# width: 1280
# height: 720
# fps: 5
detectors:
default:
type: cpu # Change to 'edgetpu' for Coral TPU or 'openvino' for Intel GPU
record:
enabled: true
retain:
days: 7
mode: motion
snapshots:
enabled: true
retain:
default: 7
FRIGATE_CONFIG
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
log_success "Frigate configured at $FRIGATE_DIR"
configure_caddy_for_service "Frigate" "5000" "frigate"
write_readme "$FRIGATE_DIR" << MD
# Frigate NVR
AI-powered network video recorder with real-time object detection for
security cameras. Detects people, cars, animals, and more.
- Web UI: http://localhost:5000
- RTSP restream: port 8554
- WebRTC: port 8555
- Recordings: \`$FRIGATE_MEDIA\`
- Config: \`config/config.yml\` — **add your camera RTSP streams here**
## Manage
\`\`\`bash
cd $FRIGATE_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## First steps
1. Edit \`config/config.yml\` — add your camera RTSP URLs under \`cameras:\`
2. Start Frigate: \`docker compose up -d\`
3. Open http://localhost:5000 to view cameras and configure detection zones
## Hardware acceleration
- Intel/AMD GPU: uncomment the \`devices: [/dev/dri/renderD128]\` block
- Google Coral TPU: set \`detectors.default.type: edgetpu\` + add USB device
- Docs: https://docs.frigate.video/configuration/hardware_acceleration
MD
echo ""
log_warning "Edit config/config.yml to add your camera RTSP streams before starting."
echo ""
local START_FRIGATE=""
prompt_yn "Start Frigate now anyway? (y/n):" "n" START_FRIGATE
if [ "$START_FRIGATE" = "y" ] || [ "$START_FRIGATE" = "Y" ]; then
docker compose up -d && log_success "Frigate started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:5000"
echo " Config: $FRIGATE_DIR/config/config.yml (add cameras here)"
echo ""
}
+171
View File
@@ -0,0 +1,171 @@
#!/bin/bash
# services/magicmirror.sh — Modular smart mirror / info dashboard (MagicMirror²).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- MAGIC MIRROR ----).
# Supports 1-3 instances (ports 8081-8083) each in ~/docker/magicmirror/<N>/.
# If you provide an existing config.js, third-party MMM-* modules are detected
# and cloned from GitHub automatically.
register_service magicmirror utilities "Modular smart mirror / info dashboard (MagicMirror²)" 8081
install_magicmirror() {
require_docker || return 1
local MM_BASE="$DOCKER_DIR/magicmirror"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] MagicMirror would:"
echo " - Ask how many instances (1-3, ports 8081-8083)"
echo " - Create $MM_BASE/<N>/ for each instance"
echo " - Optionally copy your existing config.js + clone MMM-* modules"
echo " - Offer a Caddy reverse proxy (first instance) and to start"
return 0
fi
# Number of instances
local MM_COUNT=""
prompt_text "How many MagicMirror instances? [1-3, default: 1]:" "1" MM_COUNT
MM_COUNT="${MM_COUNT:-1}"
[ "$MM_COUNT" -gt 3 ] 2>/dev/null && MM_COUNT=3
[ "$MM_COUNT" -lt 1 ] 2>/dev/null && MM_COUNT=1
mkdir -p "$MM_BASE"
chown "$ACTUAL_USER:$ACTUAL_USER" "$MM_BASE"
local TZ_VAL; TZ_VAL=$(cat /etc/timezone 2>/dev/null || echo "UTC")
local i MM_PORT MM_DIR
for i in $(seq 1 "$MM_COUNT"); do
MM_PORT=$((8080 + i))
MM_DIR="$MM_BASE/$i"
echo ""
echo "── Instance $i (port $MM_PORT) ──"
mkdir -p "$MM_DIR"
ensure_docker_dir_ownership "$MM_DIR"
cd "$MM_DIR" || continue
cat > docker-compose.yml << MM_COMPOSE
name: mm-$MM_PORT
services:
magicmirror:
image: karsten13/magicmirror:latest
container_name: magicmirror-$MM_PORT
hostname: magicmirror-$MM_PORT
restart: unless-stopped
environment:
- TZ=$TZ_VAL
volumes:
- ./config:/opt/magic_mirror/config
- ./modules:/opt/magic_mirror/modules
- ./css:/opt/magic_mirror/css
ports:
- "$MM_PORT:8080"
MM_COMPOSE
mkdir -p config modules css
# Offer to copy an existing config.js
local MM_CONFIG_CHOICE=""
echo ""
echo " Config options:"
echo " [1] Use default config (basic built-in modules)"
echo " [2] Copy existing config.js from a path"
if [ "$UNATTENDED" = true ]; then
MM_CONFIG_CHOICE="1"
else
read -r -p " Choose [1]: " MM_CONFIG_CHOICE
MM_CONFIG_CHOICE="${MM_CONFIG_CHOICE:-1}"
fi
if [ "$MM_CONFIG_CHOICE" = "2" ]; then
local MM_CONFIG_PATH=""
read -r -p " Path to config.js: " MM_CONFIG_PATH
if [ -f "$MM_CONFIG_PATH" ]; then
cp "$MM_CONFIG_PATH" config/config.js
log_success "Copied config from $MM_CONFIG_PATH"
# Copy custom.css if it exists next to config.js
local MM_CSS_DIR="${MM_CONFIG_PATH%/*}"
[ -f "$MM_CSS_DIR/custom.css" ] && cp "$MM_CSS_DIR/custom.css" css/custom.css && log_success "Copied custom.css"
# Detect MMM-* third-party modules referenced in config
local THIRD_PARTY_MODS
THIRD_PARTY_MODS=$(grep -oP "module:\s*[\"']MMM-[^\"']+[\"']" config/config.js 2>/dev/null \
| sed "s/module:\s*[\"']//g" | sed "s/[\"']//g" | sort -u)
if [ -n "$THIRD_PARTY_MODS" ]; then
echo ""
echo " Third-party modules found in config:"
echo "$THIRD_PARTY_MODS" | while read -r mod; do echo " - $mod"; done
echo ""
local MM_DL_MODS=""
prompt_yn " Download these modules from GitHub? (y/n):" "y" MM_DL_MODS
if [ "$MM_DL_MODS" = "y" ] || [ "$MM_DL_MODS" = "Y" ]; then
cd modules || true
echo "$THIRD_PARTY_MODS" | while read -r mod; do
[ -z "$mod" ] || [ -d "$mod" ] && continue
echo " Downloading $mod..."
git clone --depth 1 "https://github.com/MichMich/${mod}.git" 2>/dev/null || \
git clone --depth 1 "https://github.com/bugsounet/${mod}.git" 2>/dev/null || \
git clone --depth 1 "https://github.com/MagicMirrorOrg/${mod}.git" 2>/dev/null || \
log_warning "Could not find $mod — search at https://github.com/topics/magicmirror"
done
cd "$MM_DIR" || true
fi
fi
else
log_warning "File not found: $MM_CONFIG_PATH — using default config"
fi
fi
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MM_DIR"
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
# Offer Caddy only for first instance
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "$MM_PORT" "mirror"
local START_MM=""
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
if [ "$START_MM" = "y" ] || [ "$START_MM" = "Y" ]; then
docker compose up -d && log_success "MagicMirror instance $i started" || log_warning "Failed to start — check: docker compose logs"
fi
echo " Access at: http://localhost:$MM_PORT"
done
write_readme "$MM_BASE" << MD
# MagicMirror²
Modular smart mirror / info dashboard. Each instance has its own port and
independent config, modules, and CSS.
| Instance | Port | Directory |
|----------|------|-----------|
$(for j in $(seq 1 "$MM_COUNT"); do echo "| $j | $((8080 + j)) | \`$MM_BASE/$j/\` |"; done)
## Manage
\`\`\`bash
cd $MM_BASE/1
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Config
- Edit \`<instance>/config/config.js\` for layout and module settings.
- Add CSS overrides in \`<instance>/css/custom.css\`.
- Third-party modules go in \`<instance>/modules/<module-name>/\`.
Then run \`docker exec magicmirror-PORT sh -c 'cd /opt/magic_mirror/modules/<name> && npm install --production'\`
## Finding modules
Browse: https://github.com/topics/magicmirror
MD
echo ""
echo " MagicMirror config: $MM_BASE/<instance>/config/config.js"
echo ""
}
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# services/mealie.sh — Recipe manager & meal planner (Mealie).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- MEALIE ----).
# Own ~/docker/mealie/ with a standalone docker-compose.yml.
register_service mealie utilities "Recipe manager & meal planner (Mealie)" 9925
install_mealie() {
require_docker || return 1
local MEALIE_DIR="$DOCKER_DIR/mealie"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Mealie would:"
echo " - Create $MEALIE_DIR with docker-compose.yml (data/)"
echo " - Expose port 9925"
echo " - Default login: changeme@email.com / MyPassword (change immediately)"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
mkdir -p "$MEALIE_DIR"
ensure_docker_dir_ownership "$MEALIE_DIR"
cd "$MEALIE_DIR" || return 1
local TZ_VAL UID_VAL GID_VAL
TZ_VAL=$(cat /etc/timezone 2>/dev/null || echo "UTC")
UID_VAL=$(id -u "$ACTUAL_USER"); GID_VAL=$(id -g "$ACTUAL_USER")
cat > docker-compose.yml << MEALIE_COMPOSE
name: mealie
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:latest
container_name: mealie
hostname: mealie
restart: unless-stopped
environment:
- PUID=$UID_VAL
- PGID=$GID_VAL
- TZ=$TZ_VAL
- ALLOW_SIGNUP=true
- MAX_WORKERS=1
- WEB_CONCURRENCY=1
- BASE_URL=http://localhost:9925
volumes:
- ./data:/app/data
ports:
- "9925:9000"
MEALIE_COMPOSE
mkdir -p data
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
log_success "Mealie configured at $MEALIE_DIR"
configure_caddy_for_service "Mealie" "9925" "recipes"
write_readme "$MEALIE_DIR" << MD
# Mealie
Recipe manager and meal planner — import recipes from any URL, plan meals,
and generate shopping lists. Optional AI-powered recipe parsing.
- Web UI: http://localhost:9925
- Default login: changeme@email.com / MyPassword (change immediately!)
- App data: \`data/\`
## Manage
\`\`\`bash
cd $MEALIE_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Notes
- If using Caddy, update \`BASE_URL\` in \`docker-compose.yml\` to your domain.
MD
local START_MEALIE=""
prompt_yn "Start Mealie now? (y/n):" "y" START_MEALIE
if [ "$START_MEALIE" = "y" ] || [ "$START_MEALIE" = "Y" ]; then
docker compose up -d && log_success "Mealie started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:9925"
echo " Default: changeme@email.com / MyPassword (change immediately!)"
echo ""
}
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
# services/meshcentral.sh — Self-hosted remote device management server (MeshCentral).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- MESHCENTRAL SERVER ----).
# Own ~/docker/meshcentral/ with a standalone docker-compose.yml + .env.
# HTTPS on port 4430, agent listener on 4433. First visit: create admin account.
register_service meshcentral utilities "Self-hosted remote device management server (MeshCentral)" 4430
install_meshcentral() {
require_docker || return 1
local MC_DIR="$DOCKER_DIR/meshcentral"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] MeshCentral would:"
echo " - Create $MC_DIR with docker-compose.yml + .env (data/ files/ backups/)"
echo " - Prompt for hostname (domain or IP for agent connections)"
echo " - Expose port 4430 (HTTPS web) and 4433 (agent)"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
local MC_HOSTNAME=""
prompt_text "MeshCentral hostname (domain or IP) [localhost]:" "localhost" MC_HOSTNAME
MC_HOSTNAME="${MC_HOSTNAME:-localhost}"
mkdir -p "$MC_DIR"
ensure_docker_dir_ownership "$MC_DIR"
cd "$MC_DIR" || return 1
cat > docker-compose.yml << 'MC_COMPOSE'
name: meshcentral
services:
meshcentral:
image: ghcr.io/ylianst/meshcentral:latest
container_name: meshcentral
hostname: meshcentral
restart: unless-stopped
environment:
- NODE_ENV=production
- HOSTNAME=${MC_HOSTNAME:-localhost}
- REVERSE_PROXY=${MC_REVERSE_PROXY:-false}
- REVERSE_PROXY_TLS_PORT=${MC_TLS_PORT:-443}
- IFRAME=false
- ALLOW_NEW_ACCOUNTS=true
- WEBRTC=true
volumes:
- ./data:/opt/meshcentral/meshcentral-data
- ./files:/opt/meshcentral/meshcentral-files
- ./backups:/opt/meshcentral/meshcentral-backups
ports:
- "4430:443"
- "4433:4433"
MC_COMPOSE
cat > .env << MC_ENV
MC_HOSTNAME=$MC_HOSTNAME
MC_REVERSE_PROXY=false
MC_TLS_PORT=443
MC_ENV
mkdir -p data files backups
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR"
log_success "MeshCentral configured at $MC_DIR"
configure_caddy_for_service "MeshCentral" "4430" "mesh"
write_readme "$MC_DIR" << MD
# MeshCentral
Self-hosted remote device management — remotely access, manage, and monitor
all your computers from a single web interface. Install agents on each device.
- Web UI: https://localhost:4430 (self-signed cert on first launch)
- Agent listener: port 4433 (devices connect here — forward this port if remote)
- Hostname: \`$MC_HOSTNAME\` (update \`MC_HOSTNAME\` in .env if it changes)
- App data: \`data/\`, \`files/\`, \`backups/\`
## Manage
\`\`\`bash
cd $MC_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## First launch
1. Open https://localhost:4430 (accept the self-signed cert warning)
2. Create your admin account
3. Go to "My Devices" → "+ Add Device" → download the agent for each OS
4. Install the agent on every computer you want to manage
## Remote access
For devices outside your LAN to connect:
- Forward **TCP port 4433** on your router to this server
- Set \`MC_HOSTNAME\` in \`.env\` to your public domain/IP, then restart
## Docs
https://meshcentral.com/docs/
MD
local START_MC=""
prompt_yn "Start MeshCentral now? (y/n):" "y" START_MC
if [ "$START_MC" = "y" ] || [ "$START_MC" = "Y" ]; then
docker compose up -d && log_success "MeshCentral started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: https://localhost:4430 (accept self-signed cert)"
echo " First visit: create your admin account"
echo ""
}
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
# services/traccar.sh — GPS tracking server (Traccar).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- TRACCAR ----).
# Own ~/docker/traccar/ with a standalone docker-compose.yml + config XML.
register_service traccar utilities "GPS tracking server — phones, vehicles, assets (Traccar)" 8082
install_traccar() {
require_docker || return 1
local TRACCAR_DIR="$DOCKER_DIR/traccar"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Traccar would:"
echo " - Create $TRACCAR_DIR with docker-compose.yml + config/traccar.xml"
echo " - Expose port 8082 (web) and 5000-5150 (device protocols)"
echo " - Default login: admin@admin.com / admin (change immediately!)"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
mkdir -p "$TRACCAR_DIR"
ensure_docker_dir_ownership "$TRACCAR_DIR"
cd "$TRACCAR_DIR" || return 1
cat > docker-compose.yml << 'TRACCAR_COMPOSE'
name: traccar
services:
traccar:
image: traccar/traccar:latest
container_name: traccar
hostname: traccar
restart: unless-stopped
volumes:
- ./logs:/opt/traccar/logs:rw
- ./data:/opt/traccar/data:rw
- ./config/traccar.xml:/opt/traccar/conf/traccar.xml:ro
ports:
- "8082:8082"
- "5000-5150:5000-5150"
- "5000-5150:5000-5150/udp"
TRACCAR_COMPOSE
mkdir -p logs data config
cat > config/traccar.xml << 'TRACCAR_XML'
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE properties SYSTEM 'http://java.sun.com/dtd/properties.dtd'>
<properties>
<entry key='config.default'>./conf/default.xml</entry>
<entry key='database.driver'>org.h2.Driver</entry>
<entry key='database.url'>jdbc:h2:/opt/traccar/data/database</entry>
<entry key='database.user'>sa</entry>
<entry key='database.password'></entry>
</properties>
TRACCAR_XML
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR"
log_success "Traccar configured at $TRACCAR_DIR"
configure_caddy_for_service "Traccar" "8082" "traccar"
write_readme "$TRACCAR_DIR" << MD
# Traccar
GPS tracking server. Track phones, vehicles, and assets via the Traccar
Android/iOS app, OwnTracks, or any of 200+ supported device protocols.
- Web UI: http://localhost:8082
- Default login: admin@admin.com / admin (change immediately!)
- Device protocols: ports 5000-5150 (TCP + UDP)
- Config: \`config/traccar.xml\`
- App data: \`data/\` and \`logs/\`
## Manage
\`\`\`bash
cd $TRACCAR_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Mobile apps
- Traccar Client (Android/iOS): set server to \`http://YOUR-IP:8082\`
- OwnTracks (Android/iOS): configure HTTP endpoint to Traccar
MD
local START_TRACCAR=""
prompt_yn "Start Traccar now? (y/n):" "y" START_TRACCAR
if [ "$START_TRACCAR" = "y" ] || [ "$START_TRACCAR" = "Y" ]; then
docker compose up -d && log_success "Traccar started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:8082"
echo " Default: admin@admin.com / admin (change immediately!)"
echo ""
}
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
# services/wg-easy.sh — WireGuard VPN with a web management UI (wg-easy).
# Part of the modular post-install system (sourced by setup.sh).
#
# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- WG-EASY ----).
# Own ~/docker/wg-easy/ with a standalone docker-compose.yml + .env.
# Requires cap_add: NET_ADMIN + SYS_MODULE and ip_forward sysctl.
# Forward UDP 51820 on your router to this server for external VPN access.
register_service wg-easy utilities "WireGuard VPN with web management UI (wg-easy)" 51821
install_wg-easy() {
require_docker || return 1
local WGEASY_DIR="$DOCKER_DIR/wg-easy"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] wg-easy would:"
echo " - Create $WGEASY_DIR with docker-compose.yml + .env (config/)"
echo " - Auto-detect public IP for WG_HOST"
echo " - Generate a random web UI password"
echo " - Expose port 51821 (web UI) + 51820/udp (VPN)"
echo " - Require router port-forward: UDP 51820 → this server"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
mkdir -p "$WGEASY_DIR"
ensure_docker_dir_ownership "$WGEASY_DIR"
cd "$WGEASY_DIR" || return 1
# Auto-detect public IP as default for WG_HOST
local PUBLIC_IP WG_HOST WG_PASSWORD
PUBLIC_IP=$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null || echo "your-public-ip")
WG_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)
prompt_text "Public IP or hostname for VPN [$PUBLIC_IP]:" "$PUBLIC_IP" WG_HOST
cat > docker-compose.yml << 'WGEASY_COMPOSE'
name: wg-easy
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:latest
container_name: wg-easy
hostname: wg-easy
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
environment:
- WG_HOST=${WG_HOST}
- PASSWORD=${WG_PASSWORD}
- WG_DEFAULT_DNS=1.1.1.1
volumes:
- ./config:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
WGEASY_COMPOSE
cat > .env << WGEASY_ENV
WG_HOST=$WG_HOST
WG_PASSWORD=$WG_PASSWORD
WGEASY_ENV
mkdir -p config
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR"
log_success "wg-easy configured at $WGEASY_DIR"
configure_caddy_for_service "wg-easy" "51821" "vpn"
write_readme "$WGEASY_DIR" << MD
# wg-easy
WireGuard VPN with a web UI for managing clients, generating QR codes,
and monitoring connections.
- Web UI: http://localhost:51821
- VPN: UDP port 51820 (forward this on your router)
- Password: stored in \`.env\` (\`WG_PASSWORD\`)
- VPN host: \`$WG_HOST\` (update \`WG_HOST\` in .env if your IP changes)
- Config: \`config/\`
## Manage
\`\`\`bash
cd $WGEASY_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
## Router setup
Forward **UDP port 51820** to this server's LAN IP for external VPN access.
## Adding clients
Open http://localhost:51821, log in with your password, click "+ New Client",
download or scan the QR code with the WireGuard app.
MD
local START_WGEASY=""
prompt_yn "Start wg-easy now? (y/n):" "y" START_WGEASY
if [ "$START_WGEASY" = "y" ] || [ "$START_WGEASY" = "Y" ]; then
docker compose up -d && log_success "wg-easy started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Web UI: http://localhost:51821"
echo " Password: $WG_PASSWORD (saved in .env)"
echo " Router: forward UDP 51820 → this server for external VPN access"
echo ""
}