Merge branch 'main' into claude/zealous-feynman-odDrh
This commit is contained in:
@@ -67,11 +67,11 @@ Update them any time with `sudo ./setup.sh configure`.
|
||||
|-------|---------|
|
||||
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo) |
|
||||
| `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant`, `asterisk` |
|
||||
| `utilities` | `actualbudget`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `nextcloud`, `ntfy`, `onlyoffice`, `portainer`, `rustdesk`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` |
|
||||
| `media` | `arm`, `audiobookshelf`, `emby`, `immich`, `jellyfin`, `lyrion` |
|
||||
| `utilities` | `actualbudget`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `joplin`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy` |
|
||||
| `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` |
|
||||
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
|
||||
| `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` |
|
||||
| `extras` | `silent-send`, `sync-cc` |
|
||||
| `gaming` | `drum-rhythm-game`, `js99er`, `minecraft`, `wolf`, `wolf-pair` |
|
||||
| `extras` | `kdeconnect`, `silent-send`, `sync-cc` |
|
||||
| `backup` | `backup` — complete recovery: entire `~/docker/<service>/` for every service via Kopia (Minecraft: flush+snap, no downtime; others: stop/snap/start for DB consistency); `borg-backup` — same coverage via Borg (chunk dedup, SSH remote repos, Borgmatic/Vorta compatible); `gaming-backup` — frequent game-save snapshots (Minecraft world data, emulator saves, Steam — no downtime, run hourly) |
|
||||
|
||||
Run `./setup.sh --list` to see descriptions.
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
# services/archivebox.sh — Self-hosted web archiving (ArchiveBox).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash archivebox.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service archivebox utilities "Self-hosted web archiving — save pages like Wayback Machine (ArchiveBox)" 8000
|
||||
|
||||
install_archivebox() {
|
||||
require_docker || return 1
|
||||
|
||||
local AB_DIR="$DOCKER_DIR/archivebox"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] archivebox would:"
|
||||
echo " - Create $AB_DIR with docker-compose.yml"
|
||||
echo " - Initialize ArchiveBox data directory"
|
||||
echo " - Expose port 8000 (web UI)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$AB_DIR"
|
||||
ensure_docker_dir_ownership "$AB_DIR"
|
||||
cd "$AB_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << 'ABCOMPOSE'
|
||||
name: archivebox
|
||||
|
||||
services:
|
||||
archivebox:
|
||||
image: archivebox/archivebox:latest
|
||||
container_name: archivebox
|
||||
restart: unless-stopped
|
||||
user: "1000:1000"
|
||||
environment:
|
||||
- ALLOWED_HOSTS=*
|
||||
- MEDIA_MAX_SIZE=750m
|
||||
- PUBLIC_INDEX=True
|
||||
- PUBLIC_SNAPSHOTS=True
|
||||
- PUBLIC_ADD_VIEW=False
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "8000:8000"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
ABCOMPOSE
|
||||
|
||||
cat > .env << ABENV
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
ABENV
|
||||
|
||||
mkdir -p data
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$AB_DIR"
|
||||
|
||||
log_info "Initializing ArchiveBox data directory..."
|
||||
docker compose run --rm archivebox init --setup \
|
||||
&& log_success "ArchiveBox initialized" \
|
||||
|| log_warning "Init failed — will retry on first start"
|
||||
|
||||
configure_caddy_for_service "ArchiveBox" "archivebox:8000" "archive"
|
||||
|
||||
write_readme "$AB_DIR" << 'MD'
|
||||
# ArchiveBox
|
||||
|
||||
Self-hosted web archiving — saves full snapshots of web pages (HTML, screenshots,
|
||||
PDFs, WARC) like a personal Wayback Machine.
|
||||
|
||||
- Web UI: http://localhost:8000
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
cd ~/docker/archivebox
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose up -d # update
|
||||
```
|
||||
|
||||
## Add URLs to archive
|
||||
```bash
|
||||
# Via web UI — visit http://localhost:8000 and use the Add page
|
||||
# Via CLI:
|
||||
echo "https://example.com" | docker compose run --rm archivebox add
|
||||
docker compose run --rm archivebox add --depth=1 https://example.com
|
||||
```
|
||||
|
||||
## Create admin user
|
||||
```bash
|
||||
docker compose run --rm archivebox manage createsuperuser
|
||||
```
|
||||
MD
|
||||
|
||||
local START_AB=""
|
||||
prompt_yn "Start ArchiveBox now? (y/n):" "y" START_AB
|
||||
if [ "$START_AB" = "y" ] || [ "$START_AB" = "Y" ]; then
|
||||
docker compose up -d \
|
||||
&& log_success "ArchiveBox started — http://localhost:8000" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Web UI: http://localhost:8000"
|
||||
echo " Add URLs via the web UI or: echo 'URL' | docker compose run --rm archivebox add"
|
||||
echo ""
|
||||
}
|
||||
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_archivebox
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# services/base.sh — essential CLI packages installed on every box.
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
|
||||
register_service base base "Essential CLI packages (net-tools, git, htop, glow, …)"
|
||||
register_service base base "Essential CLI packages (net-tools, git, htop, btop, glow, …)"
|
||||
|
||||
install_base() {
|
||||
log_info "Installing essential packages..."
|
||||
@@ -10,7 +10,7 @@ install_base() {
|
||||
|
||||
# Core utilities present on every install.
|
||||
run_cmd apt-get install -y \
|
||||
net-tools ncdu git curl wget htop tree zip unzip \
|
||||
net-tools ncdu git curl wget htop btop tree zip unzip \
|
||||
ca-certificates gnupg jq rsync || log_warning "Some essential packages failed to install"
|
||||
|
||||
# glow — terminal markdown reader (charmbracelet). Not in Ubuntu repos,
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
#!/bin/bash
|
||||
# services/calibre-web.sh — Ebook library web UI with metadata editing (Calibre-Web).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash calibre-web.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service calibre-web media "Ebook library web UI with metadata editing (Calibre-Web)" 8083
|
||||
|
||||
install_calibre_web() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Calibre-Web..."
|
||||
|
||||
local CW_DIR="$DOCKER_DIR/calibre-web"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $CW_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$CW_DIR/config" "$CW_DIR/books"
|
||||
ensure_docker_dir_ownership "$CW_DIR"
|
||||
cd "$CW_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << CW_COMPOSE
|
||||
name: calibre-web
|
||||
|
||||
services:
|
||||
calibre-web:
|
||||
image: lscr.io/linuxserver/calibre-web:latest
|
||||
container_name: calibre-web
|
||||
hostname: calibre-web
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${SITE_TZ:-UTC}
|
||||
- DOCKER_MODS=linuxserver/mods:universal-calibre
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- ./books:/books
|
||||
ports:
|
||||
- "8083:8083"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: \${CADDY_NET:-caddy_net}
|
||||
CW_COMPOSE
|
||||
|
||||
cat > .env << CW_ENV
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
CW_ENV
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$CW_DIR"
|
||||
|
||||
echo ""
|
||||
log_success "Calibre-Web configured at $CW_DIR"
|
||||
|
||||
configure_caddy_for_service "Calibre-Web" "calibre-web:8083" "books"
|
||||
|
||||
write_readme "$CW_DIR" << 'MD'
|
||||
# Calibre-Web
|
||||
|
||||
Web-based ebook library with metadata editing, reading, and format conversion
|
||||
powered by Calibre.
|
||||
|
||||
## First-run setup
|
||||
1. Open the UI (http://localhost:8083) and log in with admin / admin123
|
||||
2. When prompted for the database location, enter: `/books`
|
||||
(point this at your existing Calibre library or an empty directory)
|
||||
3. Change the default password immediately under Admin → Edit User
|
||||
|
||||
## Ebook conversion
|
||||
The `DOCKER_MODS=linuxserver/mods:universal-calibre` environment variable
|
||||
installs the full Calibre binary inside the container, enabling on-the-fly
|
||||
ebook conversion (e.g. EPUB → MOBI/AZW3).
|
||||
|
||||
## Books directory
|
||||
Place your Calibre library (or individual books) in:
|
||||
~/docker/calibre-web/books/
|
||||
|
||||
If you already have a Calibre library elsewhere, mount that path instead by
|
||||
editing the `./books:/books` volume line in docker-compose.yml.
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose down
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose down && docker compose up -d
|
||||
```
|
||||
MD
|
||||
|
||||
local START_CW=""
|
||||
prompt_yn "Start Calibre-Web now? (y/n):" "y" START_CW
|
||||
if [ "$START_CW" = "y" ] || [ "$START_CW" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "Calibre-Web started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:8083"
|
||||
echo " Default login: admin / admin123 (change immediately!)"
|
||||
echo " Point the database to /books on first run."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_calibre_web
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
# services/changedetection.sh — Web page change detection and notification (Changedetection.io).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash changedetection.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service changedetection utilities "Web page change detection and notification (Changedetection.io)" 5000
|
||||
|
||||
install_changedetection() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Changedetection.io..."
|
||||
|
||||
local CD_DIR="$DOCKER_DIR/changedetection"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $CD_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$CD_DIR/data"
|
||||
ensure_docker_dir_ownership "$CD_DIR"
|
||||
cd "$CD_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << 'CD_COMPOSE'
|
||||
name: changedetection
|
||||
|
||||
services:
|
||||
changedetection:
|
||||
image: ghcr.io/dgtlmoon/changedetection.io:latest
|
||||
container_name: changedetection
|
||||
hostname: changedetection
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- BASE_URL=${BASE_URL}
|
||||
- PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000
|
||||
volumes:
|
||||
- ./data:/datastore
|
||||
networks:
|
||||
- caddy_net
|
||||
depends_on:
|
||||
- playwright-chrome
|
||||
|
||||
playwright-chrome:
|
||||
image: browserless/chrome:latest
|
||||
container_name: playwright-chrome
|
||||
hostname: playwright-chrome
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DEFAULT_LAUNCH_ARGS=--no-sandbox --disable-dev-shm-usage
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
CD_COMPOSE
|
||||
|
||||
cat > .env << CD_ENV
|
||||
# Changedetection.io environment — edit before starting if needed
|
||||
BASE_URL=https://changes.${SITE_DOMAIN}
|
||||
CADDY_NET=${SITE_CADDY_NET}
|
||||
CD_ENV
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$CD_DIR"
|
||||
chmod 600 "$CD_DIR/.env"
|
||||
|
||||
echo ""
|
||||
log_success "Changedetection.io configured at $CD_DIR"
|
||||
|
||||
configure_caddy_for_service "Changedetection" "changedetection:5000" "changes"
|
||||
|
||||
write_readme "$CD_DIR" << 'MD'
|
||||
# Changedetection.io
|
||||
|
||||
Monitor web pages for changes and receive notifications via email, Slack,
|
||||
Discord, ntfy, and many other channels. Includes a Playwright/Chrome sidecar
|
||||
for JavaScript-heavy pages.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:5000
|
||||
- Optional password can be set in Settings → General within the UI.
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose down && docker compose up -d # update
|
||||
```
|
||||
|
||||
## Environment
|
||||
Edit `.env` to change `BASE_URL` (used for notification links),
|
||||
then restart: `docker compose down && docker compose up -d`
|
||||
MD
|
||||
|
||||
local START_CD=""
|
||||
prompt_yn "Start Changedetection.io now? (y/n):" "y" START_CD
|
||||
if [ "$START_CD" = "y" ] || [ "$START_CD" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "Changedetection.io started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:5000"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_changedetection
|
||||
@@ -0,0 +1,298 @@
|
||||
#!/bin/bash
|
||||
# services/drum-rhythm-game.sh — Browser-based drum rhythm game (outis1one/drum-rhythm-game).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash drum-rhythm-game.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
#
|
||||
# Serves a single self-contained index.html via nginx. No login — protect
|
||||
# with Authelia via Caddy if you want access control.
|
||||
# Source: https://github.com/outis1one/drum-rhythm-game
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service drum-rhythm-game gaming "Browser-based drum rhythm game (outis1one/drum-rhythm-game)" 8096
|
||||
|
||||
install_drum-rhythm-game() {
|
||||
require_docker || return 1
|
||||
|
||||
local DRUM_DIR="$DOCKER_DIR/drum-rhythm-game"
|
||||
local REPO_URL="https://github.com/outis1one/drum-rhythm-game.git"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] drum-rhythm-game would:"
|
||||
echo " - Clone $REPO_URL to $DRUM_DIR/html"
|
||||
echo " - Serve index.html via nginx on port 8096"
|
||||
echo " - Offer Authelia SSO protection via Caddy (no built-in auth)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DRUM_DIR"
|
||||
ensure_docker_dir_ownership "$DRUM_DIR"
|
||||
cd "$DRUM_DIR" || return 1
|
||||
|
||||
# Clone or update the game source
|
||||
if [ -d "$DRUM_DIR/html/.git" ]; then
|
||||
log_info "Updating drum-rhythm-game source..."
|
||||
git -C "$DRUM_DIR/html" pull --ff-only 2>/dev/null \
|
||||
&& log_success "Updated to latest" \
|
||||
|| log_warning "Could not pull latest — using existing version"
|
||||
else
|
||||
log_info "Cloning drum-rhythm-game..."
|
||||
git clone --depth 1 "$REPO_URL" "$DRUM_DIR/html" \
|
||||
|| { log_error "Clone failed — check network and git access"; return 1; }
|
||||
fi
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DRUM_DIR/html"
|
||||
|
||||
cat > docker-compose.yml << 'DRUM_COMPOSE'
|
||||
name: drum-rhythm-game
|
||||
|
||||
services:
|
||||
drum-rhythm-game:
|
||||
image: nginx:alpine
|
||||
container_name: drum-rhythm-game
|
||||
hostname: drum-rhythm-game
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html:ro
|
||||
ports:
|
||||
- "8096:80"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
DRUM_COMPOSE
|
||||
|
||||
cat > .env << DRUM_ENV
|
||||
CADDY_NET=${SITE_CADDY_NET}
|
||||
DRUM_ENV
|
||||
|
||||
ensure_docker_dir_ownership "$DRUM_DIR"
|
||||
log_success "drum-rhythm-game configured at $DRUM_DIR"
|
||||
|
||||
# No built-in auth — offer Authelia SSO protection
|
||||
local DRUM_EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect drum-rhythm-game with Authelia SSO? (y/n):" "y" _use_auth
|
||||
[[ "$_use_auth" =~ ^[Yy]$ ]] && DRUM_EXTRA_BLOCK=" import authelia"
|
||||
fi
|
||||
configure_caddy_for_service "Drum Rhythm Game" "drum-rhythm-game:80" "drums" "$DRUM_EXTRA_BLOCK"
|
||||
|
||||
write_readme "$DRUM_DIR" << 'MD'
|
||||
# Drum Rhythm Game
|
||||
|
||||
Browser-based drum rhythm game — 124 synthesized orchestra pieces across
|
||||
18 genres, 120 drum patterns. Supports keyboard and USB drum controllers.
|
||||
No server required; all audio synthesized in-browser via Web Audio API.
|
||||
|
||||
Source: https://github.com/outis1one/drum-rhythm-game
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:8096
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
cd ~/docker/drum-rhythm-game
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
```
|
||||
|
||||
## Update game
|
||||
```bash
|
||||
cd ~/docker/drum-rhythm-game
|
||||
git -C html pull
|
||||
docker compose restart
|
||||
```
|
||||
MD
|
||||
|
||||
local START_DRUM=""
|
||||
prompt_yn "Start drum-rhythm-game now? (y/n):" "y" START_DRUM
|
||||
if [ "$START_DRUM" = "y" ] || [ "$START_DRUM" = "Y" ]; then
|
||||
docker compose up -d \
|
||||
&& log_success "Drum Rhythm Game started — http://localhost:8096" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " URL: http://localhost:8096"
|
||||
echo " Controls: keyboard or USB drum controller"
|
||||
echo " Update: git -C $DRUM_DIR/html pull && docker compose -f $DRUM_DIR/docker-compose.yml restart"
|
||||
echo ""
|
||||
}
|
||||
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_drum-rhythm-game
|
||||
@@ -278,6 +278,25 @@ HA_CONFIG
|
||||
configure_caddy_for_service "Home Assistant" "homeassistant:8123" "home"
|
||||
fi
|
||||
|
||||
write_readme "$HOMEASSISTANT_DIR" << MD
|
||||
# Home Assistant
|
||||
|
||||
Home automation hub. Built-in auth — no Authelia needed.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:8123
|
||||
- First run: create your admin account through the onboarding wizard
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $HOMEASSISTANT_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_HA=""
|
||||
prompt_yn "Start Home Assistant now? (y/n):" "y" START_HA
|
||||
if [ "$START_HA" = "y" ] || [ "$START_HA" = "Y" ]; then
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/bin/bash
|
||||
# services/homebox.sh — Home inventory and asset management (Homebox).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash homebox.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service homebox utilities "Home inventory and asset management (Homebox)" 7745
|
||||
|
||||
install_homebox() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Homebox..."
|
||||
|
||||
local HB_DIR="$DOCKER_DIR/homebox"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $HB_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$HB_DIR/data"
|
||||
ensure_docker_dir_ownership "$HB_DIR"
|
||||
cd "$HB_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << HB_COMPOSE
|
||||
name: homebox
|
||||
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/sysadminsmedia/homebox:latest
|
||||
container_name: homebox
|
||||
hostname: homebox
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "7745:7745"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: \${CADDY_NET:-caddy_net}
|
||||
HB_COMPOSE
|
||||
|
||||
cat > .env << HB_ENV
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
HB_ENV
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$HB_DIR"
|
||||
|
||||
echo ""
|
||||
log_success "Homebox configured at $HB_DIR"
|
||||
|
||||
configure_caddy_for_service "Homebox" "homebox:7745" "homebox"
|
||||
|
||||
write_readme "$HB_DIR" << 'MD'
|
||||
# Homebox
|
||||
|
||||
Home inventory and asset management. Track items, locations, labels,
|
||||
warranties, and attachments across your household.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:7745
|
||||
- Register your account on first visit — the first user becomes the admin.
|
||||
|
||||
## Data
|
||||
- All inventory data and attachments are stored in: ./data/
|
||||
|
||||
## Configuration
|
||||
Key environment variables (edit docker-compose.yml to change):
|
||||
- `HBOX_LOG_LEVEL` — log verbosity (info, debug, warn, error)
|
||||
- `HBOX_WEB_MAX_UPLOAD_SIZE` — max attachment upload size in MB (default: 10)
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose down
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose down && docker compose up -d
|
||||
```
|
||||
MD
|
||||
|
||||
local START_HB=""
|
||||
prompt_yn "Start Homebox now? (y/n):" "y" START_HB
|
||||
if [ "$START_HB" = "y" ] || [ "$START_HB" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "Homebox started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:7745"
|
||||
echo " Register your account on first visit."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_homebox
|
||||
@@ -0,0 +1,316 @@
|
||||
#!/bin/bash
|
||||
# services/joplin.sh — Self-hosted Joplin sync server for notes.
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash joplin.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service joplin utilities "Self-hosted Joplin sync server for notes" 22300
|
||||
|
||||
install_joplin() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Joplin Server..."
|
||||
|
||||
local JOPLIN_DIR="$DOCKER_DIR/joplin"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $JOPLIN_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$JOPLIN_DIR"
|
||||
ensure_docker_dir_ownership "$JOPLIN_DIR"
|
||||
cd "$JOPLIN_DIR" || return 1
|
||||
|
||||
local DB_PASS
|
||||
DB_PASS="$(generate_password 32)"
|
||||
local BASE_URL="https://joplin.${SITE_DOMAIN}"
|
||||
|
||||
cat > docker-compose.yml << 'JOPLIN_COMPOSE'
|
||||
name: joplin
|
||||
|
||||
services:
|
||||
joplin:
|
||||
image: joplin/server:latest
|
||||
container_name: joplin
|
||||
hostname: joplin
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- joplin-db
|
||||
env_file: .env
|
||||
ports:
|
||||
- "22300:22300"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
joplin-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: joplin-db
|
||||
hostname: joplin-db
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./db-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
JOPLIN_COMPOSE
|
||||
|
||||
cat > .env << JOPLIN_ENV
|
||||
# Joplin Server configuration
|
||||
APP_PORT=22300
|
||||
# Must match the public URL — update if your domain changes
|
||||
APP_BASE_URL=${BASE_URL}
|
||||
|
||||
# Database connection (Joplin Server)
|
||||
DB_CLIENT=pg
|
||||
POSTGRES_HOST=joplin-db
|
||||
POSTGRES_DATABASE=joplin
|
||||
POSTGRES_USER=joplin
|
||||
POSTGRES_PASSWORD=${DB_PASS}
|
||||
|
||||
# PostgreSQL sidecar
|
||||
POSTGRES_DB=joplin
|
||||
|
||||
# Caddy network
|
||||
CADDY_NET=${SITE_CADDY_NET}
|
||||
JOPLIN_ENV
|
||||
|
||||
chmod 600 .env
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JOPLIN_DIR"
|
||||
|
||||
echo ""
|
||||
log_success "Joplin Server configured at $JOPLIN_DIR"
|
||||
log_info "APP_BASE_URL set to: $BASE_URL"
|
||||
log_warning "APP_BASE_URL in .env must match the public URL used by Joplin clients."
|
||||
|
||||
configure_caddy_for_service "Joplin" "joplin:22300" "joplin"
|
||||
|
||||
write_readme "$JOPLIN_DIR" << MD
|
||||
# Joplin Server
|
||||
|
||||
Self-hosted sync server for the Joplin note-taking app.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:22300
|
||||
- Default admin: admin@localhost / admin (change immediately after first login!)
|
||||
|
||||
## Important
|
||||
\`APP_BASE_URL\` in \`.env\` must exactly match the public URL your Joplin
|
||||
clients connect to (e.g. https://joplin.example.com). If this URL changes,
|
||||
update .env and restart the stack.
|
||||
|
||||
## Client setup
|
||||
In the Joplin desktop or mobile app:
|
||||
Tools → Options → Synchronisation → Synchronisation target: Joplin Server
|
||||
Enter your server URL, email, and password.
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $JOPLIN_DIR
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose down && docker compose up -d # update
|
||||
\`\`\`
|
||||
|
||||
## Files
|
||||
- docker-compose.yml — stack definition
|
||||
- .env — secrets and config (chmod 600)
|
||||
- db-data/ — PostgreSQL data volume
|
||||
MD
|
||||
|
||||
local START_JOPLIN=""
|
||||
prompt_yn "Start Joplin Server now? (y/n):" "y" START_JOPLIN
|
||||
if [ "$START_JOPLIN" = "y" ] || [ "$START_JOPLIN" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "Joplin Server started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:22300"
|
||||
echo " Default login: admin@localhost / admin (change immediately!)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_joplin
|
||||
+25
-2
@@ -455,7 +455,13 @@ COMPOSE
|
||||
log_success "js99er configured at $JS99ER_DIR"
|
||||
|
||||
# ── 5. Reverse proxy (no-ops if Caddy isn't installed locally) ───────────
|
||||
configure_caddy_for_service "js99er" "js99er:80" "js99er"
|
||||
local JS99ER_EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect js99er with Authelia SSO? (y/n):" "y" _use_auth
|
||||
[[ "$_use_auth" =~ ^[Yy]$ ]] && JS99ER_EXTRA_BLOCK=" import authelia"
|
||||
fi
|
||||
configure_caddy_for_service "js99er" "js99er:80" "js99er" "$JS99ER_EXTRA_BLOCK"
|
||||
|
||||
# ── 6. Build & start ─────────────────────────────────────────────────────
|
||||
local START_JS99ER=""
|
||||
@@ -477,7 +483,24 @@ COMPOSE
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 7. Access summary ────────────────────────────────────────────────────
|
||||
write_readme "$JS99ER_DIR" << MD
|
||||
# js99er — TI-99/4A Emulator
|
||||
|
||||
Browser-based TI-99/4A emulator. No built-in auth — protect with Authelia if exposing externally.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:${JS99ER_PORT}
|
||||
- Online (no install): https://js99er.net
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $JS99ER_DIR
|
||||
docker compose up -d --build # start (or rebuild)
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
\`\`\`
|
||||
MD
|
||||
|
||||
echo ""
|
||||
echo " Access at: http://localhost:${JS99ER_PORT}"
|
||||
echo " If you set a domain above, it is also reachable via that domain (HTTPS)."
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
# services/kdeconnect.sh — Phone/desktop integration via KDE Connect.
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash kdeconnect.sh
|
||||
#
|
||||
# KDE Connect is an APT package — NOT a Docker service.
|
||||
# It enables Android/iPhone ↔ Linux integration:
|
||||
# • Shared clipboard • File transfer
|
||||
# • Phone notifications on desktop • Remote input (trackpad/keyboard)
|
||||
# • SMS from desktop • Battery status
|
||||
#
|
||||
# NOTE: setup.sh is_installed() needs a case entry for this service:
|
||||
# kdeconnect) command -v kdeconnect >/dev/null 2>&1 ;;
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
# Detected when the script is executed directly rather than sourced by setup.sh.
|
||||
# Sets up helpers and globals, then defers execution until after the function
|
||||
# definition at the bottom of this file.
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service kdeconnect extras "Phone/desktop integration — notifications, clipboard, file transfer (KDE Connect)"
|
||||
|
||||
install_kdeconnect() {
|
||||
log_info "Installing KDE Connect phone/desktop integration..."
|
||||
|
||||
echo ""
|
||||
echo "┌─────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ KDE CONNECT — Phone / Desktop Integration │"
|
||||
echo "│ Shared clipboard, notifications, file transfer, remote input │"
|
||||
echo "│ Works with Android (Play Store / F-Droid) and iPhone (App Store)│"
|
||||
echo "└─────────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
|
||||
# ── Already installed? ────────────────────────────────────────────────────
|
||||
if command -v kdeconnect &>/dev/null; then
|
||||
log_info "KDE Connect is already installed — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── DRY-RUN: describe the plan and bail before touching anything real ─────
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would install: kdeconnect"
|
||||
if dpkg -l gnome-shell 2>/dev/null | grep -q ^ii; then
|
||||
echo "[DRY-RUN] Would install: indicator-kdeconnect (GNOME/Ubuntu detected)"
|
||||
fi
|
||||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||
echo "[DRY-RUN] Would open UFW ports 1714:1764/tcp and 1714:1764/udp"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── 1. Install kdeconnect ─────────────────────────────────────────────────
|
||||
log_info "Installing kdeconnect package..."
|
||||
if apt-get install -y kdeconnect; then
|
||||
log_success "kdeconnect installed"
|
||||
else
|
||||
log_error "Failed to install kdeconnect — check apt output above"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── 2. GNOME indicator (Ubuntu/GNOME only) ────────────────────────────────
|
||||
if dpkg -l gnome-shell 2>/dev/null | grep -q ^ii; then
|
||||
log_info "GNOME detected — installing indicator-kdeconnect for system tray support..."
|
||||
if apt-get install -y indicator-kdeconnect; then
|
||||
log_success "indicator-kdeconnect installed"
|
||||
else
|
||||
log_warning "Could not install indicator-kdeconnect — continuing without it"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 3. Open UFW firewall ports (KDE Connect port range) ───────────────────
|
||||
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||
log_info "Opening UFW ports 1714:1764/tcp and 1714:1764/udp (KDE Connect)..."
|
||||
ufw allow 1714:1764/tcp
|
||||
ufw allow 1714:1764/udp
|
||||
log_success "UFW ports 1714-1764 opened"
|
||||
else
|
||||
log_info "UFW not active — skipping firewall rules"
|
||||
echo " If you enable UFW later, run:"
|
||||
echo " sudo ufw allow 1714:1764/tcp"
|
||||
echo " sudo ufw allow 1714:1764/udp"
|
||||
fi
|
||||
|
||||
# ── 4. Usage instructions ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " ┌─ Next steps ────────────────────────────────────────────────────┐"
|
||||
echo " │ 1. Install KDE Connect on your phone: │"
|
||||
echo " │ Android: Play Store or F-Droid → search 'KDE Connect' │"
|
||||
echo " │ iPhone: App Store → search 'KDE Connect' │"
|
||||
echo " │ │"
|
||||
echo " │ 2. Ensure your phone and computer are on the same WiFi network. │"
|
||||
echo " │ │"
|
||||
echo " │ 3. Open KDE Connect on your phone — your computer should │"
|
||||
echo " │ appear automatically. Tap it and accept the pairing request │"
|
||||
echo " │ on both devices. │"
|
||||
echo " │ │"
|
||||
echo " │ Linux Mint Cinnamon: a system tray applet is available — │"
|
||||
echo " │ right-click the desktop → Applets → search 'KDE Connect' │"
|
||||
echo " │ and add it to your panel. │"
|
||||
echo " └──────────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
|
||||
log_success "KDE Connect installation complete"
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_kdeconnect
|
||||
@@ -317,7 +317,15 @@ MM_COMPOSE
|
||||
log_success "MagicMirror instance $i configured at $MM_DIR (port $MM_PORT)"
|
||||
|
||||
# Offer Caddy only for first instance
|
||||
[ "$i" -eq 1 ] && configure_caddy_for_service "MagicMirror" "magicmirror-${MM_PORT}:8080" "mirror"
|
||||
if [ "$i" -eq 1 ]; then
|
||||
local MM_EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect MagicMirror with Authelia SSO? (y/n):" "y" _use_auth
|
||||
[[ "$_use_auth" =~ ^[Yy]$ ]] && MM_EXTRA_BLOCK=" import authelia"
|
||||
fi
|
||||
configure_caddy_for_service "MagicMirror" "magicmirror-${MM_PORT}:8080" "mirror" "$MM_EXTRA_BLOCK"
|
||||
fi
|
||||
|
||||
local START_MM=""
|
||||
prompt_yn "Start instance $i now? (y/n):" "y" START_MM
|
||||
|
||||
+15
-1
@@ -205,6 +205,13 @@ install_mealie() {
|
||||
TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
UID_VAL=$(id -u "$ACTUAL_USER"); GID_VAL=$(id -g "$ACTUAL_USER")
|
||||
|
||||
# BASE_URL must match the public URL Mealie is served on (used for email links,
|
||||
# OAuth redirects, and the web app manifest). Default to SITE_DOMAIN if set.
|
||||
local MEALIE_BASE_URL="http://localhost:9925"
|
||||
if [ -n "$SITE_DOMAIN" ] && [ "$SITE_DOMAIN" != "example.com" ]; then
|
||||
MEALIE_BASE_URL="https://recipes.${SITE_DOMAIN}"
|
||||
fi
|
||||
|
||||
cat > docker-compose.yml << MEALIE_COMPOSE
|
||||
name: mealie
|
||||
|
||||
@@ -214,6 +221,7 @@ services:
|
||||
container_name: mealie
|
||||
hostname: mealie
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- PUID=$UID_VAL
|
||||
- PGID=$GID_VAL
|
||||
@@ -221,7 +229,6 @@ services:
|
||||
- ALLOW_SIGNUP=true
|
||||
- MAX_WORKERS=1
|
||||
- WEB_CONCURRENCY=1
|
||||
- BASE_URL=http://localhost:9925
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
@@ -235,6 +242,13 @@ networks:
|
||||
name: \${CADDY_NET:-caddy_net}
|
||||
MEALIE_COMPOSE
|
||||
|
||||
cat > .env << MEALIE_ENV
|
||||
# Public URL Mealie is served on — used for email links and OAuth redirects.
|
||||
# Update if you change your domain or switch from HTTP to HTTPS.
|
||||
BASE_URL=$MEALIE_BASE_URL
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
MEALIE_ENV
|
||||
|
||||
mkdir -p data
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR"
|
||||
log_success "Mealie configured at $MEALIE_DIR"
|
||||
|
||||
@@ -2490,6 +2490,25 @@ NETEOF
|
||||
echo " Run: sudo ./setup.sh backup"
|
||||
echo ""
|
||||
|
||||
write_readme "$MC_DIR" << MD
|
||||
# Minecraft — ${MC_NAME}
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $MC_DIR
|
||||
docker compose up -d --build # start (builds image on first run)
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose up -d --build # update
|
||||
\`\`\`
|
||||
|
||||
## Backups
|
||||
World data lives in \`./data\` — covered by Kopia/Borg if installed.
|
||||
\`\`\`bash
|
||||
sudo ./setup.sh backup
|
||||
\`\`\`
|
||||
MD
|
||||
|
||||
# ── Optional: start server and run pre-gen now ──────────────────────────────
|
||||
local START_MC=""
|
||||
prompt_yn "Start the Minecraft server now? (first build takes a few minutes) (y/n) [y]:" "y" START_MC
|
||||
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
#!/bin/bash
|
||||
# services/n8n.sh — Workflow automation — connect all your self-hosted services (n8n).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash n8n.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service n8n utilities "Workflow automation — connect all your self-hosted services (n8n)" 5678
|
||||
|
||||
install_n8n() {
|
||||
require_docker || return 1
|
||||
log_info "Installing n8n..."
|
||||
|
||||
local N8N_DIR="$DOCKER_DIR/n8n"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $N8N_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$N8N_DIR/data"
|
||||
ensure_docker_dir_ownership "$N8N_DIR"
|
||||
cd "$N8N_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << 'N8N_COMPOSE'
|
||||
name: n8n
|
||||
|
||||
services:
|
||||
n8n:
|
||||
image: n8nio/n8n:latest
|
||||
container_name: n8n
|
||||
hostname: n8n
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5678:5678"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- N8N_PORT=5678
|
||||
- N8N_PROTOCOL=https
|
||||
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC}
|
||||
- N8N_HOST=${N8N_HOST:-n8n}
|
||||
- WEBHOOK_URL=${WEBHOOK_URL}
|
||||
volumes:
|
||||
- ./data:/home/node/.n8n
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
N8N_COMPOSE
|
||||
|
||||
cat > .env << N8N_ENV
|
||||
# n8n environment — edit before starting if needed
|
||||
N8N_HOST=n8n
|
||||
WEBHOOK_URL=https://n8n.${SITE_DOMAIN}
|
||||
GENERIC_TIMEZONE=${SITE_TZ}
|
||||
CADDY_NET=${SITE_CADDY_NET}
|
||||
N8N_ENV
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$N8N_DIR"
|
||||
chmod 600 "$N8N_DIR/.env"
|
||||
|
||||
echo ""
|
||||
log_success "n8n configured at $N8N_DIR"
|
||||
|
||||
configure_caddy_for_service "n8n" "n8n:5678" "n8n"
|
||||
|
||||
write_readme "$N8N_DIR" << 'MD'
|
||||
# n8n
|
||||
|
||||
Workflow automation platform — connect all your self-hosted services with
|
||||
a visual editor. Create webhooks, scheduled jobs, and multi-step automations.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:5678
|
||||
- On first run, n8n prompts you to create an owner account.
|
||||
|
||||
## Manage
|
||||
```bash
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose down && docker compose up -d # update
|
||||
```
|
||||
|
||||
## Environment
|
||||
Edit `.env` to change `WEBHOOK_URL` or `N8N_HOST` after deployment,
|
||||
then restart: `docker compose down && docker compose up -d`
|
||||
MD
|
||||
|
||||
local START_N8N=""
|
||||
prompt_yn "Start n8n now? (y/n):" "y" START_N8N
|
||||
if [ "$START_N8N" = "y" ] || [ "$START_N8N" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "n8n started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:5678"
|
||||
echo " Create your owner account on first visit."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_n8n
|
||||
@@ -235,6 +235,28 @@ CADDY_NET=$SITE_CADDY_NET
|
||||
NTFY_ENV
|
||||
|
||||
mkdir -p cache config
|
||||
|
||||
# Write server.yml — ntfy needs base-url for correct push notification links
|
||||
# Only on fresh install; never clobber existing config
|
||||
if [ ! -f config/server.yml ]; then
|
||||
local NTFY_BASE_URL=""
|
||||
if [ -n "$SITE_DOMAIN" ] && [ "$SITE_DOMAIN" != "example.com" ]; then
|
||||
NTFY_BASE_URL="https://ntfy.${SITE_DOMAIN}"
|
||||
fi
|
||||
cat > config/server.yml << NTFY_CFG
|
||||
# ntfy server configuration — https://docs.ntfy.sh/config/
|
||||
base-url: "${NTFY_BASE_URL:-https://ntfy.example.com}" # UPDATE to your actual domain
|
||||
cache-file: /var/cache/ntfy/cache.db
|
||||
cache-duration: 12h
|
||||
auth-file: /var/cache/ntfy/auth.db
|
||||
auth-default-access: deny-all
|
||||
behind-proxy: true
|
||||
NTFY_CFG
|
||||
[[ -n "$NTFY_BASE_URL" ]] \
|
||||
&& log_info "base-url set to $NTFY_BASE_URL — update if domain changes" \
|
||||
|| log_warning "base-url set to placeholder — edit config/server.yml after install"
|
||||
fi
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR"
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
#!/bin/bash
|
||||
# services/stirling-pdf.sh — PDF toolkit — merge, split, compress, OCR (Stirling PDF).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash stirling-pdf.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
source "$_COMMON"
|
||||
else
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
local _display_port="${_upstream##*:}"
|
||||
|
||||
# Determine mode: local Caddy, remote Caddy, or none
|
||||
local _mode="none"
|
||||
[[ -d "$_caddy_dir" ]] && _mode="local"
|
||||
[[ -n "${CADDY_REMOTE_HOST:-}" ]] && [[ "$_mode" != "local" ]] && _mode="remote"
|
||||
[[ "$_mode" == "none" ]] && {
|
||||
log_info "Access $_name directly on port $_display_port."
|
||||
return 0
|
||||
}
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
log_info "Remote Caddy configured (${CADDY_REMOTE_HOST})."
|
||||
log_info "A snippet file will be saved to ~/docker/caddy-snippets/."
|
||||
fi
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:$_display_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Domain prompt — pre-fill from SITE_DOMAIN when available
|
||||
local _default_domain=""
|
||||
if [[ -n "${SITE_DOMAIN:-}" ]] && [[ "$SITE_DOMAIN" != "example.com" ]]; then
|
||||
_default_domain="${_subdomain}.${SITE_DOMAIN}"
|
||||
log_info "Default: $_default_domain"
|
||||
fi
|
||||
local _domain=""
|
||||
read -r -p " Domain [${_default_domain:-required}]: " _domain
|
||||
_domain="${_domain:-$_default_domain}"
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Build upstream — remote Caddy uses host IP:port, not container name
|
||||
local _block_upstream="$_upstream"
|
||||
if [[ "$_mode" == "remote" ]]; then
|
||||
_block_upstream="${CADDY_REMOTE_HOST}:${_display_port}"
|
||||
fi
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# $_name
|
||||
${_domain} {
|
||||
reverse_proxy ${_block_upstream}
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
)"
|
||||
|
||||
if [[ "$_mode" == "local" ]]; then
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$_site_block" >> "$_caddyfile"
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/${_subdomain}.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
log_info " rsync -av $_snippet_dir/ caddy-host:~/caddy-snippets/ (all at once)"
|
||||
fi
|
||||
}
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
||||
|
||||
register_service() { :; }
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service stirling-pdf utilities "PDF toolkit — merge, split, compress, OCR (Stirling PDF)" 8070
|
||||
|
||||
install_stirling_pdf() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Stirling PDF..."
|
||||
|
||||
local PDF_DIR="$DOCKER_DIR/stirling-pdf"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $PDF_DIR"
|
||||
echo "[DRY-RUN] Would write docker-compose.yml and .env"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$PDF_DIR"
|
||||
ensure_docker_dir_ownership "$PDF_DIR"
|
||||
cd "$PDF_DIR" || return 1
|
||||
|
||||
cat > docker-compose.yml << 'PDF_COMPOSE'
|
||||
name: stirling-pdf
|
||||
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: frooodle/s-pdf:latest
|
||||
container_name: stirling-pdf
|
||||
hostname: stirling-pdf
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8070:8080"
|
||||
volumes:
|
||||
- ./training-data:/usr/share/tessdata
|
||||
- ./extraConfigs:/configs
|
||||
- ./logs:/logs
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
PDF_COMPOSE
|
||||
|
||||
cat > .env << PDF_ENV
|
||||
# Stirling PDF configuration
|
||||
|
||||
# Set to true to enable login/user management (requires restart)
|
||||
DOCKER_ENABLE_SECURITY=false
|
||||
|
||||
# Set to true to install LibreOffice for advanced HTML/book conversion ops
|
||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||
|
||||
# Caddy network
|
||||
CADDY_NET=${SITE_CADDY_NET}
|
||||
PDF_ENV
|
||||
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$PDF_DIR"
|
||||
|
||||
echo ""
|
||||
log_success "Stirling PDF configured at $PDF_DIR"
|
||||
log_info "Security/login is disabled by default (DOCKER_ENABLE_SECURITY=false)."
|
||||
log_info "To enable built-in auth, set DOCKER_ENABLE_SECURITY=true in .env and restart."
|
||||
|
||||
# No built-in auth by default — offer Authelia SSO protection
|
||||
local EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect Stirling PDF with Authelia SSO? (y/n):" "y" _use_auth
|
||||
[[ "$_use_auth" =~ ^[Yy]$ ]] && EXTRA_BLOCK=" import authelia"
|
||||
fi
|
||||
|
||||
configure_caddy_for_service "Stirling PDF" "stirling-pdf:8080" "pdf" "$EXTRA_BLOCK"
|
||||
|
||||
write_readme "$PDF_DIR" << MD
|
||||
# Stirling PDF
|
||||
|
||||
Feature-rich PDF toolkit: merge, split, compress, rotate, OCR, convert, and more.
|
||||
|
||||
## Access
|
||||
- URL: http://localhost:8070
|
||||
- No login required by default (security disabled)
|
||||
|
||||
## Enabling built-in auth
|
||||
Set \`DOCKER_ENABLE_SECURITY=true\` in \`.env\`, then restart:
|
||||
\`\`\`bash
|
||||
cd $PDF_DIR
|
||||
docker compose down && docker compose up -d
|
||||
\`\`\`
|
||||
|
||||
## OCR / Tesseract
|
||||
Additional Tesseract language packs can be placed in \`./training-data/\`.
|
||||
See: https://github.com/Frooodle/Stirling-PDF#ocr
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $PDF_DIR
|
||||
docker compose up -d # start
|
||||
docker compose down # stop
|
||||
docker compose logs -f # logs
|
||||
docker compose pull && docker compose down && docker compose up -d # update
|
||||
\`\`\`
|
||||
|
||||
## Files
|
||||
- docker-compose.yml — stack definition
|
||||
- .env — configuration flags
|
||||
- training-data/ — Tesseract OCR language data
|
||||
- extraConfigs/ — optional extra config files
|
||||
- logs/ — application logs
|
||||
MD
|
||||
|
||||
local START_PDF=""
|
||||
prompt_yn "Start Stirling PDF now? (y/n):" "y" START_PDF
|
||||
if [ "$START_PDF" = "y" ] || [ "$START_PDF" = "Y" ]; then
|
||||
docker compose up -d 2>/dev/null \
|
||||
&& log_success "Stirling PDF started" \
|
||||
|| log_warning "Failed to start — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:8070"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_stirling_pdf
|
||||
@@ -0,0 +1,258 @@
|
||||
#!/bin/bash
|
||||
# services/syncthing.sh — Continuous file sync between devices (Syncthing).
|
||||
# Part of the modular post-install system (sourced by setup.sh).
|
||||
#
|
||||
# Can also be run standalone on any machine:
|
||||
# sudo bash syncthing.sh
|
||||
# (Docker must already be installed when run standalone)
|
||||
|
||||
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_COMMON="$_SELF_DIR/../lib/common.sh"
|
||||
|
||||
if [[ -f "$_COMMON" ]]; then
|
||||
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
||||
# shellcheck source=../lib/common.sh
|
||||
source "$_COMMON"
|
||||
else
|
||||
# One-off copy — inline minimal stubs so the script works without the repo
|
||||
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
||||
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
require_docker() {
|
||||
command -v docker &>/dev/null || {
|
||||
log_error "Docker not found. Install it first:"
|
||||
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
||||
return 1
|
||||
}
|
||||
docker compose version &>/dev/null || {
|
||||
log_error "Docker Compose plugin missing:"
|
||||
log_error " sudo apt-get install -y docker-compose-plugin"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ensure_docker_dir_ownership() {
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
||||
prompt_text() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
prompt_yn() {
|
||||
local _q="$1" _def="$2" _var="$3" _r
|
||||
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
||||
read -r -p " $_q " _r
|
||||
eval "$_var='${_r:-$_def}'"
|
||||
}
|
||||
|
||||
configure_caddy_for_service() {
|
||||
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
|
||||
local _caddy_dir="$DOCKER_DIR/caddy"
|
||||
local _caddyfile="$_caddy_dir/Caddyfile"
|
||||
|
||||
if [[ ! -d "$_caddy_dir" ]]; then
|
||||
log_info "Access $_name directly on port ${_upstream##*:}."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local _do_caddy=""
|
||||
read -r -p " Configure Caddy reverse proxy for $_name? [y/N]: " _do_caddy
|
||||
[[ "${_do_caddy,,}" == "y" ]] || {
|
||||
log_info "Skipping — access at: http://localhost:${_upstream##*:}"
|
||||
return 0
|
||||
}
|
||||
|
||||
local _domain=""
|
||||
read -r -p " Domain (e.g. ${_subdomain}.${SITE_DOMAIN:-example.com}): " _domain
|
||||
[[ -n "$_domain" ]] || { log_warning "No domain entered — skipping Caddy."; return 0; }
|
||||
|
||||
# Back up before touching
|
||||
if [[ -f "$_caddyfile" ]]; then
|
||||
local _bk="$_caddy_dir/Caddyfile.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$_caddyfile" "$_bk"
|
||||
log_info "Backed up Caddyfile to $(basename "$_bk")"
|
||||
else
|
||||
touch "$_caddyfile"
|
||||
fi
|
||||
|
||||
# Remove existing block for this domain if present
|
||||
if grep -q "^${_domain}" "$_caddyfile" 2>/dev/null; then
|
||||
log_warning "$_domain already in Caddyfile"
|
||||
local _ow=""
|
||||
read -r -p " Overwrite? [y/N]: " _ow
|
||||
[[ "${_ow,,}" == "y" ]] || { log_info "Keeping existing entry."; return 0; }
|
||||
sed -i "/^${_domain}/,/^}/d" "$_caddyfile"
|
||||
fi
|
||||
|
||||
cat >> "$_caddyfile" << CBLOCK
|
||||
|
||||
# $_name
|
||||
$_domain {
|
||||
reverse_proxy $_upstream
|
||||
|
||||
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/${_domain}.log
|
||||
format json
|
||||
}
|
||||
${_extra}
|
||||
}
|
||||
CBLOCK
|
||||
|
||||
log_success "Added $_domain to Caddyfile"
|
||||
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile 2>/dev/null || true
|
||||
if docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
log_success "$_name accessible at: https://$_domain"
|
||||
else
|
||||
log_warning "Reload failed — check: docker logs caddy"
|
||||
log_info "Manual reload: docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
||||
fi
|
||||
}
|
||||
|
||||
write_readme() {
|
||||
local _dir="$1"; shift
|
||||
mkdir -p "$_dir"
|
||||
cat > "$_dir/README.md"
|
||||
}
|
||||
fi
|
||||
|
||||
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
||||
# ($HOME under sudo is /root, not the real user's home)
|
||||
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
||||
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
UNATTENDED="${UNATTENDED:-false}"
|
||||
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
||||
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
||||
|
||||
register_service() { :; } # no-op — no wizard to register into
|
||||
_RUN_STANDALONE=1
|
||||
fi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
register_service syncthing utilities "Continuous file sync between devices (Syncthing)" 8384
|
||||
|
||||
install_syncthing() {
|
||||
require_docker || return 1
|
||||
log_info "Installing Syncthing..."
|
||||
|
||||
local DIR="$DOCKER_DIR/syncthing"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo "[DRY-RUN] Would create $DIR with docker-compose.yml and .env"
|
||||
echo "[DRY-RUN] Would expose web UI on 8384, sync protocol on 22000 (tcp+udp), discovery on 21027/udp"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DIR"
|
||||
ensure_docker_dir_ownership "$DIR"
|
||||
cd "$DIR" || return 1
|
||||
|
||||
local PUID PGID
|
||||
PUID="$(id -u "$ACTUAL_USER")"
|
||||
PGID="$(id -g "$ACTUAL_USER")"
|
||||
|
||||
cat > docker-compose.yml << EOF
|
||||
name: syncthing
|
||||
|
||||
services:
|
||||
syncthing:
|
||||
image: syncthing/syncthing:latest
|
||||
container_name: syncthing
|
||||
hostname: syncthing
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=\${PUID:-$PUID}
|
||||
- PGID=\${PGID:-$PGID}
|
||||
- TZ=\${SITE_TZ:-UTC}
|
||||
ports:
|
||||
- "8384:8384"
|
||||
- "22000:22000/tcp"
|
||||
- "22000:22000/udp"
|
||||
- "21027:21027/udp"
|
||||
volumes:
|
||||
- ./config:/var/syncthing/config
|
||||
- ./data:/var/syncthing
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: \${CADDY_NET:-caddy_net}
|
||||
EOF
|
||||
|
||||
cat > .env << ENV
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
PUID=$PUID
|
||||
PGID=$PGID
|
||||
SITE_TZ=$SITE_TZ
|
||||
ENV
|
||||
|
||||
chmod 600 .env
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" .env
|
||||
|
||||
configure_caddy_for_service "Syncthing" "syncthing:8384" "sync"
|
||||
|
||||
write_readme "$DIR" << MD
|
||||
# Syncthing
|
||||
|
||||
Continuous, decentralised file synchronisation between devices.
|
||||
|
||||
## Access
|
||||
- Web UI: http://localhost:8384
|
||||
- First run: go to **Settings → GUI** and set a username and password.
|
||||
|
||||
## Firewall ports (for LAN sync)
|
||||
Open these on the host firewall so other Syncthing devices can reach this node:
|
||||
\`\`\`
|
||||
sudo ufw allow 22000/tcp comment "Syncthing sync protocol"
|
||||
sudo ufw allow 22000/udp comment "Syncthing sync protocol (QUIC)"
|
||||
sudo ufw allow 21027/udp comment "Syncthing local discovery"
|
||||
\`\`\`
|
||||
|
||||
## Manage
|
||||
\`\`\`
|
||||
cd $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=""
|
||||
prompt_yn "Start Syncthing now? (y/n):" "y" START
|
||||
if [ "$START" = "y" ] || [ "$START" = "Y" ]; then
|
||||
docker compose up -d \
|
||||
&& log_success "Syncthing started" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
echo " Access at: http://localhost:8384"
|
||||
echo " First run: set a username and password in Settings → GUI"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run immediately when executed directly (deferred until after function definition)
|
||||
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_syncthing
|
||||
+41
-15
@@ -202,25 +202,36 @@ UNIFI_ENV
|
||||
|
||||
log_success "UniFi configured at $UNIFI_DIR"
|
||||
|
||||
# ── Optional Caddy reverse proxy (HTTPS backend requires special config) ──
|
||||
if [ -d "$DOCKER_DIR/caddy" ]; then
|
||||
# ── Optional Caddy reverse proxy (HTTPS backend requires tls_insecure_skip_verify) ──
|
||||
local _caddy_mode="none"
|
||||
[ -d "$DOCKER_DIR/caddy" ] && _caddy_mode="local"
|
||||
[ -n "${CADDY_REMOTE_HOST:-}" ] && [ "$_caddy_mode" != "local" ] && _caddy_mode="remote"
|
||||
|
||||
if [ "$_caddy_mode" != "none" ]; 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 " Caddy proxies it using tls_insecure_skip_verify."
|
||||
if [ "$_caddy_mode" = "remote" ]; then
|
||||
echo " Remote Caddy (${CADDY_REMOTE_HOST}) — a snippet file will be saved."
|
||||
fi
|
||||
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
|
||||
local _def_domain="unifi.${SITE_DOMAIN:-example.com}"
|
||||
prompt_text "UniFi domain [${_def_domain}]:" "$_def_domain" 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 uses HTTPS internally — upstream must use https:// + skip verify
|
||||
local _upstream="https://unifi-app:8443"
|
||||
[ "$_caddy_mode" = "remote" ] && _upstream="https://${CADDY_REMOTE_HOST}:8443"
|
||||
|
||||
local _site_block
|
||||
_site_block="$(cat << CBLOCK
|
||||
|
||||
# UniFi Network Application
|
||||
$UNIFI_DOMAIN {
|
||||
reverse_proxy https://unifi-app:8443 {
|
||||
${UNIFI_DOMAIN} {
|
||||
reverse_proxy ${_upstream} {
|
||||
transport http {
|
||||
tls_insecure_skip_verify
|
||||
}
|
||||
@@ -234,15 +245,30 @@ $UNIFI_DOMAIN {
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/$UNIFI_DOMAIN.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"
|
||||
CBLOCK
|
||||
)"
|
||||
if [ "$_caddy_mode" = "local" ]; then
|
||||
local CADDYFILE="$DOCKER_DIR/caddy/Caddyfile"
|
||||
cp "$CADDYFILE" "$CADDYFILE.backup.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
|
||||
printf '%s\n' "$_site_block" >> "$CADDYFILE"
|
||||
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"
|
||||
else
|
||||
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
|
||||
local _snippet_file="$_snippet_dir/unifi.caddy"
|
||||
mkdir -p "$_snippet_dir"
|
||||
printf '%s\n' "$_site_block" > "$_snippet_file"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
|
||||
log_success "Snippet saved: $_snippet_file"
|
||||
log_info "Copy to Caddy machine:"
|
||||
log_info " scp $_snippet_file caddy-host:~/caddy-snippets/"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -254,10 +254,8 @@ services:
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./vaultwarden_data:/data
|
||||
expose:
|
||||
- "80"
|
||||
ports:
|
||||
- "3012:3012" # WebSocket (legacy — not needed for Vaultwarden v1.29+)
|
||||
- "8888:80"
|
||||
networks:
|
||||
- caddy_net
|
||||
|
||||
@@ -284,8 +282,6 @@ ADMIN_TOKEN=$ADMIN_TOKEN
|
||||
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
|
||||
|
||||
+21
-5
@@ -215,13 +215,26 @@ install_wg-easy() {
|
||||
cd "$WGEASY_DIR" || return 1
|
||||
|
||||
# Auto-detect public IP as default for WG_HOST
|
||||
local PUBLIC_IP WG_HOST WG_PASSWORD
|
||||
local PUBLIC_IP WG_HOST WG_PASSWORD WG_PASSWORD_HASH
|
||||
PUBLIC_IP=$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null || echo "your-public-ip")
|
||||
WG_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)
|
||||
|
||||
prompt_text "Public IP or hostname for VPN [$PUBLIC_IP]:" "$PUBLIC_IP" WG_HOST
|
||||
|
||||
cat > docker-compose.yml << 'WGEASY_COMPOSE'
|
||||
# wg-easy v14+ requires PASSWORD_HASH (bcrypt). Generate via docker.
|
||||
log_info "Generating bcrypt password hash (requires Docker)..."
|
||||
WG_PASSWORD_HASH=$(docker run --rm ghcr.io/wg-easy/wg-easy:latest wgpw "$WG_PASSWORD" 2>/dev/null \
|
||||
| grep -oP '\$2[ab]\$[^\s]+' | head -1)
|
||||
if [[ -z "$WG_PASSWORD_HASH" ]]; then
|
||||
log_warning "Could not generate bcrypt hash — falling back to plaintext PASSWORD env var."
|
||||
log_warning "If wg-easy fails to start, run: docker run --rm ghcr.io/wg-easy/wg-easy wgpw 'yourpassword'"
|
||||
log_warning "Then set PASSWORD_HASH in docker-compose.yml and remove PASSWORD."
|
||||
fi
|
||||
|
||||
# Escape $ in hash for docker-compose env (bcrypt hashes contain $$)
|
||||
local WG_HASH_ESCAPED="${WG_PASSWORD_HASH//\$/\$\$}"
|
||||
|
||||
cat > docker-compose.yml << WGEASY_COMPOSE
|
||||
name: wg-easy
|
||||
|
||||
services:
|
||||
@@ -237,8 +250,8 @@ services:
|
||||
- net.ipv4.ip_forward=1
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
environment:
|
||||
- WG_HOST=${WG_HOST}
|
||||
- PASSWORD=${WG_PASSWORD}
|
||||
- WG_HOST=\${WG_HOST}
|
||||
- PASSWORD_HASH=${WG_HASH_ESCAPED:-\${WG_PASSWORD}}
|
||||
- WG_DEFAULT_DNS=1.1.1.1
|
||||
volumes:
|
||||
- ./config:/etc/wireguard
|
||||
@@ -251,11 +264,12 @@ services:
|
||||
networks:
|
||||
caddy_net:
|
||||
external: true
|
||||
name: ${CADDY_NET:-caddy_net}
|
||||
name: \${CADDY_NET:-caddy_net}
|
||||
WGEASY_COMPOSE
|
||||
|
||||
cat > .env << WGEASY_ENV
|
||||
WG_HOST=$WG_HOST
|
||||
# Plain-text password — used only if PASSWORD_HASH could not be generated above
|
||||
WG_PASSWORD=$WG_PASSWORD
|
||||
CADDY_NET=$SITE_CADDY_NET
|
||||
WGEASY_ENV
|
||||
@@ -304,6 +318,8 @@ MD
|
||||
echo ""
|
||||
echo " Web UI: http://localhost:51821"
|
||||
echo " Password: $WG_PASSWORD (saved in .env)"
|
||||
[[ -n "$WG_PASSWORD_HASH" ]] && echo " Auth: bcrypt hash configured (v14+ compatible)" \
|
||||
|| echo " Auth: WARNING — bcrypt hash generation failed; see README"
|
||||
echo " Router: forward UDP 51820 → this server for external VPN access"
|
||||
echo ""
|
||||
}
|
||||
|
||||
@@ -433,7 +433,13 @@ COMPOSE
|
||||
fi
|
||||
|
||||
# ── 5. Caddy (optional) ───────────────────────────────────────────────────
|
||||
configure_caddy_for_service "wolf-pair" "$WOLFPAIR_PORT" "wolf-pair"
|
||||
local WOLFPAIR_EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
prompt_yn "Protect wolf-pair with Authelia SSO? (y/n):" "y" _use_auth
|
||||
[[ "$_use_auth" =~ ^[Yy]$ ]] && WOLFPAIR_EXTRA_BLOCK=" import authelia"
|
||||
fi
|
||||
configure_caddy_for_service "wolf-pair" "$WOLFPAIR_PORT" "wolf-pair" "$WOLFPAIR_EXTRA_BLOCK"
|
||||
|
||||
# ── 6. README ─────────────────────────────────────────────────────────────
|
||||
write_readme "$WOLFPAIR_DIR" << 'MD'
|
||||
|
||||
+34
-12
@@ -1046,7 +1046,7 @@ PYEOF
|
||||
echo " Set up automatic backups with the backup module:"
|
||||
echo " sudo ./setup.sh backup"
|
||||
echo ""
|
||||
# ── Caddy reverse proxy (Wolf web UI — no built-in auth, offer Authelia) ────
|
||||
# Wolf's web UI (pair/manage) has no built-in auth — protect with Authelia if available
|
||||
local WOLF_EXTRA_BLOCK=""
|
||||
if [ -d "$DOCKER_DIR/authelia" ]; then
|
||||
local _use_auth=""
|
||||
@@ -1060,32 +1060,54 @@ PYEOF
|
||||
|
||||
Cloud gaming via Moonlight. Stream any Moonlight-compatible game or app from
|
||||
this server to any device on your network.
|
||||
# Wolf — Cloud Gaming (Games-on-Whales)
|
||||
|
||||
Stream games to any Moonlight client over your LAN or Tailscale VPN.
|
||||
|
||||
## Pair a new client
|
||||
1. Open Moonlight on the client device
|
||||
2. Add host: this server's IP
|
||||
3. Run the pin command on the server:
|
||||
\`\`\`bash
|
||||
cd $WOLF_DIR && ./manage.sh pin
|
||||
\`\`\`
|
||||
|
||||
## Manage
|
||||
\`\`\`bash
|
||||
cd $WOLF_DIR
|
||||
./manage.sh start # start Wolf
|
||||
./manage.sh stop # stop Wolf
|
||||
./manage.sh logs # stream logs
|
||||
./manage.sh pin # show pairing PIN
|
||||
./manage.sh update # pull latest and restart
|
||||
./manage.sh start # start Wolf
|
||||
./manage.sh stop # stop
|
||||
./manage.sh restart # restart
|
||||
./manage.sh logs # live logs
|
||||
./manage.sh status # container status
|
||||
./manage.sh update # pull latest image and restart
|
||||
./manage.sh add-apps # add game launchers
|
||||
\`\`\`
|
||||
|
||||
## Pairing
|
||||
1. Open Moonlight on your device, select this server
|
||||
2. Run \`./manage.sh pin\` to get the pairing PIN
|
||||
3. Enter the PIN in Moonlight
|
||||
## Ports (open on firewall / router)
|
||||
| Port(s) | Protocol | Use |
|
||||
|---------|----------|-----|
|
||||
| 47984–47990 | TCP | Moonlight control |
|
||||
| 48010 | TCP | RTSP |
|
||||
| 47998–48000 | UDP | RTP video/audio/control |
|
||||
|
||||
## Game storage: \`$GAME_STORAGE_DIR\`
|
||||
- \`roms/\` → /ROMs (EmulationStation)
|
||||
- \`steam/\` → Steam data
|
||||
- \`saves/\` → RetroArch saves
|
||||
|
||||
## Backup
|
||||
\`\`\`bash
|
||||
sudo ./setup.sh backup # covers /etc/wolf saves and ES-DE settings
|
||||
\`\`\`
|
||||
MD
|
||||
|
||||
local START_WOLF=""
|
||||
prompt_yn "Start Wolf now? (y/n):" "y" START_WOLF
|
||||
if [ "$START_WOLF" = "y" ] || [ "$START_WOLF" = "Y" ]; then
|
||||
docker compose up -d && log_success "Wolf started" || log_warning "Failed to start — check: docker compose logs"
|
||||
if [[ "$START_WOLF" =~ ^[Yy]$ ]]; then
|
||||
docker compose up -d \
|
||||
&& log_success "Wolf started — pair Moonlight to this server's IP" \
|
||||
|| log_warning "Start failed — check: docker compose logs"
|
||||
fi
|
||||
|
||||
log_success "Done. Pair Moonlight and play."
|
||||
|
||||
@@ -79,6 +79,7 @@ is_installed() {
|
||||
base) command -v ncdu >/dev/null 2>&1 ;;
|
||||
glow) command -v glow >/dev/null 2>&1 ;;
|
||||
crowdsec) command -v cscli >/dev/null 2>&1 ;;
|
||||
kdeconnect) command -v kdeconnect >/dev/null 2>&1 ;;
|
||||
silent-send) [ -d "$ACTUAL_HOME/silent-send/.git" ] ;;
|
||||
sync-cc) [ -f "$ACTUAL_HOME/sync-cc/sync_cc.py" ] ;;
|
||||
sky-cam) [ -d "$ACTUAL_HOME/sky-cam/.git" ] ;;
|
||||
|
||||
Reference in New Issue
Block a user