- detect_storage_paths(): also matches drives that have .felhom-infra-backup/ so sys_drive-style mounts (internal SSD partitions with DR markers) are detected even when settings.json is gone and felhom-data/ doesn't exist. - do_nuclear_wipe(): rmdir all empty /mnt/*/ dirs at end of nuclear wipe to clean up leftover mount point directories (e.g. /mnt/hdd_1 when the raw mount was already cleaned by a prior wipe run). rmdir is safe — it refuses non-empty directories. - print_plan(): show per-drive .felhom-infra-backup entries for nuclear level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scripts/
Setup and maintenance scripts for Felhom homeserver deployments.
docker-setup.sh
Automated deployment script for Felhom customer nodes.
Takes a fresh Debian 13 server and deploys a complete Felhom homeserver stack: Docker, Traefik reverse proxy, Cloudflare Tunnel (optional), TLS certificates, FileBrowser, and the felhom-controller dashboard.
Version: 6.0.0
Quick start
# Minimal — interactive wizard for all settings
sudo ./docker-setup.sh --domain example.com --email admin@example.com
# Full — Cloudflare DNS + static IP + pre-seeded customer
sudo ./docker-setup.sh \
--ip 192.168.0.50 \
--domain example.com \
--email admin@example.com \
--cf-token "your-cloudflare-api-token" \
--customer "customer-1"
# Hub mode — one-liner: all infra settings (domain, email, CF tokens) come from Hub
sudo ./docker-setup.sh \
--hub-customer "customer-1" \
--hub-password "retrieval-password-from-hub"
CLI flags
| Flag | Argument | Description | Default |
|---|---|---|---|
--bootstrap |
— | Install sudo on fresh Debian (run as root first) | — |
--ip |
ADDRESS | Static IP address (e.g., 192.168.0.50) |
DHCP |
--gateway |
ADDRESS | Gateway address | 192.168.0.1 |
--dns |
SERVERS | DNS servers, comma-separated | 1.1.1.1,8.8.8.8 |
--interface |
NAME | Network interface name | Auto-detect |
--domain |
DOMAIN | Base domain (pre-seeds wizard) | homeserver.local |
--email |
ACME email for Let's Encrypt | — | |
--cf-token |
TOKEN | Cloudflare API token for DNS-01 challenge | — |
--customer |
ID | Customer identifier (pre-seeds wizard) | — |
--hub-customer |
ID | Download config from Hub: customer ID | — |
--hub-password |
PASSWORD | Download config from Hub: retrieval password | — |
--traefik-password |
PASSWORD | Traefik dashboard basicAuth password | Auto-generated |
--self-signed-cert |
— | Generate self-signed wildcard certificate | false |
--skip-filebrowser |
— | Skip FileBrowser installation | false |
--dry-run |
— | Show plan without making changes | false |
--debug |
— | Enable bash debug tracing (set -x) |
false |
-h, --help |
— | Show help message | — |
Installation steps
The script runs these steps in order:
| Step | Function | Description |
|---|---|---|
| 1 | install_base_packages() |
Install system packages: curl, git, htop, jq, openssl, apache2-utils, etc. |
| 2 | configure_static_ip() |
Configure static IP (optional). Supports NetworkManager, systemd-networkd, ifupdown. |
| 3 | install_docker() |
Install Docker CE + Compose plugin. GPG key, repo setup, daemon.json with DNS fallback, traefik-public network. |
| 4 | install_traefik() |
Deploy Traefik v3.6.7 reverse proxy with configurable TLS (Let's Encrypt DNS-01/HTTP-01 or self-signed). |
| 4b | install_cloudflare_tunnel() |
Deploy Cloudflare Tunnel (optional, only if tunnel token provided in wizard). |
| 5 | generate_self_signed_cert() |
Generate self-signed CA + wildcard cert (optional, if --self-signed-cert flag set). |
| 6 | run_config_wizard() |
Interactive wizard or Hub download. Generates controller.yaml with customer settings. |
| 7 | install_filebrowser() |
Deploy FileBrowser Quantum with no drive volumes (optional). Drive volumes are managed by the controller via SyncFileBrowserMounts() after storage is registered. |
| 8 | install_controller() |
Deploy felhom-controller (privileged container with system access). |
| 9 | install_tools_and_configure() |
Install ctop, lazydocker, Docker shell aliases. |
TLS certificate modes
The script supports three mutually exclusive TLS modes:
-
Let's Encrypt + Cloudflare DNS-01 (recommended)
- Requires:
--email+--cf-token - Works with Cloudflare Tunnel (no public port 80 needed)
- Automatic renewal via Traefik
- Requires:
-
Let's Encrypt + HTTP-01
- Requires:
--email(no--cf-token) - Requires port 80 publicly accessible
- Does NOT work with Cloudflare Tunnel
- Requires:
-
Self-signed certificate
- Requires:
--self-signed-cert - Generates 10-year wildcard cert with custom CA
- CA cert copied to user home for manual device import
- Requires:
Hub mode
When both --hub-customer and --hub-password are provided, the script downloads a
pre-configured controller.yaml from the Felhom Hub before any infra setup begins,
then extracts the stored values to auto-configure everything — no additional flags needed:
GET https://hub.felhom.eu/api/v1/config/{customer_id}
Header: X-Retrieval-Password: {password}
The downloaded config is parsed early in the run and populates:
| Extracted field | Used for |
|---|---|
customer.domain |
Traefik routing, TLS cert SANs, DNS display |
customer.email |
Let's Encrypt ACME registration |
infrastructure.cf_api_token |
Traefik DNS-01 TLS challenge |
infrastructure.cf_tunnel_token |
Cloudflare Tunnel connector |
CLI flags always take precedence — passing --domain overrides the hub value.
On failure (wrong credentials, network error):
- Script exits immediately with the HTTP status code and the failing URL
- Nothing is installed
Hub credentials are found in the Hub web UI under the customer's Credentials section.
Configuration wizard
When Hub download is not used, the interactive wizard prompts for:
| Section | Fields |
|---|---|
| Customer identity | ID, display name, domain, email |
| Infrastructure secrets | Cloudflare Tunnel token, CF API token |
| Paths | System data partition mount point |
| Dashboard password | bcrypt-hashed login password (optional) |
| Git sync | App catalog repo URL, username, token |
| Monitoring | 5x healthchecks.io ping UUIDs (heartbeat, system, DB dump, backup, integrity) |
Validation:
- Customer ID is required (cannot be empty or
demo-felhom) - Domain is required (cannot be
homeserver.local)
Output files
| Path | Description |
|---|---|
/opt/docker/felhom-controller/controller.yaml |
Controller configuration (perms 600) |
/opt/docker/felhom-controller/docker-compose.yml |
Controller container definition |
/opt/docker/traefik/traefik.yml |
Traefik static configuration |
/opt/docker/traefik/dynamic/dashboard.yml |
Traefik dashboard route |
/opt/docker/traefik/docker-compose.yml |
Traefik container definition |
/opt/docker/stacks/filebrowser/docker-compose.yml |
FileBrowser container definition |
/opt/docker/cloudflared/docker-compose.yml |
Cloudflare Tunnel definition (optional) |
/var/log/docker-setup.log |
Full installation log |
Safety features
- Error trapping:
set -euo pipefail+ ERR trap with diagnostic collection - Dry-run mode: Full preview without state changes
- Input validation: IP format, required fields, credential checks
- Atomic writes: Temp file + rename for sensitive config files
- Permission hardening: 600 for secrets, 644 for certificates
- Idempotency: Checks for existing installations, skips if already done
- Docker fallback: Falls back from Debian 13 (trixie) to 12 (bookworm) repo if needed
- DNS verification: Tests DNS resolution before proceeding
- Health checks: Verifies Docker daemon ready, containers running after each deploy
Prerequisites
- OS: Debian 13 (Trixie) — minimal support for other distros with warnings
- Privileges: Must run with
sudo - Network: Internet connection for package downloads and Docker image pulls
- DNS: Must resolve before script runs (verified via
getent hosts download.docker.com)
Bootstrap mode
On a fresh Debian install without sudo:
# As root — install sudo and configure user
./docker-setup.sh --bootstrap
# Then as regular user — full setup
sudo ./docker-setup.sh --domain example.com --email admin@example.com
Shell aliases installed
After setup, these aliases are added to the user's .bashrc:
dc='sudo docker compose'
dcu='sudo docker compose up -d'
dcd='sudo docker compose down'
dcl='sudo docker compose logs -f'
dps='sudo docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
dlogs='sudo docker logs -f'
dexec='sudo docker exec -it'
dprune='sudo docker system prune -af'
Helper tools installed
- ctop — Top-like interface for container metrics
- lazydocker — Terminal UI for Docker management
felhom-wipe.sh
Test node cleanup script with 4 wipe levels.
Removes felhom-managed data from a node in a controlled, repeatable way. Designed for test/demo nodes to reset state between testing cycles.
Quick start
# Preview what will be removed (dry run — default)
sudo ./felhom-wipe.sh --level full
# Execute the wipe
sudo ./felhom-wipe.sh --level full --yes
Wipe levels
| Level | What it removes |
|---|---|
soft |
Controller state files only: settings.json, metrics.db, setup-state.json, update-state.json, session-data.json, snapshot-history.json |
controller |
Soft + all non-infra Docker containers, all Docker volumes (except portainer_data), all stack directories (skips protected stacks by default) |
full |
controller-level cleanup + felhom-data/ on all storage drives (appdata, backups). Also removes old-style appdata/ and backups/ directories for pre-v0.26.0 compatibility. Removes /mnt/.felhom-scan/ (stale DR scan dir). Infra containers (including felhom-controller) are preserved; controller is restarted after cleanup. |
nuclear |
Full + all infra containers (controller, traefik, cloudflared, portainer), DR markers (.felhom-infra-backup/ on all drives), raw helper mounts (/mnt/.felhom-raw/ — unmount bind+raw, strip fstab entries), /mnt/.felhom-scan/, docker system prune -af --volumes, and all infra config directories (/opt/docker/felhom-controller/, /opt/docker/traefik/, /opt/docker/cloudflared/, /opt/docker/stacks/) |
CLI options
| Option | Description |
|---|---|
--level <level> |
Required. One of: soft, controller, full, nuclear |
--yes |
Execute the wipe. Default is dry-run (preview only). |
--include-protected |
Also remove protected stacks (controller level only). |
Path auto-detection
- Reads
stacks_diranddata_dirfrom/opt/docker/felhom-controller/controller.yamlif present - Reads registered storage paths from
settings.json - Also scans
/mnt/*/forfelhom-data/or legacyappdata/directories not in the registry
Raw helper mounts
The attach wizard creates a two-level mount structure for pre-formatted drives:
/dev/sdb1 (physical partition)
└─ /mnt/.felhom-raw/hdd_1/ ← raw mount (persists in fstab, backs the bind)
└─ felhom_data/
└─ /mnt/hdd_1/ ← bind mount (what apps actually use)
Both fstab entries survive reboots. On nuclear wipe, the script:
- Unmounts bind mounts (e.g.
/mnt/hdd_1) first - Unmounts raw mounts (e.g.
/mnt/.felhom-raw/hdd_1) - Strips both
fstabentries - Removes the now-empty
/mnt/.felhom-raw/directory
The physical data on the drive partition is not touched — only the mount point directories (empty after unmounting) are removed.
/mnt/.felhom-scan/ is a separate ephemeral directory used only during the DR setup
wizard to temporarily inspect drives. It is cleaned up from full level onwards.
What is preserved
- OS and system files
- Infrastructure containers and config (unless
nuclear) - User files:
Dokumentumok/,media/, other non-felhom directories on drives - Drive data — raw mounts are unmounted but partition contents are untouched
- DR markers on drives (unless
nuclear)
Safety
- Dry-run by default — shows plan without deleting anything
- Interactive
YESconfirmation prompt required even with--yes - Must run as root (
sudo) - Checks Docker is running before proceeding
- Protected stacks skipped by default (use
--include-protectedto override)
Redeploy after nuclear wipe
curl -fsSL https://gitea.dooplex.hu/admin/deploy-felhom-compose/raw/branch/main/scripts/docker-setup.sh | bash