Merge pull request #56 from outis1one/claude/clever-mayer-FvxF6
Claude/clever mayer fvx f6
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) |
|
| `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` |
|
| `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` |
|
| `media` | `arm`, `audiobookshelf`, `emby`, `immich`, `jellyfin`, `lyrion` |
|
||||||
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
|
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
|
||||||
| `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` |
|
| `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` |
|
||||||
|
|||||||
+15
-4
@@ -261,9 +261,20 @@ write_readme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
|
# ── Caddy reverse-proxy wiring (shared by every web service) ─────────────────
|
||||||
# Usage: configure_caddy_for_service "Name" "PORT" "default-subdomain" ["extra"]
|
# Usage: configure_caddy_for_service "Name" "UPSTREAM" "default-subdomain" ["extra"]
|
||||||
|
# UPSTREAM: container:port for caddy_net routing (e.g. "filebrowser:80"),
|
||||||
|
# or plain port number for localhost fallback (e.g. "8085").
|
||||||
configure_caddy_for_service() {
|
configure_caddy_for_service() {
|
||||||
local SERVICE_NAME="$1" SERVICE_PORT="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}"
|
local SERVICE_NAME="$1" SERVICE_UPSTREAM="$2" DEFAULT_SUBDOMAIN="$3" EXTRA_CONFIG="${4:-}"
|
||||||
|
|
||||||
|
# Derive the proxy upstream and a port number for display messages.
|
||||||
|
# Plain number → localhost:PORT (host-network or legacy services)
|
||||||
|
# name:port → used as-is (preferred: service on shared caddy_net)
|
||||||
|
local _UPSTREAM _DISPLAY_PORT
|
||||||
|
case "$SERVICE_UPSTREAM" in
|
||||||
|
*:*) _UPSTREAM="$SERVICE_UPSTREAM"; _DISPLAY_PORT="${SERVICE_UPSTREAM##*:}" ;;
|
||||||
|
*) _UPSTREAM="localhost:$SERVICE_UPSTREAM"; _DISPLAY_PORT="$SERVICE_UPSTREAM" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Caddy not installed → nothing to do
|
# Caddy not installed → nothing to do
|
||||||
[ -d "$DOCKER_DIR/caddy" ] || return 0
|
[ -d "$DOCKER_DIR/caddy" ] || return 0
|
||||||
@@ -280,7 +291,7 @@ configure_caddy_for_service() {
|
|||||||
prompt_yn "Configure Caddy reverse proxy for $SERVICE_NAME? (y/n):" "n" CONFIGURE_CADDY
|
prompt_yn "Configure Caddy reverse proxy for $SERVICE_NAME? (y/n):" "n" CONFIGURE_CADDY
|
||||||
if [ "$CONFIGURE_CADDY" != "y" ] && [ "$CONFIGURE_CADDY" != "Y" ]; then
|
if [ "$CONFIGURE_CADDY" != "y" ] && [ "$CONFIGURE_CADDY" != "Y" ]; then
|
||||||
echo " Skipping Caddy configuration."
|
echo " Skipping Caddy configuration."
|
||||||
echo " Access $SERVICE_NAME at: http://localhost:$SERVICE_PORT"
|
echo " Access $SERVICE_NAME at: http://localhost:$_DISPLAY_PORT"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -320,7 +331,7 @@ configure_caddy_for_service() {
|
|||||||
|
|
||||||
# $SERVICE_NAME
|
# $SERVICE_NAME
|
||||||
$SERVICE_DOMAIN {
|
$SERVICE_DOMAIN {
|
||||||
reverse_proxy localhost:$SERVICE_PORT
|
reverse_proxy $_UPSTREAM
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
header {
|
header {
|
||||||
|
|||||||
@@ -40,16 +40,24 @@ services:
|
|||||||
- ./data:/data
|
- ./data:/data
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
AB_COMPOSE
|
AB_COMPOSE
|
||||||
|
|
||||||
cat > .env << AB_ENV
|
cat > .env << AB_ENV
|
||||||
TZ=$TZ_VAL
|
TZ=$TZ_VAL
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
AB_ENV
|
AB_ENV
|
||||||
|
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
|
||||||
log_success "Actual Budget configured at $AB_DIR"
|
log_success "Actual Budget configured at $AB_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "ActualBudget" "5006" "budget"
|
configure_caddy_for_service "ActualBudget" "actualbudget:5006" "budget"
|
||||||
|
|
||||||
write_readme "$AB_DIR" << MD
|
write_readme "$AB_DIR" << MD
|
||||||
# Actual Budget
|
# Actual Budget
|
||||||
|
|||||||
+9
-1
@@ -73,10 +73,18 @@ services:
|
|||||||
# Add more optical drives as needed:
|
# Add more optical drives as needed:
|
||||||
# - /dev/sr1:/dev/sr1
|
# - /dev/sr1:/dev/sr1
|
||||||
privileged: true
|
privileged: true
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
ARM_COMPOSE
|
ARM_COMPOSE
|
||||||
|
|
||||||
cat > .env << ARM_ENV
|
cat > .env << ARM_ENV
|
||||||
ARM_OUTPUT=$ARM_OUTPUT
|
ARM_OUTPUT=$ARM_OUTPUT
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
ARM_ENV
|
ARM_ENV
|
||||||
|
|
||||||
mkdir -p config logs
|
mkdir -p config logs
|
||||||
@@ -85,7 +93,7 @@ ARM_ENV
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ARM_OUTPUT"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ARM_OUTPUT"
|
||||||
log_success "A.R.M. configured at $ARM_DIR"
|
log_success "A.R.M. configured at $ARM_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "A.R.M." "8080" "arm"
|
configure_caddy_for_service "A.R.M." "arm:8080" "arm"
|
||||||
|
|
||||||
write_readme "$ARM_DIR" << MD
|
write_readme "$ARM_DIR" << MD
|
||||||
# A.R.M. (Automatic Ripping Machine)
|
# A.R.M. (Automatic Ripping Machine)
|
||||||
|
|||||||
@@ -50,18 +50,26 @@ services:
|
|||||||
- \${PODCASTS_PATH:-./podcasts}:/podcasts
|
- \${PODCASTS_PATH:-./podcasts}:/podcasts
|
||||||
ports:
|
ports:
|
||||||
- "13378:80"
|
- "13378:80"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
ABS_COMPOSE
|
ABS_COMPOSE
|
||||||
|
|
||||||
cat > .env << ABS_ENV
|
cat > .env << ABS_ENV
|
||||||
AUDIOBOOKS_PATH=$AUDIOBOOKS_PATH
|
AUDIOBOOKS_PATH=$AUDIOBOOKS_PATH
|
||||||
PODCASTS_PATH=./podcasts
|
PODCASTS_PATH=./podcasts
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
ABS_ENV
|
ABS_ENV
|
||||||
|
|
||||||
mkdir -p config metadata podcasts
|
mkdir -p config metadata podcasts
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ABS_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ABS_DIR"
|
||||||
log_success "Audiobookshelf configured at $ABS_DIR"
|
log_success "Audiobookshelf configured at $ABS_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "AudioBookshelf" "13378" "audiobooks"
|
configure_caddy_for_service "AudioBookshelf" "audiobookshelf:80" "audiobooks"
|
||||||
|
|
||||||
write_readme "$ABS_DIR" << MD
|
write_readme "$ABS_DIR" << MD
|
||||||
# Audiobookshelf
|
# Audiobookshelf
|
||||||
|
|||||||
+10
-3
@@ -82,6 +82,13 @@ services:
|
|||||||
- ACME_AGREE=true
|
- ACME_AGREE=true
|
||||||
labels:
|
labels:
|
||||||
- "io.podman.annotations.label/crowdsec.enable=true"
|
- "io.podman.annotations.label/crowdsec.enable=true"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
driver: bridge
|
||||||
|
name: caddy_net
|
||||||
CADDY_COMPOSE
|
CADDY_COMPOSE
|
||||||
|
|
||||||
# Create Caddyfile if it doesn't exist
|
# Create Caddyfile if it doesn't exist
|
||||||
@@ -114,7 +121,7 @@ CADDY_COMPOSE
|
|||||||
# Example:
|
# Example:
|
||||||
# myservice.yourdomain.com {
|
# myservice.yourdomain.com {
|
||||||
# import authelia
|
# import authelia
|
||||||
# reverse_proxy localhost:PORT
|
# reverse_proxy container_name:port
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# ActualBudget
|
# ActualBudget
|
||||||
@@ -124,7 +131,7 @@ CADDY_COMPOSE
|
|||||||
# format json
|
# format json
|
||||||
# level INFO
|
# level INFO
|
||||||
# }
|
# }
|
||||||
# reverse_proxy localhost:5006
|
# reverse_proxy actualbudget:5006
|
||||||
# header {
|
# header {
|
||||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
# Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
# X-Frame-Options "SAMEORIGIN"
|
# X-Frame-Options "SAMEORIGIN"
|
||||||
@@ -165,7 +172,7 @@ a web service). You can also edit it by hand:
|
|||||||
|
|
||||||
```
|
```
|
||||||
myservice.example.com {
|
myservice.example.com {
|
||||||
reverse_proxy localhost:1234
|
reverse_proxy container_name:1234
|
||||||
log {
|
log {
|
||||||
output file /var/log/caddy/myservice.example.com.log
|
output file /var/log/caddy/myservice.example.com.log
|
||||||
format json
|
format json
|
||||||
|
|||||||
+9
-1
@@ -59,17 +59,25 @@ services:
|
|||||||
# Uncomment for hardware transcoding (Intel/AMD):
|
# Uncomment for hardware transcoding (Intel/AMD):
|
||||||
# devices:
|
# devices:
|
||||||
# - /dev/dri:/dev/dri
|
# - /dev/dri:/dev/dri
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
EMBY_COMPOSE
|
EMBY_COMPOSE
|
||||||
|
|
||||||
cat > .env << EMBY_ENV
|
cat > .env << EMBY_ENV
|
||||||
MEDIA_PATH=$MEDIA_PATH
|
MEDIA_PATH=$MEDIA_PATH
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
EMBY_ENV
|
EMBY_ENV
|
||||||
|
|
||||||
mkdir -p config
|
mkdir -p config
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$EMBY_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$EMBY_DIR"
|
||||||
log_success "Emby configured at $EMBY_DIR"
|
log_success "Emby configured at $EMBY_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Emby" "8096" "emby"
|
configure_caddy_for_service "Emby" "emby:8096" "emby"
|
||||||
|
|
||||||
write_readme "$EMBY_DIR" << MD
|
write_readme "$EMBY_DIR" << MD
|
||||||
# Emby
|
# Emby
|
||||||
|
|||||||
+21
-3
@@ -26,13 +26,11 @@ name: filebrowser
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:s6
|
image: filebrowser/filebrowser:latest
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
hostname: filebrowser
|
hostname: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- PUID=$(id -u "$ACTUAL_USER")
|
|
||||||
- PGID=$(id -g "$ACTUAL_USER")
|
|
||||||
- TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}
|
- TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}
|
||||||
volumes:
|
volumes:
|
||||||
- ${FB_PATH}:/srv
|
- ${FB_PATH}:/srv
|
||||||
@@ -40,10 +38,18 @@ services:
|
|||||||
- ./config/settings.json:/config/settings.json
|
- ./config/settings.json:/config/settings.json
|
||||||
ports:
|
ports:
|
||||||
- "8085:80"
|
- "8085:80"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
FB_COMPOSE
|
FB_COMPOSE
|
||||||
|
|
||||||
cat > .env << FB_ENV
|
cat > .env << FB_ENV
|
||||||
FB_PATH=$FB_PATH
|
FB_PATH=$FB_PATH
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
FB_ENV
|
FB_ENV
|
||||||
|
|
||||||
mkdir -p database config
|
mkdir -p database config
|
||||||
@@ -61,9 +67,21 @@ FB_SETTINGS
|
|||||||
|
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR"
|
||||||
|
|
||||||
|
# Deploy user-management helper script
|
||||||
|
local _TOOLS_DIR
|
||||||
|
_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../tools" 2>/dev/null && pwd)" || true
|
||||||
|
if [ -f "$_TOOLS_DIR/manage_users.sh" ]; then
|
||||||
|
cp "$_TOOLS_DIR/manage_users.sh" "$FB_DIR/manage_users.sh"
|
||||||
|
chmod 750 "$FB_DIR/manage_users.sh"
|
||||||
|
chown "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR/manage_users.sh"
|
||||||
|
log_success "manage_users.sh installed at $FB_DIR/manage_users.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Filebrowser configured at $FB_DIR"
|
log_success "Filebrowser configured at $FB_DIR"
|
||||||
|
|
||||||
|
configure_caddy_for_service "FileBrowser" "filebrowser:80" "files"
|
||||||
|
|
||||||
write_readme "$FB_DIR" << MD
|
write_readme "$FB_DIR" << MD
|
||||||
# FileBrowser
|
# FileBrowser
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -44,17 +44,25 @@ services:
|
|||||||
- ./data:/fmd/data
|
- ./data:/fmd/data
|
||||||
ports:
|
ports:
|
||||||
- "8084:8080"
|
- "8084:8080"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
FMD_COMPOSE
|
FMD_COMPOSE
|
||||||
|
|
||||||
cat > .env << FMD_ENV
|
cat > .env << FMD_ENV
|
||||||
FMD_ADMIN_PASSWORD=$FMD_PASS
|
FMD_ADMIN_PASSWORD=$FMD_PASS
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
FMD_ENV
|
FMD_ENV
|
||||||
|
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR"
|
||||||
log_success "FindMyDevice configured at $FMD_DIR"
|
log_success "FindMyDevice configured at $FMD_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "FindMyDevice" "8084" "fmd"
|
configure_caddy_for_service "FindMyDevice" "fmd:8080" "fmd"
|
||||||
|
|
||||||
write_readme "$FMD_DIR" << MD
|
write_readme "$FMD_DIR" << MD
|
||||||
# FindMyDevice (FMD)
|
# FindMyDevice (FMD)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ FN_FRIGATE__MQTT__PASSWORD=${MQTT_PASS}
|
|||||||
FN_FRIGATE__SERVER=http://frigate:5000
|
FN_FRIGATE__SERVER=http://frigate:5000
|
||||||
FN_FRIGATE__PUBLIC_URL=${FRIGATE_PUBLIC_URL}
|
FN_FRIGATE__PUBLIC_URL=${FRIGATE_PUBLIC_URL}
|
||||||
FN_ALERTS__NTFY__SERVER=${NTFY_SERVER}
|
FN_ALERTS__NTFY__SERVER=${NTFY_SERVER}
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
ENVEOF
|
ENVEOF
|
||||||
chmod 600 "$DIR/.env"
|
chmod 600 "$DIR/.env"
|
||||||
log_success ".env written"
|
log_success ".env written"
|
||||||
@@ -225,6 +226,8 @@ COMPOSEEOF
|
|||||||
- "8554:8554"
|
- "8554:8554"
|
||||||
- "8555:8555/tcp"
|
- "8555:8555/tcp"
|
||||||
- "8555:8555/udp"
|
- "8555:8555/udp"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/api/version"]
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/api/version"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -257,6 +260,11 @@ COMPOSEEOF
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- ./frigate-notify/config.yml:/app/config.yml:ro
|
- ./frigate-notify/config.yml:/app/config.yml:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
COMPOSEEOF
|
COMPOSEEOF
|
||||||
log_success "docker-compose.yml written"
|
log_success "docker-compose.yml written"
|
||||||
|
|
||||||
@@ -506,7 +514,7 @@ FNEOF
|
|||||||
# ── Caddy snippet ──────────────────────────────────────────────────────────
|
# ── Caddy snippet ──────────────────────────────────────────────────────────
|
||||||
if [ -n "$FRIGATE_PUBLIC_URL" ] && [ "$FRIGATE_PUBLIC_URL" != "https://cam.yourdomain.com" ]; then
|
if [ -n "$FRIGATE_PUBLIC_URL" ] && [ "$FRIGATE_PUBLIC_URL" != "https://cam.yourdomain.com" ]; then
|
||||||
local _DOM="${FRIGATE_PUBLIC_URL#https://}"
|
local _DOM="${FRIGATE_PUBLIC_URL#https://}"
|
||||||
configure_caddy_for_service "Frigate" "8971" "frigate-audio" || true
|
configure_caddy_for_service "Frigate" "frigate-audio:8971" "frigate-audio" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_docker_dir_ownership "$DIR"
|
ensure_docker_dir_ownership "$DIR"
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yml:/app/config.yml:ro
|
- ./config.yml:/app/config.yml:ro
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
FN_COMPOSE
|
FN_COMPOSE
|
||||||
|
|
||||||
# Smart defaults based on what's installed
|
# Smart defaults based on what's installed
|
||||||
|
|||||||
+9
-1
@@ -71,10 +71,18 @@ $DEVICE_BLOCK
|
|||||||
- "8554:8554"
|
- "8554:8554"
|
||||||
- "8555:8555/tcp"
|
- "8555:8555/tcp"
|
||||||
- "8555:8555/udp"
|
- "8555:8555/udp"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
FRIGATE_COMPOSE
|
FRIGATE_COMPOSE
|
||||||
|
|
||||||
cat > .env << FRIGATE_ENV
|
cat > .env << FRIGATE_ENV
|
||||||
FRIGATE_MEDIA=$FRIGATE_MEDIA
|
FRIGATE_MEDIA=$FRIGATE_MEDIA
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
FRIGATE_ENV
|
FRIGATE_ENV
|
||||||
|
|
||||||
mkdir -p config
|
mkdir -p config
|
||||||
@@ -120,7 +128,7 @@ FRIGATE_CONFIG
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true
|
||||||
log_success "Frigate configured at $FRIGATE_DIR"
|
log_success "Frigate configured at $FRIGATE_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Frigate" "5000" "frigate"
|
configure_caddy_for_service "Frigate" "frigate:5000" "frigate"
|
||||||
|
|
||||||
write_readme "$FRIGATE_DIR" << MD
|
write_readme "$FRIGATE_DIR" << MD
|
||||||
# Frigate NVR
|
# Frigate NVR
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -29,13 +29,16 @@ install_homeassistant() {
|
|||||||
echo " HomeKit, mDNS/Zeroconf, some Zigbee/Z-Wave & Bluetooth)."
|
echo " HomeKit, mDNS/Zeroconf, some Zigbee/Z-Wave & Bluetooth)."
|
||||||
local HA_NETMODE=""
|
local HA_NETMODE=""
|
||||||
prompt_text " Choose networking mode [1]:" "1" HA_NETMODE
|
prompt_text " Choose networking mode [1]:" "1" HA_NETMODE
|
||||||
local HA_NET_LINES
|
local HA_NET_LINES HA_CADDY_NET_LINES
|
||||||
if [ "$HA_NETMODE" = "2" ]; then
|
if [ "$HA_NETMODE" = "2" ]; then
|
||||||
HA_NET_LINES=" network_mode: host"
|
HA_NET_LINES=" network_mode: host"
|
||||||
|
HA_CADDY_NET_LINES=""
|
||||||
echo " → Host networking selected (best device discovery)."
|
echo " → Host networking selected (best device discovery)."
|
||||||
else
|
else
|
||||||
HA_NET_LINES=" ports:
|
HA_NET_LINES=" ports:
|
||||||
- \"8123:8123\""
|
- \"8123:8123\""
|
||||||
|
HA_CADDY_NET_LINES=" networks:
|
||||||
|
- caddy_net"
|
||||||
echo " → Bridge networking selected (port 8123 published)."
|
echo " → Bridge networking selected (port 8123 published)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -55,6 +58,12 @@ services:
|
|||||||
- ./config:/config
|
- ./config:/config
|
||||||
- /run/dbus:/run/dbus:ro
|
- /run/dbus:/run/dbus:ro
|
||||||
${HA_NET_LINES}
|
${HA_NET_LINES}
|
||||||
|
${HA_CADDY_NET_LINES}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
HOMEASSISTANT_COMPOSE
|
HOMEASSISTANT_COMPOSE
|
||||||
|
|
||||||
mkdir -p config
|
mkdir -p config
|
||||||
@@ -82,7 +91,11 @@ HA_CONFIG
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "Home Assistant configured at $HOMEASSISTANT_DIR"
|
log_success "Home Assistant configured at $HOMEASSISTANT_DIR"
|
||||||
|
|
||||||
|
if [ "$HA_NETMODE" = "2" ]; then
|
||||||
configure_caddy_for_service "Home Assistant" "8123" "home"
|
configure_caddy_for_service "Home Assistant" "8123" "home"
|
||||||
|
else
|
||||||
|
configure_caddy_for_service "Home Assistant" "homeassistant:8123" "home"
|
||||||
|
fi
|
||||||
|
|
||||||
local START_HA=""
|
local START_HA=""
|
||||||
prompt_yn "Start Home Assistant now? (y/n):" "y" START_HA
|
prompt_yn "Start Home Assistant now? (y/n):" "y" START_HA
|
||||||
|
|||||||
+17
-1
@@ -137,6 +137,8 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
@@ -170,6 +172,11 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
IMMICH_COMPOSE
|
IMMICH_COMPOSE
|
||||||
else
|
else
|
||||||
cat > docker-compose.yml << 'IMMICH_COMPOSE'
|
cat > docker-compose.yml << 'IMMICH_COMPOSE'
|
||||||
@@ -192,6 +199,8 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
@@ -225,6 +234,11 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
IMMICH_COMPOSE
|
IMMICH_COMPOSE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -254,6 +268,7 @@ DB_PASSWORD=$DB_PASS
|
|||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
TZ=$TZ_VAL
|
TZ=$TZ_VAL
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
IMMICH_ENV
|
IMMICH_ENV
|
||||||
else
|
else
|
||||||
cat > .env << IMMICH_ENV
|
cat > .env << IMMICH_ENV
|
||||||
@@ -271,6 +286,7 @@ DB_PASSWORD=$DB_PASS
|
|||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
TZ=$TZ_VAL
|
TZ=$TZ_VAL
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
IMMICH_ENV
|
IMMICH_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -556,7 +572,7 @@ IMPORT_BODY
|
|||||||
|
|
||||||
log_success "Immich configured at $IMMICH_DIR"
|
log_success "Immich configured at $IMMICH_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Immich" "2283" "immich"
|
configure_caddy_for_service "Immich" "immich-server:2283" "immich"
|
||||||
|
|
||||||
write_readme "$IMMICH_DIR" << MD
|
write_readme "$IMMICH_DIR" << MD
|
||||||
# Immich
|
# Immich
|
||||||
|
|||||||
@@ -69,17 +69,25 @@ $HWACCEL_BLOCK
|
|||||||
- "8096:8096"
|
- "8096:8096"
|
||||||
- "1900:1900/udp"
|
- "1900:1900/udp"
|
||||||
- "7359:7359/udp"
|
- "7359:7359/udp"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
JELLYFIN_COMPOSE
|
JELLYFIN_COMPOSE
|
||||||
|
|
||||||
cat > .env << JELLYFIN_ENV
|
cat > .env << JELLYFIN_ENV
|
||||||
MEDIA_PATH=$MEDIA_PATH
|
MEDIA_PATH=$MEDIA_PATH
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
JELLYFIN_ENV
|
JELLYFIN_ENV
|
||||||
|
|
||||||
mkdir -p config cache
|
mkdir -p config cache
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR"
|
||||||
log_success "Jellyfin configured at $JELLYFIN_DIR"
|
log_success "Jellyfin configured at $JELLYFIN_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Jellyfin" "8096" "jellyfin"
|
configure_caddy_for_service "Jellyfin" "jellyfin:8096" "jellyfin"
|
||||||
|
|
||||||
write_readme "$JELLYFIN_DIR" << MD
|
write_readme "$JELLYFIN_DIR" << MD
|
||||||
# Jellyfin
|
# Jellyfin
|
||||||
|
|||||||
+8
-1
@@ -260,6 +260,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${JS99ER_PORT}:80"
|
- "${JS99ER_PORT}:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
COMPOSE
|
COMPOSE
|
||||||
log_success "Created js99er/docker-compose.yml"
|
log_success "Created js99er/docker-compose.yml"
|
||||||
|
|
||||||
@@ -268,7 +275,7 @@ COMPOSE
|
|||||||
log_success "js99er configured at $JS99ER_DIR"
|
log_success "js99er configured at $JS99ER_DIR"
|
||||||
|
|
||||||
# ── 5. Reverse proxy (no-ops if Caddy isn't installed locally) ───────────
|
# ── 5. Reverse proxy (no-ops if Caddy isn't installed locally) ───────────
|
||||||
configure_caddy_for_service "js99er" "$JS99ER_PORT" "js99er"
|
configure_caddy_for_service "js99er" "js99er:80" "js99er"
|
||||||
|
|
||||||
# ── 6. Build & start ─────────────────────────────────────────────────────
|
# ── 6. Build & start ─────────────────────────────────────────────────────
|
||||||
local START_JS99ER=""
|
local START_JS99ER=""
|
||||||
|
|||||||
@@ -56,10 +56,16 @@ services:
|
|||||||
- \${MUSIC_PATH}:/music:ro
|
- \${MUSIC_PATH}:/music:ro
|
||||||
- ./playlists:/playlists:rw
|
- ./playlists:/playlists:rw
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
LYRION_COMPOSE
|
LYRION_COMPOSE
|
||||||
|
|
||||||
cat > .env << LYRION_ENV
|
cat > .env << LYRION_ENV
|
||||||
MUSIC_PATH=$MUSIC_PATH
|
MUSIC_PATH=$MUSIC_PATH
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
LYRION_ENV
|
LYRION_ENV
|
||||||
|
|
||||||
mkdir -p config playlists
|
mkdir -p config playlists
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ services:
|
|||||||
- ./css:/opt/magic_mirror/css
|
- ./css:/opt/magic_mirror/css
|
||||||
ports:
|
ports:
|
||||||
- "$MM_PORT:8080"
|
- "$MM_PORT:8080"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
MM_COMPOSE
|
MM_COMPOSE
|
||||||
|
|
||||||
mkdir -p config modules css
|
mkdir -p config modules css
|
||||||
@@ -125,7 +132,7 @@ MM_COMPOSE
|
|||||||
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
|
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
|
||||||
|
|
||||||
# Offer Caddy only for first instance
|
# Offer Caddy only for first instance
|
||||||
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "$MM_PORT" "mirror"
|
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "magicmirror-${MM_PORT}:8080" "mirror"
|
||||||
|
|
||||||
local START_MM=""
|
local START_MM=""
|
||||||
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
|
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
+8
-1
@@ -50,13 +50,20 @@ services:
|
|||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
ports:
|
ports:
|
||||||
- "9925:9000"
|
- "9925:9000"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
MEALIE_COMPOSE
|
MEALIE_COMPOSE
|
||||||
|
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
|
||||||
log_success "Mealie configured at $MEALIE_DIR"
|
log_success "Mealie configured at $MEALIE_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Mealie" "9925" "recipes"
|
configure_caddy_for_service "Mealie" "mealie:9000" "recipes"
|
||||||
|
|
||||||
write_readme "$MEALIE_DIR" << MD
|
write_readme "$MEALIE_DIR" << MD
|
||||||
# Mealie
|
# Mealie
|
||||||
|
|||||||
@@ -54,19 +54,27 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "4430:443"
|
- "4430:443"
|
||||||
- "4433:4433"
|
- "4433:4433"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
MC_COMPOSE
|
MC_COMPOSE
|
||||||
|
|
||||||
cat > .env << MC_ENV
|
cat > .env << MC_ENV
|
||||||
MC_HOSTNAME=$MC_HOSTNAME
|
MC_HOSTNAME=$MC_HOSTNAME
|
||||||
MC_REVERSE_PROXY=false
|
MC_REVERSE_PROXY=false
|
||||||
MC_TLS_PORT=443
|
MC_TLS_PORT=443
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
MC_ENV
|
MC_ENV
|
||||||
|
|
||||||
mkdir -p data files backups
|
mkdir -p data files backups
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR"
|
||||||
log_success "MeshCentral configured at $MC_DIR"
|
log_success "MeshCentral configured at $MC_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "MeshCentral" "4430" "mesh"
|
configure_caddy_for_service "MeshCentral" "meshcentral:443" "mesh"
|
||||||
|
|
||||||
write_readme "$MC_DIR" << MD
|
write_readme "$MC_DIR" << MD
|
||||||
# MeshCentral
|
# MeshCentral
|
||||||
|
|||||||
@@ -35,10 +35,18 @@ services:
|
|||||||
- ./config:/etc/ntfy
|
- ./config:/etc/ntfy
|
||||||
ports:
|
ports:
|
||||||
- "8090:80"
|
- "8090:80"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
NTFY_COMPOSE
|
NTFY_COMPOSE
|
||||||
|
|
||||||
cat > .env << NTFY_ENV
|
cat > .env << NTFY_ENV
|
||||||
TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}
|
TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
NTFY_ENV
|
NTFY_ENV
|
||||||
|
|
||||||
mkdir -p cache config
|
mkdir -p cache config
|
||||||
@@ -47,6 +55,8 @@ NTFY_ENV
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "ntfy configured at $NTFY_DIR"
|
log_success "ntfy configured at $NTFY_DIR"
|
||||||
|
|
||||||
|
configure_caddy_for_service "ntfy" "ntfy:80" "ntfy"
|
||||||
|
|
||||||
write_readme "$NTFY_DIR" << MD
|
write_readme "$NTFY_DIR" << MD
|
||||||
# ntfy
|
# ntfy
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9443:9443"
|
- "9443:9443"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
PORTAINER_COMPOSE
|
PORTAINER_COMPOSE
|
||||||
|
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
@@ -41,6 +48,8 @@ PORTAINER_COMPOSE
|
|||||||
echo ""
|
echo ""
|
||||||
log_success "Portainer configured at $PORTAINER_DIR"
|
log_success "Portainer configured at $PORTAINER_DIR"
|
||||||
|
|
||||||
|
configure_caddy_for_service "Portainer" "portainer:9000" "portainer"
|
||||||
|
|
||||||
write_readme "$PORTAINER_DIR" << MD
|
write_readme "$PORTAINER_DIR" << MD
|
||||||
# Portainer
|
# Portainer
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
+8
-1
@@ -42,6 +42,13 @@ services:
|
|||||||
- "8082:8082"
|
- "8082:8082"
|
||||||
- "5000-5150:5000-5150"
|
- "5000-5150:5000-5150"
|
||||||
- "5000-5150:5000-5150/udp"
|
- "5000-5150:5000-5150/udp"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
TRACCAR_COMPOSE
|
TRACCAR_COMPOSE
|
||||||
|
|
||||||
mkdir -p logs data config
|
mkdir -p logs data config
|
||||||
@@ -63,7 +70,7 @@ TRACCAR_XML
|
|||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR"
|
||||||
log_success "Traccar configured at $TRACCAR_DIR"
|
log_success "Traccar configured at $TRACCAR_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "Traccar" "8082" "traccar"
|
configure_caddy_for_service "Traccar" "traccar:8082" "traccar"
|
||||||
|
|
||||||
write_readme "$TRACCAR_DIR" << MD
|
write_readme "$TRACCAR_DIR" << MD
|
||||||
# Traccar
|
# Traccar
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -32,6 +32,13 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
UPTIME_COMPOSE
|
UPTIME_COMPOSE
|
||||||
|
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
@@ -68,7 +75,7 @@ docker compose logs -f # logs
|
|||||||
MD
|
MD
|
||||||
|
|
||||||
# Configure Caddy reverse proxy before starting
|
# Configure Caddy reverse proxy before starting
|
||||||
configure_caddy_for_service "Uptime Kuma" "3001" "uptime"
|
configure_caddy_for_service "Uptime Kuma" "uptime-kuma:3001" "uptime"
|
||||||
|
|
||||||
local START_UPTIME=""
|
local START_UPTIME=""
|
||||||
prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME
|
prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -71,6 +71,13 @@ services:
|
|||||||
- WATCHTOWER_DEBUG=false
|
- WATCHTOWER_DEBUG=false
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: \${CADDY_NET:-caddy_net}
|
||||||
WT_COMPOSE
|
WT_COMPOSE
|
||||||
|
|
||||||
# Create .env
|
# Create .env
|
||||||
@@ -99,6 +106,7 @@ MONITOR_ONLY=$MONITOR_ONLY
|
|||||||
#
|
#
|
||||||
# Full list: https://containrrr.dev/shoutrrr/services/overview/
|
# Full list: https://containrrr.dev/shoutrrr/services/overview/
|
||||||
NOTIFICATION_URL=$NTFY_URL
|
NOTIFICATION_URL=$NTFY_URL
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
WT_ENV
|
WT_ENV
|
||||||
|
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WT_DIR" 2>/dev/null || true
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WT_DIR" 2>/dev/null || true
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
}
|
||||||
+9
-1
@@ -60,18 +60,26 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "51820:51820/udp"
|
- "51820:51820/udp"
|
||||||
- "51821:51821/tcp"
|
- "51821:51821/tcp"
|
||||||
|
networks:
|
||||||
|
- caddy_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
WGEASY_COMPOSE
|
WGEASY_COMPOSE
|
||||||
|
|
||||||
cat > .env << WGEASY_ENV
|
cat > .env << WGEASY_ENV
|
||||||
WG_HOST=$WG_HOST
|
WG_HOST=$WG_HOST
|
||||||
WG_PASSWORD=$WG_PASSWORD
|
WG_PASSWORD=$WG_PASSWORD
|
||||||
|
CADDY_NET=$SITE_CADDY_NET
|
||||||
WGEASY_ENV
|
WGEASY_ENV
|
||||||
|
|
||||||
mkdir -p config
|
mkdir -p config
|
||||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR"
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR"
|
||||||
log_success "wg-easy configured at $WGEASY_DIR"
|
log_success "wg-easy configured at $WGEASY_DIR"
|
||||||
|
|
||||||
configure_caddy_for_service "wg-easy" "51821" "vpn"
|
configure_caddy_for_service "wg-easy" "wg-easy:51821" "vpn"
|
||||||
|
|
||||||
write_readme "$WGEASY_DIR" << MD
|
write_readme "$WGEASY_DIR" << MD
|
||||||
# wg-easy
|
# wg-easy
|
||||||
|
|||||||
@@ -239,6 +239,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy_net:
|
||||||
|
external: true
|
||||||
|
name: ${CADDY_NET:-caddy_net}
|
||||||
COMPOSE
|
COMPOSE
|
||||||
log_success "docker-compose.yml written"
|
log_success "docker-compose.yml written"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,643 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# manage_users.sh — FileBrowser user management via the REST API.
|
||||||
|
#
|
||||||
|
# Placed in ~/docker/filebrowser/ by the filebrowser installer.
|
||||||
|
# Requires: curl, jq (sudo apt install curl jq)
|
||||||
|
#
|
||||||
|
# Run with no arguments for the interactive menu.
|
||||||
|
# Pass a command for one-shot use (see --help).
|
||||||
|
#
|
||||||
|
# ── Username rules ────────────────────────────────────────────────────────────
|
||||||
|
# Letters, numbers, hyphens, underscores only. No spaces, dots, or @.
|
||||||
|
# Examples: alice bob-smith data_user2
|
||||||
|
#
|
||||||
|
# ── Password rules ────────────────────────────────────────────────────────────
|
||||||
|
# Minimum 8 characters. No maximum.
|
||||||
|
# Must contain at least one letter and one number.
|
||||||
|
#
|
||||||
|
# ── Scope (file path) ─────────────────────────────────────────────────────────
|
||||||
|
# FileBrowser supports ONE scope path per user.
|
||||||
|
# Scope is an absolute path inside the container, relative to /srv (= FB_PATH).
|
||||||
|
#
|
||||||
|
# If FB_PATH=~/drives/data1:
|
||||||
|
# / → full access (all of ~/drives/data1)
|
||||||
|
# /alice → alice's own subdir (pair with linked dirs below)
|
||||||
|
# /music → music subdir only
|
||||||
|
#
|
||||||
|
# ── Multi-directory access via linked directories ────────────────────────────
|
||||||
|
# FileBrowser follows symlinks inside the scope dir.
|
||||||
|
# Give a user scope=/alice, then link extra folders into /alice:
|
||||||
|
#
|
||||||
|
# /alice/music → /srv/music (alice sees "music" in her root)
|
||||||
|
# /alice/photos → /srv/photos (alice sees "photos" too)
|
||||||
|
#
|
||||||
|
# This script creates those symlinks via "docker exec", so they live in
|
||||||
|
# the bind-mount on the host as well (target is /srv/... so it appears
|
||||||
|
# broken from the host, but resolves correctly inside the container).
|
||||||
|
#
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
FB_URL="${FB_URL:-http://localhost:8085}"
|
||||||
|
TOKEN=""
|
||||||
|
|
||||||
|
# ── Output helpers ────────────────────────────────────────────────────────────
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
B=$'\e[1m' R=$'\e[0m' GRN=$'\e[32m' RED=$'\e[31m' DIM=$'\e[2m'
|
||||||
|
else
|
||||||
|
B="" R="" GRN="" RED="" DIM=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
die() { echo "${RED}ERROR:${R} $*" >&2; exit 1; }
|
||||||
|
ok() { echo " ${GRN}✓${R} $*"; }
|
||||||
|
errmsg() { echo " ${RED}✗${R} $*" >&2; }
|
||||||
|
hr() { printf ' %s\n' "────────────────────────────────────────────"; }
|
||||||
|
banner() { echo; hr; printf " ${B}%-44s${R}\n" "$*"; hr; }
|
||||||
|
|
||||||
|
# ── Prerequisites ─────────────────────────────────────────────────────────────
|
||||||
|
require_cmds() {
|
||||||
|
for _c in "$@"; do
|
||||||
|
command -v "$_c" &>/dev/null || die "'$_c' not found — sudo apt install $_c"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Validation ────────────────────────────────────────────────────────────────
|
||||||
|
validate_username() {
|
||||||
|
[[ -n "$1" ]] || { errmsg "Username cannot be empty."; return 1; }
|
||||||
|
[[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || {
|
||||||
|
errmsg "Invalid username '$1'. Use only letters, numbers, hyphens, underscores."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_password() {
|
||||||
|
[[ ${#1} -ge 8 ]] || { errmsg "Password too short (minimum 8 characters)."; return 1; }
|
||||||
|
[[ "$1" =~ [a-zA-Z] ]] || { errmsg "Password must contain at least one letter."; return 1; }
|
||||||
|
[[ "$1" =~ [0-9] ]] || { errmsg "Password must contain at least one number."; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_scope() {
|
||||||
|
[[ "$1" == /* ]] || { errmsg "Scope must start with / (e.g. / or /alice or /music)"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# prompt_password VARNAME [label]
|
||||||
|
# Uses nameref (bash 4.3+) so the caller's local variable is set correctly.
|
||||||
|
prompt_password() {
|
||||||
|
local -n _pp_ref="$1"
|
||||||
|
local _label="${2:-New password}"
|
||||||
|
local _p1 _p2
|
||||||
|
while true; do
|
||||||
|
read -r -s -p " $_label: " _p1; echo
|
||||||
|
validate_password "$_p1" || continue
|
||||||
|
read -r -s -p " Confirm: " _p2; echo
|
||||||
|
[[ "$_p1" == "$_p2" ]] || { errmsg "Passwords do not match. Try again."; continue; }
|
||||||
|
_pp_ref="$_p1"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Auth — login once, reuse token ────────────────────────────────────────────
|
||||||
|
ensure_token() {
|
||||||
|
[[ -n "$TOKEN" ]] && return 0
|
||||||
|
echo
|
||||||
|
echo " ${B}FileBrowser login${R} ${DIM}(${FB_URL})${R}"
|
||||||
|
local _u _p _tok
|
||||||
|
read -r -p " Admin username [admin]: " _u
|
||||||
|
_u="${_u:-admin}"
|
||||||
|
read -r -s -p " Admin password: " _p; echo
|
||||||
|
_tok=$(curl -s -X POST "$FB_URL/api/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"$_u\",\"password\":\"$_p\"}")
|
||||||
|
[[ "$_tok" == *"."*"."* ]] \
|
||||||
|
|| die "Login failed. Check credentials and that FileBrowser is running at $FB_URL"
|
||||||
|
TOKEN="$_tok"
|
||||||
|
ok "Logged in as $_u"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── REST wrappers ─────────────────────────────────────────────────────────────
|
||||||
|
api_get() { curl -sf -X GET "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
||||||
|
api_post() { curl -sf -X POST "$FB_URL$1" -H "X-Auth: $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" -d "$2"; }
|
||||||
|
api_put() { curl -sf -X PUT "$FB_URL$1" -H "X-Auth: $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" -d "$2"; }
|
||||||
|
api_delete() { curl -sf -X DELETE "$FB_URL$1" -H "X-Auth: $TOKEN"; }
|
||||||
|
|
||||||
|
find_user() {
|
||||||
|
api_get "/api/users" | jq -r --arg u "$1" '.[] | select(.username==$u)'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_user_id() {
|
||||||
|
local _j
|
||||||
|
_j=$(find_user "$1")
|
||||||
|
[[ -n "$_j" ]] || { errmsg "User '$1' not found."; return 1; }
|
||||||
|
echo "$_j" | jq -r '.id'
|
||||||
|
}
|
||||||
|
|
||||||
|
default_perms() {
|
||||||
|
echo '{"admin":false,"execute":false,"create":true,"rename":true,
|
||||||
|
"modify":true,"delete":true,"share":false,"download":true}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Docker helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
get_container_name() {
|
||||||
|
local _compose="$SCRIPT_DIR/docker-compose.yml"
|
||||||
|
if [[ -f "$_compose" ]]; then
|
||||||
|
local _name
|
||||||
|
_name=$(grep 'container_name:' "$_compose" | head -1 | awk '{print $2}')
|
||||||
|
[[ -n "$_name" ]] && { echo "$_name"; return; }
|
||||||
|
fi
|
||||||
|
echo "filebrowser"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container() {
|
||||||
|
require_cmds docker
|
||||||
|
local _running
|
||||||
|
_running=$(docker inspect --format='{{.State.Running}}' "$1" 2>/dev/null || echo "false")
|
||||||
|
[[ "$_running" == "true" ]] \
|
||||||
|
|| { errmsg "Container '$1' is not running. Start it: docker compose up -d"; return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print symlinks inside /srv<scope>, one per line. Paths shown as FileBrowser sees them (no /srv).
|
||||||
|
list_links() {
|
||||||
|
local _c="$1" _scope="$2"
|
||||||
|
docker exec "$_c" find "/srv$_scope" -maxdepth 1 -type l \
|
||||||
|
-exec sh -c '
|
||||||
|
_name=$(basename "$1")
|
||||||
|
_target=$(readlink "$1")
|
||||||
|
_display="${_target#/srv}"
|
||||||
|
[ -z "$_display" ] && _display="/"
|
||||||
|
printf " %-24s→ %s\n" "$_name" "$_display"
|
||||||
|
' _ {} \; 2>/dev/null | sort || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── prompt_add_links SCOPE ────────────────────────────────────────────────────
|
||||||
|
# Loop: user types folder names as they appear in FileBrowser — blank to finish.
|
||||||
|
# Shared by cmd_add (offered inline) and menu_links (add option).
|
||||||
|
prompt_add_links() {
|
||||||
|
local _scope="$1"
|
||||||
|
local _c
|
||||||
|
_c=$(get_container_name)
|
||||||
|
check_container "$_c" || return 1
|
||||||
|
|
||||||
|
local _scope_dir="/srv$_scope"
|
||||||
|
docker exec "$_c" mkdir -p "$_scope_dir" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Type a folder name to add — ? to list available folders, blank when done."
|
||||||
|
echo
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local _src=""
|
||||||
|
read -r -p " Folder to add [done]: " _src
|
||||||
|
[[ -n "$_src" ]] || break
|
||||||
|
|
||||||
|
if [[ "$_src" == "?" ]]; then
|
||||||
|
local _avail
|
||||||
|
_avail=$(docker exec "$_c" find /srv -maxdepth 1 -mindepth 1 -type d \
|
||||||
|
-not -name ".*" 2>/dev/null | sed 's|^/srv/||' | sort | xargs echo) || true
|
||||||
|
echo " Available: ${_avail:-(none found)}"
|
||||||
|
echo
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Normalise: strip surrounding slashes
|
||||||
|
_src="${_src#/}"
|
||||||
|
_src="${_src%/}"
|
||||||
|
[[ -n "$_src" ]] || continue
|
||||||
|
|
||||||
|
local _target="/srv/$_src"
|
||||||
|
local _link_name
|
||||||
|
_link_name=$(basename "$_src")
|
||||||
|
|
||||||
|
if ! docker exec "$_c" test -e "$_target" 2>/dev/null; then
|
||||||
|
errmsg "'$_src' not found in FileBrowser — check the path and try again."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local _link_path="$_scope_dir/$_link_name"
|
||||||
|
if docker exec "$_c" test -e "$_link_path" 2>/dev/null; then
|
||||||
|
errmsg "'$_link_name' already exists in this user's folder — use Remove to clear it first."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker exec "$_c" ln -s "$_target" "$_link_path"
|
||||||
|
if [[ "$_src" == "$_link_name" ]]; then
|
||||||
|
ok "User can now see '$_link_name'"
|
||||||
|
else
|
||||||
|
ok "User can now see '$_link_name' ${DIM}(from $_src)${R}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Linked-directory submenu (shown from Modify option 5) ────────────────────
|
||||||
|
menu_links() {
|
||||||
|
local _scope="$1"
|
||||||
|
local _c
|
||||||
|
_c=$(get_container_name)
|
||||||
|
check_container "$_c" || return 1
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
banner "Linked directories (scope: $_scope)"
|
||||||
|
echo " Folders this user can see beyond their scope:"
|
||||||
|
echo
|
||||||
|
local _links
|
||||||
|
_links=$(list_links "$_c" "$_scope")
|
||||||
|
if [[ -n "$_links" ]]; then
|
||||||
|
echo "$_links"
|
||||||
|
else
|
||||||
|
echo " (none)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo " 1 Add folders"
|
||||||
|
echo " 2 Remove a folder"
|
||||||
|
echo " 0 Back"
|
||||||
|
echo
|
||||||
|
local _ch=""
|
||||||
|
read -r -p " Choice: " _ch
|
||||||
|
case "$_ch" in
|
||||||
|
1) prompt_add_links "$_scope" || true ;;
|
||||||
|
2)
|
||||||
|
local _link_name=""
|
||||||
|
echo
|
||||||
|
read -r -p " Folder name to remove: " _link_name
|
||||||
|
[[ -n "$_link_name" ]] || continue
|
||||||
|
local _link_path="/srv${_scope}/${_link_name}"
|
||||||
|
if ! docker exec "$_c" test -L "$_link_path" 2>/dev/null; then
|
||||||
|
errmsg "'$_link_name' is not a linked folder — refusing to delete."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
docker exec "$_c" rm "$_link_path"
|
||||||
|
ok "'$_link_name' removed."
|
||||||
|
;;
|
||||||
|
0) break ;;
|
||||||
|
*) errmsg "Invalid choice." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: list ─────────────────────────────────────────────────────────────────
|
||||||
|
cmd_list() {
|
||||||
|
ensure_token
|
||||||
|
echo
|
||||||
|
printf " ${B}%-22s %-5s %s${R}\n" "USERNAME" "ADMIN" "SCOPE"
|
||||||
|
printf " %-22s %-5s %s\n" "--------" "-----" "-----"
|
||||||
|
api_get "/api/users" | \
|
||||||
|
jq -r '.[] | [.username, (if .perm.admin then "yes" else "no" end), .scope] | @tsv' | \
|
||||||
|
while IFS=$'\t' read -r _u _a _s; do
|
||||||
|
printf " %-22s %-5s %s\n" "$_u" "$_a" "$_s"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: add ─────────────────────────────────────────────────────────────────
|
||||||
|
cmd_add() {
|
||||||
|
local _username="${1:-}" _scope="${2:-}" _is_admin="false"
|
||||||
|
[[ "${3:-}" == "--admin" ]] && _is_admin="true"
|
||||||
|
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
echo
|
||||||
|
echo " ${B}Username:${R} letters, numbers, hyphens, underscores only. No dots or @."
|
||||||
|
echo " ${B}Password:${R} min 8 chars, at least 1 letter and 1 number."
|
||||||
|
echo " ${B}Scope:${R} one path per user — use linked dirs for multi-folder access."
|
||||||
|
echo
|
||||||
|
read -r -p " Username: " _username
|
||||||
|
local _adm=""
|
||||||
|
read -r -p " Admin? [y/N]: " _adm
|
||||||
|
[[ "${_adm,,}" == "y" ]] && _is_admin="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
if [[ -z "$_scope" ]]; then
|
||||||
|
echo
|
||||||
|
echo " Scope examples:"
|
||||||
|
echo " / full access (all of FB_PATH)"
|
||||||
|
echo " $_username user's own private subdir (pair with linked dirs)"
|
||||||
|
echo " music music subdir only"
|
||||||
|
echo
|
||||||
|
read -r -p " Scope for '$_username': " _scope
|
||||||
|
_scope="/${_scope#/}" # ensure leading /
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate_scope "$_scope" || return 1
|
||||||
|
|
||||||
|
local _ex
|
||||||
|
_ex=$(find_user "$_username")
|
||||||
|
if [[ -n "$_ex" ]]; then
|
||||||
|
errmsg "User '$_username' already exists. Use Modify to change it."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
local password=""
|
||||||
|
prompt_password password "Password for $_username"
|
||||||
|
|
||||||
|
local _perms _body
|
||||||
|
_perms=$(default_perms)
|
||||||
|
[[ "$_is_admin" == "true" ]] && _perms=$(echo "$_perms" | jq '.admin = true')
|
||||||
|
_body=$(jq -n \
|
||||||
|
--arg u "$_username" --arg p "$password" --arg s "$_scope" \
|
||||||
|
--argjson perms "$_perms" \
|
||||||
|
'{username:$u, password:$p, scope:$s, locale:"en", viewMode:"list",
|
||||||
|
singleClick:false, sorting:{by:"name",asc:true}, perm:$perms,
|
||||||
|
commands:[], lockPassword:false, hideDotfiles:false, dateFormat:false}')
|
||||||
|
api_post "/api/users" "$_body" >/dev/null
|
||||||
|
echo
|
||||||
|
ok "User '$_username' created | scope: $_scope | admin: $_is_admin"
|
||||||
|
|
||||||
|
# Offer linked directories (pointless if scope is already /)
|
||||||
|
if command -v docker &>/dev/null && [[ "$_scope" != "/" ]]; then
|
||||||
|
local _do_links=""
|
||||||
|
read -r -p " Add linked directories for '$_username'? [y/N]: " _do_links
|
||||||
|
if [[ "${_do_links,,}" == "y" ]]; then
|
||||||
|
prompt_add_links "$_scope" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: delete ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_delete() {
|
||||||
|
local _username="${1:-}"
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
cmd_list
|
||||||
|
read -r -p " Username to delete: " _username
|
||||||
|
fi
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
local _uid
|
||||||
|
_uid=$(get_user_id "$_username") || return 1
|
||||||
|
|
||||||
|
local _c=""
|
||||||
|
read -r -p " Delete '$_username' (id $_uid)? [y/N]: " _c
|
||||||
|
[[ "${_c,,}" == "y" ]] || { echo " Aborted."; return 0; }
|
||||||
|
api_delete "/api/users/$_uid" >/dev/null
|
||||||
|
ok "User '$_username' deleted."
|
||||||
|
echo " ${DIM}Note: symlinks in their scope dir still exist on disk if you want to reuse them.${R}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: passwd ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_passwd() {
|
||||||
|
local _username="${1:-}"
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
cmd_list
|
||||||
|
read -r -p " Username: " _username
|
||||||
|
fi
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
local _uid _user
|
||||||
|
_uid=$(get_user_id "$_username") || return 1
|
||||||
|
_user=$(find_user "$_username")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ${B}Password rules:${R} min 8 chars, at least 1 letter and 1 number."
|
||||||
|
echo
|
||||||
|
local password=""
|
||||||
|
prompt_password password "New password for $_username"
|
||||||
|
|
||||||
|
local _body
|
||||||
|
_body=$(echo "$_user" | jq --arg p "$password" '. + {password: $p}')
|
||||||
|
api_put "/api/users/$_uid" "$_body" >/dev/null
|
||||||
|
echo
|
||||||
|
ok "Password updated for '$_username'."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: scope ────────────────────────────────────────────────────────────────
|
||||||
|
cmd_scope() {
|
||||||
|
local _username="${1:-}" _new_scope="${2:-}"
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
cmd_list
|
||||||
|
read -r -p " Username: " _username
|
||||||
|
fi
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
local _uid _user _old_scope
|
||||||
|
_uid=$(get_user_id "$_username") || return 1
|
||||||
|
_user=$(find_user "$_username")
|
||||||
|
_old_scope=$(echo "$_user" | jq -r '.scope')
|
||||||
|
|
||||||
|
if [[ -z "$_new_scope" ]]; then
|
||||||
|
echo
|
||||||
|
echo " Current scope: $_old_scope"
|
||||||
|
echo " ${DIM}Linked folders in the old scope are not moved automatically.${R}"
|
||||||
|
echo
|
||||||
|
read -r -p " New scope: " _new_scope
|
||||||
|
_new_scope="/${_new_scope#/}"
|
||||||
|
fi
|
||||||
|
validate_scope "$_new_scope" || return 1
|
||||||
|
|
||||||
|
local _body
|
||||||
|
_body=$(echo "$_user" | jq --arg s "$_new_scope" '. + {scope: $s}')
|
||||||
|
api_put "/api/users/$_uid" "$_body" >/dev/null
|
||||||
|
echo
|
||||||
|
ok "Scope updated for '$_username': $_old_scope → $_new_scope"
|
||||||
|
|
||||||
|
# Offer to add links into the new scope
|
||||||
|
if command -v docker &>/dev/null && [[ "$_new_scope" != "/" ]]; then
|
||||||
|
local _do_links=""
|
||||||
|
read -r -p " Add linked directories into '$_new_scope'? [y/N]: " _do_links
|
||||||
|
if [[ "${_do_links,,}" == "y" ]]; then
|
||||||
|
prompt_add_links "$_new_scope" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: rename ───────────────────────────────────────────────────────────────
|
||||||
|
cmd_rename() {
|
||||||
|
local _username="${1:-}" _new_username="${2:-}"
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
cmd_list
|
||||||
|
read -r -p " Username to rename: " _username
|
||||||
|
fi
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
if [[ -z "$_new_username" ]]; then
|
||||||
|
read -r -p " New username: " _new_username
|
||||||
|
fi
|
||||||
|
validate_username "$_new_username" || return 1
|
||||||
|
|
||||||
|
local _uid _user _body
|
||||||
|
_uid=$(get_user_id "$_username") || return 1
|
||||||
|
_user=$(find_user "$_username")
|
||||||
|
_body=$(echo "$_user" | jq --arg u "$_new_username" '. + {username: $u}')
|
||||||
|
api_put "/api/users/$_uid" "$_body" >/dev/null
|
||||||
|
ok "Renamed: '$_username' → '$_new_username'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── cmd: info ─────────────────────────────────────────────────────────────────
|
||||||
|
cmd_info() {
|
||||||
|
local _username="${1:-}"
|
||||||
|
ensure_token
|
||||||
|
|
||||||
|
if [[ -z "$_username" ]]; then
|
||||||
|
cmd_list
|
||||||
|
read -r -p " Username: " _username
|
||||||
|
fi
|
||||||
|
validate_username "$_username" || return 1
|
||||||
|
|
||||||
|
local _user
|
||||||
|
_user=$(find_user "$_username")
|
||||||
|
[[ -n "$_user" ]] || { errmsg "User '$_username' not found."; return 1; }
|
||||||
|
echo
|
||||||
|
echo "$_user" | jq '{username, scope,
|
||||||
|
admin: .perm.admin,
|
||||||
|
create: .perm.create,
|
||||||
|
modify: .perm.modify,
|
||||||
|
delete: .perm.delete,
|
||||||
|
download: .perm.download,
|
||||||
|
execute: .perm.execute}'
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Modify submenu ────────────────────────────────────────────────────────────
|
||||||
|
menu_modify() {
|
||||||
|
ensure_token
|
||||||
|
cmd_list
|
||||||
|
|
||||||
|
local _cur=""
|
||||||
|
read -r -p " Username to modify: " _cur
|
||||||
|
validate_username "$_cur" || return 1
|
||||||
|
get_user_id "$_cur" >/dev/null || return 1
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local _user _scope _admin
|
||||||
|
_user=$(find_user "$_cur") || { errmsg "User '$_cur' no longer exists."; break; }
|
||||||
|
[[ -n "$_user" ]] || { errmsg "User '$_cur' no longer exists."; break; }
|
||||||
|
_scope=$(echo "$_user" | jq -r '.scope')
|
||||||
|
_admin=$(echo "$_user" | jq -r 'if .perm.admin then "yes" else "no" end')
|
||||||
|
|
||||||
|
banner "Modify: $_cur"
|
||||||
|
echo " ${B}Scope:${R} $_scope"
|
||||||
|
echo " ${B}Admin:${R} $_admin"
|
||||||
|
echo
|
||||||
|
echo " 1 Change username"
|
||||||
|
echo " 2 Change password"
|
||||||
|
echo " 3 Change scope (file path)"
|
||||||
|
echo " 4 Toggle admin status"
|
||||||
|
echo " 5 Linked directories ${DIM}(add/remove multi-folder symlinks)${R}"
|
||||||
|
echo " 0 Back"
|
||||||
|
echo
|
||||||
|
local _ch=""
|
||||||
|
read -r -p " Choice: " _ch
|
||||||
|
|
||||||
|
case "$_ch" in
|
||||||
|
1)
|
||||||
|
local _new_u=""
|
||||||
|
echo
|
||||||
|
read -r -p " New username: " _new_u
|
||||||
|
validate_username "$_new_u" || continue
|
||||||
|
local _uid1 _body1
|
||||||
|
_uid1=$(get_user_id "$_cur") || continue
|
||||||
|
_body1=$(echo "$_user" | jq --arg u "$_new_u" '. + {username: $u}')
|
||||||
|
if api_put "/api/users/$_uid1" "$_body1" >/dev/null; then
|
||||||
|
ok "Renamed: '$_cur' → '$_new_u'"
|
||||||
|
_cur="$_new_u"
|
||||||
|
else
|
||||||
|
errmsg "Rename failed."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2) cmd_passwd "$_cur" || true ;;
|
||||||
|
3) cmd_scope "$_cur" || true ;;
|
||||||
|
4)
|
||||||
|
local _uid4
|
||||||
|
_uid4=$(get_user_id "$_cur") || continue
|
||||||
|
local _toggled
|
||||||
|
_toggled=$(echo "$_user" | jq '.perm.admin = (.perm.admin | not)')
|
||||||
|
if api_put "/api/users/$_uid4" "$_toggled" >/dev/null; then
|
||||||
|
local _new_admin
|
||||||
|
_new_admin=$(echo "$_toggled" | jq -r 'if .perm.admin then "yes" else "no" end')
|
||||||
|
ok "Admin for '$_cur' is now: $_new_admin"
|
||||||
|
else
|
||||||
|
errmsg "Toggle failed."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
5) menu_links "$_scope" || true ;;
|
||||||
|
0) break ;;
|
||||||
|
*) errmsg "Invalid choice." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── usage ─────────────────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
FileBrowser user management
|
||||||
|
|
||||||
|
Run with no arguments for the interactive menu.
|
||||||
|
|
||||||
|
One-shot usage:
|
||||||
|
manage_users.sh list
|
||||||
|
manage_users.sh add <username> <scope> [--admin]
|
||||||
|
manage_users.sh delete <username>
|
||||||
|
manage_users.sh passwd <username>
|
||||||
|
manage_users.sh scope <username> <new-scope>
|
||||||
|
manage_users.sh rename <username> <new-username>
|
||||||
|
manage_users.sh info <username>
|
||||||
|
|
||||||
|
Scope is relative to /srv inside the container (= FB_PATH on the host).
|
||||||
|
Username: letters, numbers, hyphens, underscores only. No dots or @.
|
||||||
|
Password: min 8 chars, at least one letter and one number.
|
||||||
|
|
||||||
|
Multi-directory access: use the interactive menu — linked directories
|
||||||
|
are offered automatically when you add a user or change their scope.
|
||||||
|
|
||||||
|
Override URL: FB_URL=http://localhost:8085 ./manage_users.sh
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main interactive menu ─────────────────────────────────────────────────────
|
||||||
|
run_interactive() {
|
||||||
|
ensure_token
|
||||||
|
while true; do
|
||||||
|
banner "FileBrowser User Manager"
|
||||||
|
echo " ${DIM}${FB_URL}${R}"
|
||||||
|
echo
|
||||||
|
echo " 1 List users"
|
||||||
|
echo " 2 Add user"
|
||||||
|
echo " 3 Delete user"
|
||||||
|
echo " 4 Modify user (username / password / scope / admin / links)"
|
||||||
|
echo " 5 View user details"
|
||||||
|
echo " 0 Exit"
|
||||||
|
echo
|
||||||
|
local _ch=""
|
||||||
|
read -r -p " Choice: " _ch
|
||||||
|
case "$_ch" in
|
||||||
|
1) cmd_list || true ;;
|
||||||
|
2) cmd_add || true ;;
|
||||||
|
3) cmd_delete || true ;;
|
||||||
|
4) menu_modify || true ;;
|
||||||
|
5) cmd_info || true ;;
|
||||||
|
0) echo; echo " Goodbye."; echo; exit 0 ;;
|
||||||
|
*) errmsg "Invalid choice." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
require_cmds curl jq
|
||||||
|
|
||||||
|
_cmd="${1:-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$_cmd" in
|
||||||
|
"") run_interactive ;;
|
||||||
|
list) ensure_token; cmd_list ;;
|
||||||
|
add) ensure_token; cmd_add "$@" ;;
|
||||||
|
delete|del) ensure_token; cmd_delete "${1:-}" ;;
|
||||||
|
passwd|pw) ensure_token; cmd_passwd "${1:-}" ;;
|
||||||
|
scope) ensure_token; cmd_scope "${1:-}" "${2:-}" ;;
|
||||||
|
rename) ensure_token; cmd_rename "${1:-}" "${2:-}" ;;
|
||||||
|
info) ensure_token; cmd_info "${1:-}" ;;
|
||||||
|
help|--help|-h) usage ;;
|
||||||
|
*) errmsg "Unknown command: $_cmd"; echo; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user