From 4a37b3622d5bf71039cfa6dd761bdd66004c90b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 17:01:14 +0000 Subject: [PATCH] v0.9.6: README generation + migrate authelia + 5 services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/common.sh: add write_readme helper. Every module now writes a README.md into its ~/docker// folder (self-documenting service folders). - services/authelia.sh: SSO + 2FA portal, ported from the authelia-setup repo + the monolith's working block (secrets + Argon2 hash generation, caddy_net, Caddyfile forward-auth snippet + portal block, README). Guards against clobbering an existing install. - services/{filebrowser,ntfy,uptimekuma,portainer,watchtower}.sh: mechanical migrations from the monolith, each with a README. Ports 8085/8090/3001/9443/—. All pass bash -n; ./setup.sh --list shows them under homelab; dry-run run-one exits 0 for each with real commands guarded. https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK --- CHANGELOG.md | 25 ++++ VERSION | 2 +- lib/common.sh | 20 +++ services/authelia.sh | 315 ++++++++++++++++++++++++++++++++++++++++ services/filebrowser.sh | 99 +++++++++++++ services/ntfy.sh | 87 +++++++++++ services/portainer.sh | 76 ++++++++++ services/uptimekuma.sh | 81 +++++++++++ services/watchtower.sh | 162 +++++++++++++++++++++ 9 files changed, 866 insertions(+), 1 deletion(-) create mode 100644 services/authelia.sh create mode 100644 services/filebrowser.sh create mode 100644 services/ntfy.sh create mode 100644 services/portainer.sh create mode 100644 services/uptimekuma.sh create mode 100644 services/watchtower.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d59bfb..9447615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to this project. Versions follow `MAJOR.MINOR.PATCH`. The project is pre-1.0 while the modular system reaches parity with the monolithic `ubuntu-post-install-*.sh` scripts. +## [0.9.6] - 2026-06-03 + +### Added +- **Per-service README generation.** New `write_readme` helper in + `lib/common.sh`; every module now writes a `README.md` into its + `~/docker//` folder (what it is, access URL, start/stop, data + location, reverse-proxy notes) — so each service folder is self-documenting. +- Migrated 6 services from the monolith into modules (all in the `homelab` + group, each with a README): + - `authelia` — SSO + 2FA portal, ported from the `authelia-setup` repo + the + monolith's working block: prompts for domain/SMTP, generates + jwt/session/storage secrets + the admin Argon2 hash, writes + compose/config/users, creates `caddy_net`, and injects the forward-auth + snippet + portal block into the Caddyfile. Won't clobber an existing install. + - `filebrowser` (8085), `ntfy` (8090), `uptimekuma` (3001), + `portainer` (9443), `watchtower` (no web port). + +### Notes +- `homelab` group now: authelia, filebrowser, homeassistant, ntfy, portainer, + uptimekuma, watchtower. +- Remaining monolith services still to migrate: ActualBudget, ARM, + AudioBookshelf, Caddy, CrowdSec, Emby, FindMyDevice, Frigate, Frigate-Notify, + Immich, Jellyfin, Lyrion, MagicMirror, Mealie, MeshCentral, Traccar, ddclient, + wg-easy. + ## [0.9.5] - 2026-06-03 ### Added diff --git a/VERSION b/VERSION index b0bb878..85b7c69 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.5 +0.9.6 diff --git a/lib/common.sh b/lib/common.sh index e6bd534..9f24c6c 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -121,6 +121,26 @@ prompt_text() { eval "$varname='${response:-$default}'" } +# ── Per-service README generation ──────────────────────────────────────────── +# Write /README.md from stdin (markdown). Every module is encouraged to +# call this so each ~/docker// folder is self-documenting. +# Usage: +# write_readme "$DIR" </dev/null # consume the heredoc so the caller isn't blocked + echo "[DRY-RUN] Would write $dir/README.md" + return 0 + fi + mkdir -p "$dir" + cat > "$dir/README.md" + chown "$ACTUAL_USER:$ACTUAL_USER" "$dir/README.md" 2>/dev/null || true +} + # ── Caddy reverse-proxy wiring (shared by every web service) ───────────────── # Usage: configure_caddy_for_service "Name" "PORT" "default-subdomain" ["extra"] configure_caddy_for_service() { diff --git a/services/authelia.sh b/services/authelia.sh new file mode 100644 index 0000000..9a57e56 --- /dev/null +++ b/services/authelia.sh @@ -0,0 +1,315 @@ +#!/bin/bash +# services/authelia.sh — Authelia SSO + 2FA portal (forward-auth for Caddy). +# Ported from the authelia-setup repo / the monolith's working block. +# Part of the modular post-install system (sourced by setup.sh). + +register_service authelia homelab "SSO + 2FA auth portal (Authelia)" 9091 + +install_authelia() { + require_docker || return 1 + local AUTHELIA_DIR="$DOCKER_DIR/authelia" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would set up Authelia:" + echo " • Create $AUTHELIA_DIR (config/secrets, data)" + echo " • Generate jwt/session/storage secrets + admin password hash" + echo " • Write docker-compose.yml, configuration.yml, users.yml, README.md" + echo " • Create the caddy_net network and add the forward-auth snippet to the Caddyfile" + return 0 + fi + + # Don't clobber an existing install (it would regenerate secrets and break sessions). + if [ -f "$AUTHELIA_DIR/docker-compose.yml" ]; then + local RECONF="" + echo " ⚠ Authelia already exists at $AUTHELIA_DIR (secrets/users would be regenerated)." + prompt_yn " Reconfigure from scratch? (y/n):" "n" RECONF + if [ "$RECONF" != "y" ] && [ "$RECONF" != "Y" ]; then + echo " Keeping existing Authelia. (Edit config/users.yml then: cd $AUTHELIA_DIR && docker compose restart authelia)" + return 0 + fi + fi + + log_info "Installing Authelia..." + mkdir -p "$AUTHELIA_DIR/config/secrets" "$AUTHELIA_DIR/data" + + # ── Collect configuration ──────────────────────────────────────────────── + echo "" + echo " Authelia needs a few details to configure." + echo "" + local AUTHELIA_DOMAIN AUTHELIA_ADMIN_USER AUTHELIA_ADMIN_DISPLAY AUTHELIA_ADMIN_EMAIL + local AUTHELIA_SMTP_HOST AUTHELIA_SMTP_PORT AUTHELIA_SMTP_USER AUTHELIA_SMTP_PASS AUTHELIA_TZ + prompt_text " Your domain (e.g., example.com):" "example.com" AUTHELIA_DOMAIN + prompt_text " Admin username:" "admin" AUTHELIA_ADMIN_USER + prompt_text " Admin display name:" "Administrator" AUTHELIA_ADMIN_DISPLAY + prompt_text " Admin email:" "admin@${AUTHELIA_DOMAIN}" AUTHELIA_ADMIN_EMAIL + prompt_text " SMTP server (e.g., smtp.migadu.com):" "smtp.migadu.com" AUTHELIA_SMTP_HOST + prompt_text " SMTP port:" "587" AUTHELIA_SMTP_PORT + prompt_text " SMTP username (full email):" "authelia@${AUTHELIA_DOMAIN}" AUTHELIA_SMTP_USER + prompt_text " SMTP password:" "" AUTHELIA_SMTP_PASS + prompt_text " Timezone (e.g., America/New_York):" "America/New_York" AUTHELIA_TZ + + # ── Secrets ────────────────────────────────────────────────────────────── + echo "" + echo " Generating secrets..." + echo "$(openssl rand -hex 32)" > "$AUTHELIA_DIR/config/secrets/jwt_secret" + echo "$(openssl rand -hex 32)" > "$AUTHELIA_DIR/config/secrets/session_secret" + echo "$(openssl rand -hex 32)" > "$AUTHELIA_DIR/config/secrets/storage_secret" + echo "$AUTHELIA_SMTP_PASS" > "$AUTHELIA_DIR/config/secrets/smtp_password" + chmod 600 "$AUTHELIA_DIR/config/secrets/"* + echo " ✓ Secrets generated" + + # ── Admin password hash ────────────────────────────────────────────────── + echo "" + local AUTHELIA_TEMP_PASS AUTHELIA_HASH + prompt_text " Temporary password for admin (users reset via email):" "TempPass2026!" AUTHELIA_TEMP_PASS + echo " Generating password hash..." + AUTHELIA_HASH=$(docker run --rm authelia/authelia:4.39.20 \ + authelia crypto hash generate argon2 --password "$AUTHELIA_TEMP_PASS" 2>/dev/null \ + | grep -oP '(?<=Digest: ).*' || echo "REPLACE_WITH_HASH") + if [ "$AUTHELIA_HASH" = "REPLACE_WITH_HASH" ]; then + log_warning "Could not generate hash automatically. After install run:" + echo " docker run --rm authelia/authelia:4.39.20 authelia crypto hash generate argon2 --password 'yourpassword'" + echo " then update $AUTHELIA_DIR/config/users.yml" + else + echo " ✓ Password hash generated" + fi + + ensure_docker_dir_ownership "$AUTHELIA_DIR" + cd "$AUTHELIA_DIR" || return 1 + + # ── .env ───────────────────────────────────────────────────────────────── + cat > .env << AUTHELIA_ENV +MY_DOMAIN=${AUTHELIA_DOMAIN} +SMTP_USER=${AUTHELIA_SMTP_USER} +DOCKER_MY_NETWORK=caddy_net +TZ=${AUTHELIA_TZ} +AUTHELIA_ENV + + # ── docker-compose.yml (quoted heredoc: ${SMTP_USER} resolved by compose/.env) ── + cat > docker-compose.yml << 'AUTHELIA_COMPOSE' +name: authelia + +services: + authelia: + image: authelia/authelia:4.39.20 + pull_policy: missing + container_name: authelia + user: "1000:1000" + volumes: + - ./config:/config + - ./data:/data + environment: + - AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/config/secrets/jwt_secret + - AUTHELIA_SESSION_SECRET_FILE=/config/secrets/session_secret + - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/storage_secret + - AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/config/secrets/smtp_password + - AUTHELIA_NOTIFIER_SMTP_USERNAME=${SMTP_USER} + - AUTHELIA_NOTIFIER_SMTP_SENDER=Authelia <${SMTP_USER}> + expose: + - 9091 + restart: unless-stopped + networks: + - caddy_net + +networks: + caddy_net: + external: true +AUTHELIA_COMPOSE + + # ── configuration.yml ──────────────────────────────────────────────────── + cat > config/configuration.yml << AUTHELIA_CONFIG +--- +# Authelia configuration. Secrets injected via AUTHELIA_* env vars in compose. +theme: dark + +server: + address: tcp://0.0.0.0:9091 + +log: + level: info + file_path: /data/authelia.log + +totp: + period: 30 + skew: 1 + +authentication_backend: + file: + path: /config/users.yml + password: + algorithm: argon2 + argon2: + variant: argon2id + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 + +access_control: + default_policy: deny + rules: + - domain: "*.${AUTHELIA_DOMAIN}" + policy: two_factor + +session: + name: authelia_session + expiration: 12h + inactivity: 2h + remember_me: 7d + cookies: + - domain: ${AUTHELIA_DOMAIN} + authelia_url: https://auth.${AUTHELIA_DOMAIN} + default_redirection_url: https://${AUTHELIA_DOMAIN} + +storage: + local: + path: /data/db.sqlite3 + +notifier: + disable_startup_check: false + smtp: + address: smtp://${AUTHELIA_SMTP_HOST}:${AUTHELIA_SMTP_PORT} + timeout: 10s + identifier: localhost + subject: "[Authelia] {title}" + startup_check_address: ${AUTHELIA_SMTP_USER} + disable_require_tls: false + disable_starttls: false +AUTHELIA_CONFIG + + # ── users.yml ──────────────────────────────────────────────────────────── + cat > config/users.yml << AUTHELIA_USERS +--- +# Authelia users database +# Add users: copy a block, change username/email/displayname, restart authelia. +# Generate a hash: docker run --rm authelia/authelia:4.39.20 authelia crypto hash generate argon2 --password 'thepassword' +# Login with username (not email). Use "Forgot Password" to set a real password. + +users: + ${AUTHELIA_ADMIN_USER}: + displayname: "${AUTHELIA_ADMIN_DISPLAY}" + email: ${AUTHELIA_ADMIN_EMAIL} + password: "${AUTHELIA_HASH}" + groups: + - admins + - users +AUTHELIA_USERS + + chown -R 1000:1000 "$AUTHELIA_DIR/config" "$AUTHELIA_DIR/data" + log_success "Authelia configured at $AUTHELIA_DIR" + + # ── caddy_net network ──────────────────────────────────────────────────── + if ! docker network ls --format '{{.Name}}' | grep -q "^caddy_net$"; then + docker network create caddy_net >/dev/null 2>&1 && echo " ✓ Created docker network caddy_net" \ + || echo " ⚠ Failed to create caddy_net" + else + echo " ✓ Docker network caddy_net already exists" + fi + + # ── Caddyfile forward-auth snippet + portal block ──────────────────────── + local CADDY_FILE="$DOCKER_DIR/caddy/Caddyfile" + if [ -f "$CADDY_FILE" ]; then + echo " Configuring Caddy for Authelia..." + if ! grep -q "(authelia)" "$CADDY_FILE"; then + cp "$CADDY_FILE" "$CADDY_FILE.backup.$(date +%Y%m%d-%H%M%S)" + { cat << 'SNIPPET_EOF' +# ── Authelia forward auth snippet ───────────────────────────────────────────── +(authelia) { + forward_auth authelia:9091 { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Name Remote-Email + } +} + +SNIPPET_EOF + cat "$CADDY_FILE"; } > "$CADDY_FILE.tmp" && mv "$CADDY_FILE.tmp" "$CADDY_FILE" + echo " ✓ Authelia snippet added to Caddyfile" + fi + if ! grep -q "auth.${AUTHELIA_DOMAIN}" "$CADDY_FILE"; then + cat >> "$CADDY_FILE" << CADDY_AUTH_BLOCK + +# ── Authelia login portal ────────────────────────────────────────────────────── +auth.${AUTHELIA_DOMAIN} { + reverse_proxy authelia:9091 + log { + output file /var/log/caddy/auth.log + } +} +CADDY_AUTH_BLOCK + echo " ✓ Authelia portal block added for auth.${AUTHELIA_DOMAIN}" + fi + docker ps --format '{{.Names}}' | grep -q "^caddy$" && \ + { docker exec -w /etc/caddy caddy caddy reload 2>/dev/null && echo " ✓ Caddy reloaded" || echo " ⚠ Reload manually after checking the Caddyfile"; } + else + echo " ℹ Caddy not installed yet — add the (authelia) snippet + auth.${AUTHELIA_DOMAIN} block to your Caddyfile later (see README)." + fi + + # ── README for the service folder ──────────────────────────────────────── + write_readme "$AUTHELIA_DIR" << README_MD +# Authelia — SSO + 2FA portal + +Single login (with TOTP two-factor) that protects any Caddy subdomain via +forward-auth. Portal: **https://auth.${AUTHELIA_DOMAIN}** + +## Layout +\`\`\` +$AUTHELIA_DIR/ +├── docker-compose.yml +├── .env +├── config/ +│ ├── configuration.yml +│ ├── users.yml +│ └── secrets/ # jwt/session/storage/smtp — never commit +└── data/ # sqlite db + log +\`\`\` + +## Protect a service with Authelia +In that service's Caddy site block, add \`import authelia\`: +\`\`\` +myservice.${AUTHELIA_DOMAIN} { + import authelia + reverse_proxy localhost:PORT +} +\`\`\` +The \`(authelia)\` snippet and the \`auth.${AUTHELIA_DOMAIN}\` portal block were +added to \`$DOCKER_DIR/caddy/Caddyfile\` automatically. + +## Manage +\`\`\` +cd $AUTHELIA_DIR +docker compose up -d # start +docker compose restart authelia +docker compose logs -f authelia +docker compose down # stop +\`\`\` + +## Users +- Login with the **username** (not email). Admin user: \`${AUTHELIA_ADMIN_USER}\`. +- Tell users to click **Forgot Password** on first login to set their own + password (Authelia emails a reset link via SMTP). +- Add a user: copy a block in \`config/users.yml\`, change username/email/ + displayname, generate a hash, then \`docker compose restart authelia\`: +\`\`\` +docker run --rm authelia/authelia:4.39.20 authelia crypto hash generate argon2 --password 'thepassword' +\`\`\` + +## Notes +- Authelia listens on 9091 **internally only** (no published port) and is + reached through Caddy on the shared \`caddy_net\` docker network. +- Two-factor is **required** (\`default_policy: deny\`, rule \`two_factor\` for + \`*.${AUTHELIA_DOMAIN}\`). +README_MD + + local START_AUTHELIA="" + prompt_yn "Start Authelia now? (y/n):" "y" START_AUTHELIA + if [ "$START_AUTHELIA" = "y" ] || [ "$START_AUTHELIA" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "Authelia started" || log_warning "Failed to start Authelia" + fi + + echo "" + echo " Auth portal: https://auth.${AUTHELIA_DOMAIN}" + echo " Admin login: ${AUTHELIA_ADMIN_USER} (use Forgot Password to set a real password)" + echo " README: $AUTHELIA_DIR/README.md" + echo "" +} diff --git a/services/filebrowser.sh b/services/filebrowser.sh new file mode 100644 index 0000000..0145e55 --- /dev/null +++ b/services/filebrowser.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# services/filebrowser.sh — FileBrowser web-based file manager. +# Part of the modular post-install system (sourced by setup.sh). + +register_service filebrowser homelab "Web file manager (FileBrowser)" 8085 + +install_filebrowser() { + require_docker || return 1 + log_info "Installing Filebrowser..." + local FB_DIR="$DOCKER_DIR/filebrowser" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $FB_DIR" + return 0 + fi + + mkdir -p "$FB_DIR" + ensure_docker_dir_ownership "$FB_DIR" + cd "$FB_DIR" || return 1 + + local FB_PATH="" + prompt_text "Path to browse [default: $ACTUAL_HOME]:" "$ACTUAL_HOME" FB_PATH + + cat > docker-compose.yml << FB_COMPOSE +name: filebrowser + +services: + filebrowser: + image: filebrowser/filebrowser:s6 + container_name: filebrowser + hostname: filebrowser + restart: unless-stopped + environment: + - PUID=$(id -u "$ACTUAL_USER") + - PGID=$(id -g "$ACTUAL_USER") + - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + volumes: + - ${FB_PATH}:/srv + - ./database/filebrowser.db:/database/filebrowser.db + - ./config/settings.json:/config/settings.json + ports: + - "8085:80" +FB_COMPOSE + + cat > .env << FB_ENV +FB_PATH=$FB_PATH +FB_ENV + + mkdir -p database config + touch database/filebrowser.db + cat > config/settings.json << 'FB_SETTINGS' +{ + "port": 80, + "baseURL": "", + "address": "", + "log": "stdout", + "database": "/database/filebrowser.db", + "root": "/srv" +} +FB_SETTINGS + + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR" + + echo "" + log_success "Filebrowser configured at $FB_DIR" + + write_readme "$FB_DIR" << MD +# FileBrowser + +Web-based file manager. Browse, upload, and download files through a browser. + +## Access +- URL: http://localhost:8085 +- Default login: admin / admin (change immediately!) + +## Data +- Browsed path: $FB_PATH (mounted to /srv) +- Database: ./database/filebrowser.db +- Settings: ./config/settings.json + +## Manage +\`\`\` +cd $FB_DIR +docker compose up -d # start +docker compose down # stop +docker compose logs -f # logs +\`\`\` +MD + + local START_FB="" + prompt_yn "Start Filebrowser now? (y/n):" "y" START_FB + if [ "$START_FB" = "y" ] || [ "$START_FB" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "Filebrowser started" || log_warning "Failed to start" + fi + + echo " Access at: http://localhost:8085" + echo " Default login: admin / admin (change immediately!)" + echo "" +} diff --git a/services/ntfy.sh b/services/ntfy.sh new file mode 100644 index 0000000..a020528 --- /dev/null +++ b/services/ntfy.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# services/ntfy.sh — ntfy self-hosted push notification server. +# Part of the modular post-install system (sourced by setup.sh). + +register_service ntfy homelab "Self-hosted push notifications (ntfy)" 8090 + +install_ntfy() { + require_docker || return 1 + log_info "Installing ntfy..." + local NTFY_DIR="$DOCKER_DIR/ntfy" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $NTFY_DIR" + return 0 + fi + + mkdir -p "$NTFY_DIR" + ensure_docker_dir_ownership "$NTFY_DIR" + cd "$NTFY_DIR" || return 1 + + cat > docker-compose.yml << 'NTFY_COMPOSE' +name: ntfy + +services: + ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + hostname: ntfy + restart: unless-stopped + command: serve + environment: + - TZ=${TZ} + volumes: + - ./cache:/var/cache/ntfy + - ./config:/etc/ntfy + ports: + - "8090:80" +NTFY_COMPOSE + + cat > .env << NTFY_ENV +TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") +NTFY_ENV + + mkdir -p cache config + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR" + + echo "" + log_success "ntfy configured at $NTFY_DIR" + + write_readme "$NTFY_DIR" << MD +# ntfy + +Self-hosted push notification server. Send notifications from scripts to your +phone or browser. + +## Access +- URL: http://localhost:8090 + +## Usage +- Send a notification: \`curl -d "Hello!" localhost:8090/mytopic\` +- Subscribe on phone: ntfy app -> Add subscription -> localhost:8090/mytopic + +## Data +- Config: ./config (mounted to /etc/ntfy) +- Cache: ./cache (mounted to /var/cache/ntfy) + +## Manage +\`\`\` +cd $NTFY_DIR +docker compose up -d # start +docker compose down # stop +docker compose logs -f # logs +\`\`\` +MD + + local START_NTFY="" + prompt_yn "Start ntfy now? (y/n):" "y" START_NTFY + if [ "$START_NTFY" = "y" ] || [ "$START_NTFY" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "ntfy started" || log_warning "Failed to start" + fi + + echo " Access at: http://localhost:8090" + echo "" + echo " Send notification: curl -d \"Hello!\" localhost:8090/mytopic" + echo " Subscribe on phone: ntfy app → Add subscription → localhost:8090/mytopic" + echo "" +} diff --git a/services/portainer.sh b/services/portainer.sh new file mode 100644 index 0000000..7d11603 --- /dev/null +++ b/services/portainer.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# services/portainer.sh — Portainer Docker management web UI. +# Part of the modular post-install system (sourced by setup.sh). + +register_service portainer homelab "Docker management UI (Portainer)" 9443 + +install_portainer() { + require_docker || return 1 + log_info "Installing Portainer..." + local PORTAINER_DIR="$DOCKER_DIR/portainer" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $PORTAINER_DIR" + return 0 + fi + + mkdir -p "$PORTAINER_DIR" + ensure_docker_dir_ownership "$PORTAINER_DIR" + cd "$PORTAINER_DIR" || return 1 + + cat > docker-compose.yml << 'PORTAINER_COMPOSE' +name: portainer + +services: + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + hostname: portainer + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data + ports: + - "9000:9000" + - "9443:9443" +PORTAINER_COMPOSE + + mkdir -p data + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$PORTAINER_DIR" + + echo "" + log_success "Portainer configured at $PORTAINER_DIR" + + write_readme "$PORTAINER_DIR" << MD +# Portainer + +Web UI for managing Docker — containers, images, volumes, and networks. + +## Access +- HTTPS: https://localhost:9443 +- HTTP: http://localhost:9000 +- Create your admin account on first visit. + +## Data +- App data: ./data (mounted to /data) +- Mounts the Docker socket to manage the host's Docker. + +## Manage +\`\`\` +cd $PORTAINER_DIR +docker compose up -d # start +docker compose down # stop +docker compose logs -f # logs +\`\`\` +MD + + local START_PORTAINER="" + prompt_yn "Start Portainer now? (y/n):" "y" START_PORTAINER + if [ "$START_PORTAINER" = "y" ] || [ "$START_PORTAINER" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "Portainer started" || log_warning "Failed to start" + fi + + echo " Access at: https://localhost:9443" + echo " Create admin account on first visit" + echo "" +} diff --git a/services/uptimekuma.sh b/services/uptimekuma.sh new file mode 100644 index 0000000..013ab6c --- /dev/null +++ b/services/uptimekuma.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# services/uptimekuma.sh — Uptime Kuma uptime/status monitoring. +# Part of the modular post-install system (sourced by setup.sh). + +register_service uptimekuma homelab "Uptime/status monitoring (Uptime Kuma)" 3001 + +install_uptimekuma() { + require_docker || return 1 + log_info "Installing Uptime Kuma..." + local UPTIME_DIR="$DOCKER_DIR/uptime-kuma" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $UPTIME_DIR" + return 0 + fi + + mkdir -p "$UPTIME_DIR" + ensure_docker_dir_ownership "$UPTIME_DIR" + cd "$UPTIME_DIR" || return 1 + + cat > docker-compose.yml << 'UPTIME_COMPOSE' +name: uptime-kuma + +services: + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + hostname: uptime-kuma + restart: unless-stopped + volumes: + - ./data:/app/data + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - "3001:3001" +UPTIME_COMPOSE + + mkdir -p data + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$UPTIME_DIR" + + echo "" + log_success "Uptime Kuma configured at $UPTIME_DIR" + + write_readme "$UPTIME_DIR" << MD +# Uptime Kuma + +Self-hosted uptime/status monitoring dashboard. Monitor websites, servers, and +Docker containers. + +## Access +- URL: http://localhost:3001 +- Create your admin account on first visit. + +## Data +- App data: ./data (mounted to /app/data) +- Mounts the Docker socket (read-only) for container monitoring. + +## Reverse proxy +If Caddy is installed, you can expose this via the prompt during install +(see configure_caddy_for_service). Default subdomain: uptime. + +## Manage +\`\`\` +cd $UPTIME_DIR +docker compose up -d # start +docker compose down # stop +docker compose logs -f # logs +\`\`\` +MD + + # Configure Caddy reverse proxy before starting + configure_caddy_for_service "Uptime Kuma" "3001" "uptime" + + local START_UPTIME="" + prompt_yn "Start Uptime Kuma now? (y/n):" "y" START_UPTIME + if [ "$START_UPTIME" = "y" ] || [ "$START_UPTIME" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "Uptime Kuma started" || log_warning "Failed to start" + fi + + echo " Access at: http://localhost:3001" + echo "" +} diff --git a/services/watchtower.sh b/services/watchtower.sh new file mode 100644 index 0000000..d82e247 --- /dev/null +++ b/services/watchtower.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# services/watchtower.sh — Watchtower automatic container update monitoring. +# Part of the modular post-install system (sourced by setup.sh). + +register_service watchtower homelab "Automatic container updates (Watchtower)" + +install_watchtower() { + require_docker || return 1 + log_info "Installing Watchtower..." + local WT_DIR="$DOCKER_DIR/watchtower" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $WT_DIR" + return 0 + fi + + mkdir -p "$WT_DIR" 2>/dev/null || true + ensure_docker_dir_ownership "$WT_DIR" + cd "$WT_DIR" 2>/dev/null || cd "$DOCKER_DIR" || return 1 + + # Ask about mode + echo "" + echo "Watchtower Mode:" + echo " [M] Monitor only - Get notifications about available updates (SAFE)" + echo " [A] Auto-update - Automatically pull and restart containers (RISKY)" + echo "" + echo " ⚠️ Auto-update can break apps like Immich that need DB migrations!" + echo " Recommendation: Use monitor mode, update manually when ready." + echo "" + local WT_MODE="M" + prompt_text "Mode [M/A]:" "M" WT_MODE + WT_MODE=$(echo "$WT_MODE" | tr '[:lower:]' '[:upper:]') + + local MONITOR_ONLY + if [ "$WT_MODE" = "A" ]; then + MONITOR_ONLY="false" + echo " Mode: Auto-update (containers will be updated automatically)" + else + MONITOR_ONLY="true" + echo " Mode: Monitor only (you'll be notified of updates)" + fi + + # Check for ntfy + local NTFY_URL="" + if [ -d "$DOCKER_DIR/ntfy" ]; then + echo " ✓ ntfy detected - configuring notifications" + NTFY_URL="http://ntfy/watchtower" + fi + + cat > docker-compose.yml << WT_COMPOSE +name: watchtower + +services: + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + hostname: watchtower + restart: unless-stopped + environment: + # Check for updates daily at 4 AM + - WATCHTOWER_SCHEDULE=0 0 4 * * * + # Monitor only - don't auto-update (change to false for auto-update) + - WATCHTOWER_MONITOR_ONLY=${MONITOR_ONLY} + # Cleanup old images after update + - WATCHTOWER_CLEANUP=true + # Include stopped containers + - WATCHTOWER_INCLUDE_STOPPED=true + # Notification URL (ntfy, Discord, Slack, etc.) + - WATCHTOWER_NOTIFICATION_URL=\${NOTIFICATION_URL:-} + # Show debug info + - WATCHTOWER_DEBUG=false + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro +WT_COMPOSE + + # Create .env + cat > .env << WT_ENV +# Watchtower Configuration +# ========================= +# +# Monitor-only mode: Watchtower checks for updates but doesn't apply them. +# This is SAFER because some apps (Immich, Mealie) have database migrations +# that can break if you update without proper procedures. +# +# To update manually: +# cd ~/docker/{app} +# docker compose pull +# docker compose up -d + +# Set to "false" to enable auto-updates (RISKY!) +MONITOR_ONLY=$MONITOR_ONLY + +# Notification URL (optional) +# Examples: +# ntfy: ntfy://ntfy.example.com/watchtower +# Discord: discord://token@id +# Slack: slack://hook-url +# Gotify: gotify://hostname/token +# +# Full list: https://containrrr.dev/shoutrrr/services/overview/ +NOTIFICATION_URL=$NTFY_URL +WT_ENV + + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WT_DIR" 2>/dev/null || true + + echo "" + log_success "Watchtower installed at $WT_DIR" + + write_readme "$WT_DIR" << MD +# Watchtower + +Monitors running containers for image updates. Defaults to NOTIFY-ONLY mode, +because apps like Immich can have breaking DB migrations on auto-update. + +## No web interface +Watchtower has no web UI/port. It runs in the background and checks for updates +daily at 4 AM. + +## Configuration +- Mode: $([ "$MONITOR_ONLY" = "true" ] && echo "Monitor only" || echo "Auto-update") (set MONITOR_ONLY in .env; "false" = auto-update) +- Notifications: set NOTIFICATION_URL in .env (ntfy, Discord, Slack, Gotify, ...) + See https://containrrr.dev/shoutrrr/services/overview/ + +## Exclude a container +Add this label to any container you want Watchtower to ignore: +\`com.centurylinklabs.watchtower.enable=false\` + +## Update an app manually +\`\`\` +cd ~/docker/ +docker compose pull +docker compose up -d +\`\`\` + +## Manage +\`\`\` +cd $WT_DIR +docker compose up -d # start +docker compose down # stop +docker compose logs -f # logs +\`\`\` +MD + + local START_WATCHTOWER="" + prompt_yn "Start Watchtower now? (y/n):" "y" START_WATCHTOWER + if [ "$START_WATCHTOWER" = "y" ] || [ "$START_WATCHTOWER" = "Y" ]; then + docker compose up -d 2>/dev/null && log_success "Watchtower started" || log_warning "Failed to start" + fi + + echo " Mode: $([ "$MONITOR_ONLY" = "true" ] && echo "Monitor only" || echo "Auto-update")" + echo "" + echo " Checks for updates daily at 4 AM." + if [ -n "$NTFY_URL" ]; then + echo " Notifications: $NTFY_URL" + else + echo " Configure NOTIFICATION_URL in .env for alerts." + fi + echo "" + echo " To exclude a container from Watchtower:" + echo " Add label: com.centurylinklabs.watchtower.enable=false" + echo "" +}