Add 6 new utility services: gatus, mail-archiver, rustdesk, unifi, vaultwarden, watchyourlan

- gatus: status/uptime monitoring page with hot-reloaded config
- mail-archiver: IMAP email archive and full-text search (postgres backend)
- rustdesk: self-hosted remote desktop relay, cross-VLAN/FQDN docs
- unifi: UniFi Network Application with MongoDB init via Docker configs
- vaultwarden: Bitwarden-compatible password manager with SMTP prompts
- watchyourlan: network device tracker (host networking for ARP scanning)

All follow caddy_net container-routing pattern except rustdesk (raw TCP/UDP)
and watchyourlan (network_mode: host, incompatible with caddy_net).

README.md services table updated with all 6 new entries.

https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
This commit is contained in:
Claude
2026-06-07 22:32:44 +00:00
parent 9c52ac22e1
commit 774f6ad561
7 changed files with 995 additions and 1 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ Update them any time with `sudo ./setup.sh configure`.
|-------|---------|
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo) |
| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant` |
| `utilities` | `actualbudget`, `ddclient`, `filebrowser`, `fmd`, `magicmirror`, `mealie`, `meshcentral`, `ntfy`, `portainer`, `traccar`, `uptimekuma`, `watchtower`, `wg-easy` |
| `utilities` | `actualbudget`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `magicmirror`, `mail-archiver`, `mealie`, `meshcentral`, `ntfy`, `portainer`, `rustdesk`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` |
| `media` | `arm`, `audiobookshelf`, `emby`, `immich`, `jellyfin`, `lyrion` |
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
| `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` |
+159
View File
@@ -0,0 +1,159 @@
#!/bin/bash
# services/gatus.sh — Gatus status/uptime monitoring page.
# Part of the modular post-install system (sourced by setup.sh).
#
# Gatus polls endpoints (HTTP, TCP, DNS, ICMP) on a schedule and shows a
# clean status dashboard. Config is hot-reloaded from gatus_config/config.yaml.
register_service gatus utilities "Status & uptime monitoring page (Gatus)" 8086
install_gatus() {
require_docker || return 1
log_info "Installing Gatus..."
local GATUS_DIR="$DOCKER_DIR/gatus"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $GATUS_DIR (gatus_config/, gatus_data/)"
echo "[DRY-RUN] Would deploy twinproduction/gatus:latest"
echo "[DRY-RUN] Port 8086 published, config at gatus_config/config.yaml"
return 0
fi
mkdir -p "$GATUS_DIR/gatus_config" "$GATUS_DIR/gatus_data"
ensure_docker_dir_ownership "$GATUS_DIR"
cd "$GATUS_DIR" || return 1
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
cat > docker-compose.yml << 'GATUS_COMPOSE'
name: gatus
services:
gatus:
image: twinproduction/gatus:latest
container_name: gatus
hostname: gatus
restart: unless-stopped
env_file: .env
ports:
- "8086:8080"
volumes:
- ./gatus_config:/config
- ./gatus_data:/data
networks:
- caddy_net
networks:
caddy_net:
external: true
name: ${CADDY_NET:-caddy_net}
GATUS_COMPOSE
cat > .env << GATUS_ENV
TZ=$TZ_VAL
CADDY_NET=$SITE_CADDY_NET
GATUS_ENV
# Write a sample config if none exists
if [ ! -f gatus_config/config.yaml ]; then
cat > gatus_config/config.yaml << 'GATUS_CFG'
# Gatus configuration — docs: https://github.com/TwiN/gatus
#
# Add or remove endpoints below. Config is hot-reloaded on changes.
# Alert types: ntfy, slack, discord, email, telegram, and more.
storage:
type: sqlite
path: /data/gatus.db
ui:
title: "Status"
header: "Services"
# ── Endpoints ─────────────────────────────────────────────────────────────────
endpoints:
- name: Google DNS
group: external
url: "8.8.8.8"
dns:
query-name: "google.com"
query-type: "A"
interval: 5m
conditions:
- "[DNS_RCODE] == NOERROR"
- name: Example HTTPS
group: external
url: "https://example.com"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 3000"
- "[CERTIFICATE_EXPIRATION] > 48h"
# ── Add your services below ────────────────────────────────────────────────
# - name: Mealie
# group: homelab
# url: "http://mealie:9000/api/app/about"
# interval: 1m
# conditions:
# - "[STATUS] == 200"
# - "[RESPONSE_TIME] < 500"
#
# - name: Portainer
# group: homelab
# url: "https://portainer:9443"
# interval: 1m
# conditions:
# - "[STATUS] == 200"
# client:
# insecure: true
GATUS_CFG
fi
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$GATUS_DIR"
log_success "Gatus configured at $GATUS_DIR"
configure_caddy_for_service "Gatus" "gatus:8080" "status"
write_readme "$GATUS_DIR" << MD
# Gatus — status & uptime monitoring
Clean, self-hosted status page. Polls HTTP, TCP, DNS, and ICMP endpoints.
## Access
- URL: http://localhost:8086
## Configuration
Edit \`gatus_config/config.yaml\` — changes are **hot-reloaded** without restarting.
Key concepts:
- \`endpoints:\` — what to check (HTTP, TCP, DNS, ICMP)
- \`interval:\` — how often (e.g. 1m, 5m)
- \`conditions:\` — pass/fail rules ([STATUS], [RESPONSE_TIME], etc.)
- \`alerts:\` — notify via ntfy, Slack, Discord, email, etc.
Full docs: https://github.com/TwiN/gatus
## Manage
\`\`\`bash
cd $GATUS_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs (check config errors here)
docker compose pull && docker compose up -d # update
\`\`\`
MD
local START_GATUS=""
prompt_yn "Start Gatus now? (y/n):" "y" START_GATUS
if [ "$START_GATUS" = "y" ] || [ "$START_GATUS" = "Y" ]; then
docker compose up -d \
&& log_success "Gatus started" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo " Access at: http://localhost:8086"
echo " Config: $GATUS_DIR/gatus_config/config.yaml (hot-reloaded)"
echo ""
}
+160
View File
@@ -0,0 +1,160 @@
#!/bin/bash
# services/mail-archiver.sh — Mail Archiver (IMAP email archive & search).
# Part of the modular post-install system (sourced by setup.sh).
#
# Self-hosted email archive — connects to IMAP accounts, indexes messages,
# and provides full-text search. No big-tech email required.
# Image: s1t5/mailarchiver DB: postgres:17-alpine
register_service mail-archiver utilities "IMAP email archive & search (Mail Archiver)" 5000
install_mail-archiver() {
require_docker || return 1
log_info "Installing Mail Archiver..."
local MA_DIR="$DOCKER_DIR/mail-archiver"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $MA_DIR (mailarchiver_database/)"
echo "[DRY-RUN] Would deploy s1t5/mailarchiver:latest + postgres:17-alpine"
echo "[DRY-RUN] Accessed via Caddy reverse proxy (no direct host port)"
echo "[DRY-RUN] Would generate DB and admin passwords"
return 0
fi
mkdir -p "$MA_DIR/mailarchiver_database"
ensure_docker_dir_ownership "$MA_DIR"
cd "$MA_DIR" || return 1
local DB_PASS ADMIN_PASS TZ_VAL
DB_PASS=$(generate_password 32)
ADMIN_PASS=$(generate_password 24)
TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
cat > docker-compose.yml << 'MA_COMPOSE'
name: mail-archiver
services:
mailarchiver-app:
image: s1t5/mailarchiver:latest
container_name: mailarchiver-app
hostname: mailarchiver-app
restart: unless-stopped
env_file: .env
expose:
- "5000"
depends_on:
mailarchiver-db:
condition: service_healthy
networks:
- caddy_net
mailarchiver-db:
image: postgres:17-alpine
container_name: mailarchiver-db
hostname: mailarchiver-db
restart: unless-stopped
env_file: .env
expose:
- "5432"
volumes:
- ./mailarchiver_database:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mailuser -d MailArchiver"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
networks:
caddy_net:
external: true
name: ${CADDY_NET:-caddy_net}
MA_COMPOSE
cat > .env << MA_ENV
# ── General ───────────────────────────────────────────────────────────────────
TZ=$TZ_VAL
CADDY_NET=$SITE_CADDY_NET
# ── Database connection (app → postgres) ──────────────────────────────────────
ConnectionStrings__DefaultConnection=Host=mailarchiver-db;Database=MailArchiver;Username=mailuser;Password=$DB_PASS;
# ── Web authentication ────────────────────────────────────────────────────────
Authentication__Enabled=true
Authentication__Username=admin
Authentication__Password=$ADMIN_PASS
Authentication__SessionTimeoutMinutes=60
Authentication__CookieName=MailArchiverAuth
# ── Mail sync schedule ────────────────────────────────────────────────────────
MailSync__IntervalMinutes=15
MailSync__TimeoutMinutes=60
MailSync__ConnectionTimeoutSeconds=180
MailSync__CommandTimeoutSeconds=300
# ── Batch restore limits ──────────────────────────────────────────────────────
BatchRestore__AsyncThreshold=50
BatchRestore__MaxSyncEmails=150
BatchRestore__MaxAsyncEmails=50000
BatchRestore__SessionTimeoutMinutes=30
BatchRestore__DefaultBatchSize=50
# ── Postgres tuning ───────────────────────────────────────────────────────────
Npgsql__CommandTimeout=600
# ── Postgres container ────────────────────────────────────────────────────────
POSTGRES_DB=MailArchiver
POSTGRES_USER=mailuser
POSTGRES_PASSWORD=$DB_PASS
MA_ENV
chmod 600 .env
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MA_DIR"
log_success "Mail Archiver configured at $MA_DIR"
configure_caddy_for_service "Mail Archiver" "mailarchiver-app:5000" "mail"
write_readme "$MA_DIR" << MD
# Mail Archiver
Self-hosted IMAP email archive and full-text search.
Add your IMAP mail accounts through the web UI — Mail Archiver will pull
and index all messages, then let you search the full archive.
## Access
- URL: via Caddy reverse proxy (no direct host port)
- Login: admin / (see .env Authentication__Password)
## Adding mail accounts
1. Open the web UI → Settings → Mail Accounts
2. Add IMAP server, username, and password
3. Mail Archiver syncs every \`MailSync__IntervalMinutes\` minutes (default: 15)
## Credentials
Stored in \`.env\` (chmod 600):
- Web admin password: \`Authentication__Password\`
- DB password: \`POSTGRES_PASSWORD\`
## Manage
\`\`\`bash
cd $MA_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
MD
local START_MA=""
prompt_yn "Start Mail Archiver now? (y/n):" "y" START_MA
if [ "$START_MA" = "y" ] || [ "$START_MA" = "Y" ]; then
docker compose up -d \
&& log_success "Mail Archiver started" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Admin login: admin / $(grep Authentication__Password .env | cut -d= -f2)"
echo " Add IMAP accounts via the web UI after starting."
echo ""
}
+161
View File
@@ -0,0 +1,161 @@
#!/bin/bash
# services/rustdesk.sh — RustDesk self-hosted remote desktop relay server.
# Part of the modular post-install system (sourced by setup.sh).
#
# RustDesk is an open-source TeamViewer alternative. This installs the
# SERVER-SIDE relay/rendezvous daemon — clients still need the RustDesk app.
# For cross-VLAN / cross-internet access, point RELAY at this server's FQDN.
#
# Ports that must reach this host (firewall/router):
# 21115 TCP — NAT type test
# 21116 TCP — ID register / heartbeat / relay rendezvous
# 21116 UDP — UDP hole-punching
# 21117 TCP — relay traffic (the "HBBR" relay daemon)
# 21118 TCP — WebSocket (browser client support)
# 21119 TCP — WebSocket HTTPS (browser client support)
register_service rustdesk utilities "Self-hosted remote desktop relay (RustDesk)" 21117
install_rustdesk() {
require_docker || return 1
log_info "Installing RustDesk server..."
local RD_DIR="$DOCKER_DIR/rustdesk"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $RD_DIR (rustdesk_data/)"
echo "[DRY-RUN] Would deploy rustdesk/rustdesk-server-s6:latest"
echo "[DRY-RUN] Ports: 21115-21119 TCP, 21116 UDP"
echo "[DRY-RUN] Would prompt for server FQDN/IP (RELAY env var)"
return 0
fi
mkdir -p "$RD_DIR/rustdesk_data"
ensure_docker_dir_ownership "$RD_DIR"
cd "$RD_DIR" || return 1
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
echo ""
echo " RustDesk needs to know its own public hostname or IP."
echo " Clients will connect to this address for relay traffic."
echo " Use a FQDN if you have one (e.g. rustdesk.example.com),"
echo " or your server's public IP if not."
echo ""
local RELAY_HOST=""
prompt_text "Public hostname or IP for this server:" "" RELAY_HOST
if [ -z "$RELAY_HOST" ]; then
log_warning "No relay host set — you MUST edit RELAY in .env before clients will work."
RELAY_HOST="your-server-fqdn-or-ip"
fi
local ENCRYPTED_ONLY="1"
local _enc=""
prompt_yn "Require encrypted connections only? (recommended) (y/n):" "y" _enc
[ "$_enc" = "n" ] || [ "$_enc" = "N" ] && ENCRYPTED_ONLY="0"
cat > docker-compose.yml << 'RD_COMPOSE'
name: rustdesk
services:
rustdesk:
image: rustdesk/rustdesk-server-s6:latest
container_name: rustdesk
hostname: rustdesk
restart: unless-stopped
env_file: .env
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
- "21117:21117"
- "21118:21118"
- "21119:21119"
volumes:
- ./rustdesk_data:/data
RD_COMPOSE
cat > .env << RD_ENV
# ── General ───────────────────────────────────────────────────────────────────
TZ=$TZ_VAL
# ── RustDesk server ───────────────────────────────────────────────────────────
# RELAY: public FQDN or IP that clients use to reach the relay daemon (HBBR).
# Include the port if it's non-standard: hostname:21117
RELAY=$RELAY_HOST:21117
# ENCRYPTED_ONLY: 1 = only clients with the matching public key can connect.
# After first startup, copy the key from ./rustdesk_data/id_ed25519.pub to
# each client: Settings → Network → Key.
ENCRYPTED_ONLY=$ENCRYPTED_ONLY
# KEY_PRIV and KEY_PUB — optional: paste key file contents here instead of
# relying on the volume-mounted file. Useful for portability.
# KEY_PRIV=<content of ./rustdesk_data/id_ed25519>
# KEY_PUB=<content of ./rustdesk_data/id_ed25519.pub>
RD_ENV
chmod 600 .env
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$RD_DIR"
log_success "RustDesk configured at $RD_DIR"
write_readme "$RD_DIR" << MD
# RustDesk — self-hosted remote desktop relay
Open-source TeamViewer alternative. This is the server-side relay/rendezvous
daemon. Clients use the RustDesk desktop/mobile app to connect.
## After starting: get the public key
\`\`\`bash
cat $RD_DIR/rustdesk_data/id_ed25519.pub
\`\`\`
Paste this key into each client:
**Settings → Network → ID/Relay Server**
- ID Server: $RELAY_HOST
- Relay Server: $RELAY_HOST
- Key: <paste id_ed25519.pub contents>
## Firewall / router rules required
Open these ports to this server's IP:
| Port | Protocol | Purpose |
|------|----------|---------|
| 21115 | TCP | NAT type test |
| 21116 | TCP+UDP | ID register / hole-punching |
| 21117 | TCP | Relay traffic |
| 21118 | TCP | WebSocket |
| 21119 | TCP | WebSocket HTTPS |
## Cross-VLAN setup
Use the server's FQDN (not LAN IP) in RELAY so clients on any VLAN
or on the internet can reach the relay. DNS must resolve the FQDN to
the server's public IP.
## Manage
\`\`\`bash
cd $RD_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
MD
local START_RD=""
prompt_yn "Start RustDesk server now? (y/n):" "y" START_RD
if [ "$START_RD" = "y" ] || [ "$START_RD" = "Y" ]; then
docker compose up -d \
&& log_success "RustDesk started" \
|| log_warning "Failed to start — check: docker compose logs"
echo ""
echo " After startup, get the public key:"
echo " cat $RD_DIR/rustdesk_data/id_ed25519.pub"
echo " Paste it into client Settings → Network → Key."
fi
echo ""
echo " Relay host: $RELAY_HOST"
echo " Ports 21115-21119 must be open in your firewall/router."
echo ""
}
+212
View File
@@ -0,0 +1,212 @@
#!/bin/bash
# services/unifi.sh — UniFi Network Application (Ubiquiti controller).
# Part of the modular post-install system (sourced by setup.sh).
#
# Two containers: mongo:4 (DB) + linuxserver unifi-network-application (app).
# Web UI runs on HTTPS port 8443 — no plain HTTP web interface.
# Caddy reverse-proxy wiring uses TLS passthrough or tls_insecure_skip_verify.
register_service unifi utilities "Ubiquiti network controller (UniFi)" 8443
install_unifi() {
require_docker || return 1
log_info "Installing UniFi Network Application..."
local UNIFI_DIR="$DOCKER_DIR/unifi"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $UNIFI_DIR (mongo_db_data/, unifi_data/)"
echo "[DRY-RUN] Would deploy mongo:4 + linuxserver/unifi-network-application:latest"
echo "[DRY-RUN] Ports: 8443 (HTTPS web UI), 8080 (device inform), 3478/udp (STUN), 10001/udp (discovery)"
echo "[DRY-RUN] Would generate MongoDB credentials"
return 0
fi
mkdir -p "$UNIFI_DIR"
ensure_docker_dir_ownership "$UNIFI_DIR"
cd "$UNIFI_DIR" || return 1
local MONGO_PASS TZ_VAL UID_VAL GID_VAL
MONGO_PASS=$(generate_password 24)
TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
UID_VAL=$(id -u "$ACTUAL_USER")
GID_VAL=$(id -g "$ACTUAL_USER")
# Single-quoted heredoc: ${...} left literal for Docker Compose to expand from .env
cat > docker-compose.yml << 'UNIFI_COMPOSE'
name: unifi
services:
unifi-db:
image: mongo:4
container_name: unifi-db
hostname: unifi-db
restart: unless-stopped
env_file: .env
volumes:
- ./mongo_db_data:/data/db
expose:
- "27017"
configs:
- source: init-mongo.js
target: /docker-entrypoint-initdb.d/init-mongo.js
unifi-app:
image: lscr.io/linuxserver/unifi-network-application:latest
container_name: unifi-app
hostname: unifi-app
restart: unless-stopped
env_file: .env
depends_on:
- unifi-db
volumes:
- ./unifi_data:/config
ports:
- "8443:8443"
- "8080:8080"
- "3478:3478/udp"
- "10001:10001/udp"
# Optional — uncomment as needed:
# - "1900:1900/udp" # L2 discovery (may conflict with UPnP)
# - "8843:8843" # guest portal HTTPS
# - "8880:8880" # guest portal HTTP
# - "6789:6789" # mobile speed test
# - "5514:5514/udp" # remote syslog
networks:
- caddy_net
networks:
caddy_net:
external: true
name: ${CADDY_NET:-caddy_net}
# Inline MongoDB init — Docker Compose interpolates vars from .env at startup.
configs:
init-mongo.js:
content: |
db.getSiblingDB("${MONGO_DBNAME}").createUser({user: "${MONGO_USER}", pwd: "${MONGO_PASS}", roles: [{role: "${MONGO_ROLE}", db: "${MONGO_DBNAME}"}]});
db.getSiblingDB("${MONGO_DBNAME}_stat").createUser({user: "${MONGO_USER}", pwd: "${MONGO_PASS}", roles: [{role: "${MONGO_ROLE}", db: "${MONGO_DBNAME}_stat"}]});
UNIFI_COMPOSE
cat > .env << UNIFI_ENV
# ── General ───────────────────────────────────────────────────────────────────
TZ=$TZ_VAL
CADDY_NET=$SITE_CADDY_NET
# ── LinuxServer — UniFi app ───────────────────────────────────────────────────
PUID=$UID_VAL
PGID=$GID_VAL
MEM_LIMIT=1024
MEM_STARTUP=512
# ── MongoDB connection ────────────────────────────────────────────────────────
MONGO_USER=unifi
MONGO_PASS=$MONGO_PASS
MONGO_HOST=unifi-db
MONGO_PORT=27017
MONGO_DBNAME=unifi_db
MONGO_ROLE=dbOwner
# MONGO_TLS= # optional
# MONGO_AUTHSOURCE= # optional
UNIFI_ENV
chmod 600 .env
mkdir -p mongo_db_data unifi_data
ensure_docker_dir_ownership "$UNIFI_DIR"
log_success "UniFi configured at $UNIFI_DIR"
# ── Optional Caddy reverse proxy (HTTPS backend requires special config) ──
if [ -d "$DOCKER_DIR/caddy" ]; then
echo ""
echo " UniFi web UI is HTTPS-only (self-signed cert internally)."
echo " Caddy can proxy it, but requires tls_insecure_skip_verify."
echo ""
local CADDY_UNIFI=""
prompt_yn "Configure Caddy reverse proxy for UniFi? (y/n):" "n" CADDY_UNIFI
if [ "$CADDY_UNIFI" = "y" ] || [ "$CADDY_UNIFI" = "Y" ]; then
local UNIFI_DOMAIN=""
prompt_text "UniFi domain (e.g. unifi.example.com):" "unifi.${SITE_DOMAIN:-example.com}" UNIFI_DOMAIN
if [ -n "$UNIFI_DOMAIN" ]; then
local CADDYFILE="$DOCKER_DIR/caddy/Caddyfile"
cp "$CADDYFILE" "$CADDYFILE.backup.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
cat >> "$CADDYFILE" << CADDY_BLOCK
# UniFi Network Application
$UNIFI_DOMAIN {
reverse_proxy https://unifi-app:8443 {
transport http {
tls_insecure_skip_verify
}
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/$UNIFI_DOMAIN.log
format json
}
}
CADDY_BLOCK
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null \
&& log_success "Caddy configured for $UNIFI_DOMAIN" \
|| log_warning "Caddy reload failed — check: docker logs caddy"
fi
fi
fi
write_readme "$UNIFI_DIR" << MD
# UniFi Network Application
Ubiquiti network controller. Manages UniFi APs, switches, and gateways.
## Access
- Web UI: **https://localhost:8443** (HTTPS, self-signed cert — accept the warning)
- First run: complete the setup wizard and adopt your devices.
## Device adoption
Make sure devices can reach **http://<server-ip>:8080/inform** as the inform URL.
In the controller: Settings → System → Application Configuration → Override inform host.
## Ports
| Port | Protocol | Purpose |
|------|----------|---------|
| 8443 | TCP | HTTPS web UI |
| 8080 | TCP | Device inform / HTTP redirect |
| 3478 | UDP | STUN |
| 10001 | UDP | AP discovery |
## Manage
\`\`\`bash
cd $UNIFI_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update (wait for DB first)
\`\`\`
## Migration from old UniFi Controller
1. Backup: Settings → System → Backup → Create Backup
2. Down the old container
3. Spin up this stack
4. Restore: Settings → System → Backup → Restore
MD
local START_UNIFI=""
prompt_yn "Start UniFi now? (y/n):" "y" START_UNIFI
if [ "$START_UNIFI" = "y" ] || [ "$START_UNIFI" = "Y" ]; then
docker compose up -d \
&& log_success "UniFi started (first startup takes ~60 s while DB initializes)" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Web UI: https://localhost:8443 (accept the self-signed cert warning)"
echo " MongoDB credentials saved to: $UNIFI_DIR/.env"
echo ""
}
+165
View File
@@ -0,0 +1,165 @@
#!/bin/bash
# services/vaultwarden.sh — Vaultwarden (self-hosted Bitwarden server).
# Part of the modular post-install system (sourced by setup.sh).
#
# Vaultwarden is an unofficial, lightweight Bitwarden-compatible server.
# All official Bitwarden clients (browser extension, desktop, mobile) work with it.
# Requires HTTPS in production — set DOMAIN to your public URL.
register_service vaultwarden utilities "Bitwarden-compatible password manager (Vaultwarden)" 80
install_vaultwarden() {
require_docker || return 1
log_info "Installing Vaultwarden..."
local VW_DIR="$DOCKER_DIR/vaultwarden"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $VW_DIR (vaultwarden_data/)"
echo "[DRY-RUN] Would deploy vaultwarden/server:latest"
echo "[DRY-RUN] Would generate admin token and prompt for domain"
echo "[DRY-RUN] Signups disabled by default (enable via admin panel)"
return 0
fi
mkdir -p "$VW_DIR/vaultwarden_data"
ensure_docker_dir_ownership "$VW_DIR"
cd "$VW_DIR" || return 1
local ADMIN_TOKEN TZ_VAL
ADMIN_TOKEN=$(generate_password 48)
TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
echo ""
echo " Vaultwarden needs to know its public HTTPS URL so Bitwarden clients"
echo " can connect and password-reset emails link correctly."
echo ""
local VW_DOMAIN=""
local DEFAULT_DOMAIN="https://vault.${SITE_DOMAIN:-example.com}"
prompt_text "Vaultwarden public URL (e.g. https://vault.example.com):" "$DEFAULT_DOMAIN" VW_DOMAIN
[ -z "$VW_DOMAIN" ] && VW_DOMAIN="$DEFAULT_DOMAIN"
echo ""
echo " SMTP (optional) — for password-reset and invite emails."
echo " Press Enter to skip each field and configure SMTP later in .env."
echo ""
local SMTP_HOST="" SMTP_FROM="" SMTP_USER="" SMTP_PASS="" SMTP_PORT="587"
prompt_text "SMTP host (e.g. smtp.gmail.com) [skip]:" "" SMTP_HOST
if [ -n "$SMTP_HOST" ]; then
prompt_text "SMTP port [587]:" "587" SMTP_PORT
prompt_text "SMTP from address:" "" SMTP_FROM
prompt_text "SMTP username:" "" SMTP_USER
prompt_text "SMTP password:" "" SMTP_PASS
fi
cat > docker-compose.yml << 'VW_COMPOSE'
name: vaultwarden
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
hostname: vaultwarden
restart: unless-stopped
env_file: .env
volumes:
- ./vaultwarden_data:/data
expose:
- "80"
ports:
- "3012:3012" # WebSocket (legacy — not needed for Vaultwarden v1.29+)
networks:
- caddy_net
networks:
caddy_net:
external: true
name: ${CADDY_NET:-caddy_net}
VW_COMPOSE
cat > .env << VW_ENV
# ── General ───────────────────────────────────────────────────────────────────
TZ=$TZ_VAL
CADDY_NET=$SITE_CADDY_NET
# ── Vaultwarden ───────────────────────────────────────────────────────────────
# Public URL — MUST match the URL clients use (affects TOTP, push, reset emails)
DOMAIN=$VW_DOMAIN
# Admin panel: https://<domain>/admin — keep this token secret
# To disable admin panel: delete ADMIN_TOKEN from this file
ADMIN_TOKEN=$ADMIN_TOKEN
# Signups: false = only the first admin can invite users via admin panel
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=false
# WebSocket notifications (v1.29+: built into port 80, no separate port needed)
WEBSOCKET_ENABLED=true
# ── SMTP (optional — for password-reset and invite emails) ────────────────────
SMTP_HOST=$SMTP_HOST
SMTP_PORT=$SMTP_PORT
SMTP_SECURITY=starttls
SMTP_FROM=$SMTP_FROM
SMTP_USERNAME=$SMTP_USER
SMTP_PASSWORD=$SMTP_PASS
VW_ENV
chmod 600 .env
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$VW_DIR"
log_success "Vaultwarden configured at $VW_DIR"
configure_caddy_for_service "Vaultwarden" "vaultwarden:80" "vault"
write_readme "$VW_DIR" << MD
# Vaultwarden — Bitwarden-compatible password manager
Lightweight, self-hosted Bitwarden server. Works with all official
Bitwarden clients: browser extension, desktop app, and mobile app.
## Setup
1. Point your Bitwarden client to: $VW_DOMAIN
2. Create the first account (signups are off after the first user — use admin panel)
3. Admin panel: **$VW_DOMAIN/admin** (use ADMIN_TOKEN from .env)
## Admin panel
The admin panel lets you manage users, send invites, and configure settings.
URL: \`$VW_DOMAIN/admin\`
Token: see \`ADMIN_TOKEN\` in .env
**Security:** remove or rotate ADMIN_TOKEN after initial setup if you don't
need ongoing admin access.
## Inviting users (signups disabled)
Admin panel → Users → Invite User → enter email.
Requires SMTP to be configured for the invite email to arrive.
## Credentials
- Admin token: stored in .env (chmod 600)
- User vaults: encrypted in vaultwarden_data/
## Manage
\`\`\`bash
cd $VW_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
MD
local START_VW=""
prompt_yn "Start Vaultwarden now? (y/n):" "y" START_VW
if [ "$START_VW" = "y" ] || [ "$START_VW" = "Y" ]; then
docker compose up -d \
&& log_success "Vaultwarden started" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Domain: $VW_DOMAIN"
echo " Admin panel: $VW_DOMAIN/admin"
echo " Admin token: $ADMIN_TOKEN"
echo " (Token also saved to $VW_DIR/.env)"
echo ""
}
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# services/watchyourlan.sh — WatchYourLAN network device tracker.
# Part of the modular post-install system (sourced by setup.sh).
#
# Continuously scans the network for connected devices, tracks history,
# and can alert on new/unknown devices. Uses network_mode: host so it
# can see the physical network directly (required for ARP scanning).
register_service watchyourlan utilities "Network device tracker (WatchYourLAN)" 8840
install_watchyourlan() {
require_docker || return 1
log_info "Installing WatchYourLAN..."
local WYL_DIR="$DOCKER_DIR/watchyourlan"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $WYL_DIR (watchyourlan_data/)"
echo "[DRY-RUN] Would deploy aceberg/watchyourlan:latest (network_mode: host)"
echo "[DRY-RUN] Port 8840 on host, needs network interface name"
return 0
fi
mkdir -p "$WYL_DIR/watchyourlan_data"
ensure_docker_dir_ownership "$WYL_DIR"
cd "$WYL_DIR" || return 1
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
# Auto-detect primary network interface
local DEFAULT_IFACE
DEFAULT_IFACE=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
[ -z "$DEFAULT_IFACE" ] && DEFAULT_IFACE="eth0"
echo ""
echo " WatchYourLAN needs to know which network interface to scan."
echo " Your detected primary interface: $DEFAULT_IFACE"
echo ""
ip link show 2>/dev/null | awk -F: '/^[0-9]+: / && !/lo/ {gsub(/ /,"",$2); print " •", $2}' || true
echo ""
local SCAN_IFACE=""
prompt_text "Network interface to scan:" "$DEFAULT_IFACE" SCAN_IFACE
[ -z "$SCAN_IFACE" ] && SCAN_IFACE="$DEFAULT_IFACE"
local GUI_PORT="8840"
prompt_text "GUI port [8840]:" "8840" GUI_PORT
[ -z "$GUI_PORT" ] && GUI_PORT="8840"
cat > docker-compose.yml << 'WYL_COMPOSE'
name: watchyourlan
services:
watchyourlan:
image: aceberg/watchyourlan:latest
container_name: watchyourlan
hostname: watchyourlan
restart: unless-stopped
network_mode: host
env_file: .env
volumes:
- ./watchyourlan_data:/data
WYL_COMPOSE
cat > .env << WYL_ENV
# ── General ───────────────────────────────────────────────────────────────────
TZ=$TZ_VAL
# ── WatchYourLAN ──────────────────────────────────────────────────────────────
# Network interface to scan (ARP scanning requires the physical interface)
IFACE=$SCAN_IFACE
# GUI bind address and port (network_mode: host — binds directly to the host)
GUIIP=0.0.0.0
GUIPORT=$GUI_PORT
# Web UI theme (darkly, cosmo, lumen, sandstone, etc.)
THEME=darkly
WYL_ENV
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WYL_DIR"
log_success "WatchYourLAN configured at $WYL_DIR"
# WatchYourLAN uses network_mode: host, so Caddy container-name routing
# can't reach it via caddy_net. Access is directly on host port $GUI_PORT.
# If behind Caddy on the same host, configure manually with host IP:PORT.
if [ -d "$DOCKER_DIR/caddy" ]; then
echo ""
log_info "Note: WatchYourLAN uses host networking (needed for ARP scanning)."
log_info "It cannot join caddy_net. To put it behind Caddy, add this block manually:"
echo ""
echo " yourdomain.com {"
echo " reverse_proxy <HOST_IP>:$GUI_PORT"
echo " }"
echo ""
echo " where HOST_IP is this server's IP on the Docker bridge (usually 172.17.0.1)."
fi
write_readme "$WYL_DIR" << MD
# WatchYourLAN — network device tracker
Scans the network continuously for connected devices, tracks history,
and alerts on new or unknown devices joining the network.
## Access
- URL: http://localhost:$GUI_PORT (or http://<server-ip>:$GUI_PORT from LAN)
## Scanning interface
Configured to scan: **$SCAN_IFACE**
Change \`IFACE\` in .env and restart if you need to scan a different interface.
## Network mode note
WatchYourLAN uses \`network_mode: host\` to see real ARP traffic.
This means it cannot be added to caddy_net for reverse proxy via container name.
To put it behind Caddy, use the host's IP directly in the Caddyfile:
\`reverse_proxy 172.17.0.1:$GUI_PORT\` (adjust IP to your Docker bridge gateway).
## Manage
\`\`\`bash
cd $WYL_DIR
docker compose up -d # start
docker compose down # stop
docker compose logs -f # logs
docker compose pull && docker compose up -d # update
\`\`\`
MD
local START_WYL=""
prompt_yn "Start WatchYourLAN now? (y/n):" "y" START_WYL
if [ "$START_WYL" = "y" ] || [ "$START_WYL" = "Y" ]; then
docker compose up -d \
&& log_success "WatchYourLAN started" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo " Access at: http://localhost:$GUI_PORT"
echo " Scanning: interface $SCAN_IFACE"
echo ""
}