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:
@@ -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` |
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user