Files
pihole-baremetal/install.sh
Vish ce5674cb6c Initial commit: Pi-hole baremetal one-liner installer
- Supports Ubuntu, Debian, Fedora, Rocky, CentOS, RHEL, Arch, openSUSE
- Automatic OS detection and dependency installation
- Automatic network interface and IP detection
- Configurable upstream DNS servers
- Firewall configuration (firewalld/ufw/iptables)
- Management helper script (pihole-manage)
- Unattended installation option
- FreeBSD redirects to AdGuard Home alternative
2026-01-18 08:12:38 +00:00

647 lines
19 KiB
Bash

#!/bin/bash
# =============================================================================
# Pi-hole Baremetal Installer
# =============================================================================
# Network-wide ad blocking DNS server - installs directly on the system
#
# Supported: Ubuntu, Debian, Fedora, Rocky/Alma/RHEL 8+, Arch, openSUSE, FreeBSD
# Works on minimal/headless installs
#
# Usage:
# curl -fsSL <url>/install.sh | sudo bash
#
# Options:
# --unattended Skip all prompts, use defaults
# --no-lighttpd Skip web server (API/admin only via CLI)
# --set-password Set admin password non-interactively (reads from stdin)
# --ipv4 <ip> Set static IPv4 address
# --interface <if> Set network interface (e.g., eth0)
# --dns1 <ip> Set upstream DNS 1 (default: 1.1.1.1)
# --dns2 <ip> Set upstream DNS 2 (default: 1.0.0.1)
# =============================================================================
set -o pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
# Configuration defaults
UNATTENDED=false
INSTALL_LIGHTTPD=true
PIHOLE_DNS_1="1.1.1.1"
PIHOLE_DNS_2="1.0.0.1"
INTERFACE=""
IPV4_ADDRESS=""
ADMIN_PASSWORD=""
INSTALL_DIR="/etc/pihole"
PIHOLE_SKIP_OS_CHECK=false
# Parse arguments
while [ $# -gt 0 ]; do
case $1 in
--unattended) UNATTENDED=true; shift ;;
--no-lighttpd) INSTALL_LIGHTTPD=false; shift ;;
--set-password) ADMIN_PASSWORD="$2"; shift 2 ;;
--ipv4) IPV4_ADDRESS="$2"; shift 2 ;;
--interface) INTERFACE="$2"; shift 2 ;;
--dns1) PIHOLE_DNS_1="$2"; shift 2 ;;
--dns2) PIHOLE_DNS_2="$2"; shift 2 ;;
--skip-os-check) PIHOLE_SKIP_OS_CHECK=true; shift ;;
--help|-h)
echo "Pi-hole Baremetal Installer"
echo ""
echo "Usage: install.sh [options]"
echo ""
echo "Options:"
echo " --unattended Skip prompts, use defaults"
echo " --no-lighttpd Skip web server installation"
echo " --set-password X Set admin password"
echo " --ipv4 <ip> Set static IPv4 (e.g., 192.168.1.10/24)"
echo " --interface <if> Set interface (e.g., eth0)"
echo " --dns1 <ip> Upstream DNS 1 (default: 1.1.1.1)"
echo " --dns2 <ip> Upstream DNS 2 (default: 1.0.0.1)"
echo " --skip-os-check Skip OS compatibility check"
exit 0
;;
*) shift ;;
esac
done
# Check root
[ "$(id -u)" -ne 0 ] && error "Run as root: sudo bash install.sh"
# Detect OS
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
OS_VERSION=${VERSION_ID:-}
OS_NAME="${NAME:-$OS}"
elif [ "$(uname)" = "FreeBSD" ]; then
OS="freebsd"
OS_VERSION=$(freebsd-version -u 2>/dev/null | cut -d'-' -f1)
OS_NAME="FreeBSD"
else
error "Cannot detect OS"
fi
log "Detected: $OS_NAME $OS_VERSION"
}
# Check if OS is supported by official Pi-hole installer
check_pihole_support() {
case $OS in
ubuntu|debian|raspbian)
# Supported versions
return 0
;;
fedora)
return 0
;;
centos|rhel|rocky|almalinux)
return 0
;;
*)
if [ "$PIHOLE_SKIP_OS_CHECK" = true ]; then
warn "OS '$OS' may not be officially supported by Pi-hole"
warn "Attempting installation anyway (--skip-os-check enabled)"
return 0
else
return 1
fi
;;
esac
}
# Wait for apt/dpkg locks
wait_for_apt_lock() {
local max_wait=120
local waited=0
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \
fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || \
fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
if [ $waited -eq 0 ]; then
log "Waiting for apt/dpkg lock to be released..."
fi
sleep 5
waited=$((waited + 5))
if [ $waited -ge $max_wait ]; then
warn "Timeout waiting for apt lock, attempting to continue..."
break
fi
done
}
# Wait for zypper lock
wait_for_zypper_lock() {
local max_wait=120
local waited=0
while pgrep -x zypper >/dev/null 2>&1; do
if [ $waited -eq 0 ]; then
log "Waiting for zypper lock to be released..."
fi
sleep 5
waited=$((waited + 5))
if [ $waited -ge $max_wait ]; then
warn "Timeout waiting for zypper lock..."
break
fi
done
}
# Install prerequisites
install_prerequisites() {
log "Installing prerequisites..."
case $OS in
ubuntu|debian|raspbian|linuxmint|pop)
export DEBIAN_FRONTEND=noninteractive
wait_for_apt_lock
apt-get update -qq
apt-get install -y -qq curl ca-certificates git iproute2 procps >/dev/null 2>&1
;;
fedora)
dnf install -y -q curl ca-certificates git iproute procps-ng >/dev/null 2>&1
;;
rocky|almalinux|rhel|centos)
if command -v dnf >/dev/null 2>&1; then
dnf install -y -q curl ca-certificates git iproute procps-ng >/dev/null 2>&1
else
yum install -y curl ca-certificates git iproute procps >/dev/null 2>&1
fi
;;
arch|manjaro|endeavouros)
pacman -Sy --noconfirm --quiet curl ca-certificates git iproute2 procps-ng >/dev/null 2>&1
;;
opensuse*|sles)
wait_for_zypper_lock
zypper install -y curl ca-certificates git iproute2 procps >/dev/null 2>&1
;;
freebsd)
env ASSUME_ALWAYS_YES=YES pkg bootstrap >/dev/null 2>&1 || true
pkg install -y bash curl git ca_root_nss >/dev/null 2>&1
;;
*)
warn "Unknown OS, attempting to continue..."
;;
esac
success "Prerequisites installed"
}
# Detect network interface
detect_interface() {
if [ -n "$INTERFACE" ]; then
return
fi
# Try to find the default interface
INTERFACE=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'dev \K\S+' | head -1)
if [ -z "$INTERFACE" ]; then
# Fallback: get first non-loopback interface
INTERFACE=$(ip -o link show | awk -F': ' '$2 != "lo" {print $2; exit}')
fi
if [ -z "$INTERFACE" ]; then
INTERFACE="eth0"
fi
log "Using interface: $INTERFACE"
}
# Detect IP address
detect_ip() {
if [ -n "$IPV4_ADDRESS" ]; then
return
fi
# Get current IP on the detected interface
IPV4_ADDRESS=$(ip -4 addr show "$INTERFACE" 2>/dev/null | grep -oP 'inet \K[\d.]+/\d+' | head -1)
if [ -z "$IPV4_ADDRESS" ]; then
# Fallback: get any non-loopback IP
IPV4_ADDRESS=$(hostname -I 2>/dev/null | awk '{print $1}')
if [ -n "$IPV4_ADDRESS" ]; then
IPV4_ADDRESS="${IPV4_ADDRESS}/24"
fi
fi
if [ -z "$IPV4_ADDRESS" ]; then
error "Could not detect IP address. Use --ipv4 to specify."
fi
log "Using IP: $IPV4_ADDRESS"
}
# Get gateway
detect_gateway() {
GATEWAY=$(ip route | grep default | awk '{print $3}' | head -1)
if [ -z "$GATEWAY" ]; then
GATEWAY="192.168.1.1"
fi
log "Gateway: $GATEWAY"
}
# Create Pi-hole setup vars for unattended install
create_setupvars() {
log "Creating Pi-hole configuration..."
mkdir -p "$INSTALL_DIR"
# Extract IP without CIDR for some settings
IP_ONLY=$(echo "$IPV4_ADDRESS" | cut -d'/' -f1)
cat > "$INSTALL_DIR/setupVars.conf" << EOF
PIHOLE_INTERFACE=$INTERFACE
IPV4_ADDRESS=$IPV4_ADDRESS
IPV6_ADDRESS=
PIHOLE_DNS_1=$PIHOLE_DNS_1
PIHOLE_DNS_2=$PIHOLE_DNS_2
QUERY_LOGGING=true
INSTALL_WEB_SERVER=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" )
INSTALL_WEB_INTERFACE=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" )
LIGHTTPD_ENABLED=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" )
CACHE_SIZE=10000
DNS_FQDN_REQUIRED=true
DNS_BOGUS_PRIV=true
DNSMASQ_LISTENING=local
WEBPASSWORD=
BLOCKING_ENABLED=true
EOF
success "Configuration created at $INSTALL_DIR/setupVars.conf"
}
# Install Pi-hole using official installer
install_pihole_official() {
log "Installing Pi-hole (official installer)..."
# Download and run official installer
if [ "$UNATTENDED" = true ]; then
curl -sSL https://install.pi-hole.net | bash /dev/stdin --unattended
else
curl -sSL https://install.pi-hole.net | bash
fi
if [ $? -ne 0 ]; then
error "Pi-hole installation failed"
fi
success "Pi-hole installed successfully"
}
# Install Pi-hole on Arch Linux (manual process)
install_pihole_arch() {
log "Installing Pi-hole on Arch Linux..."
# Install dependencies
pacman -Sy --noconfirm --needed \
php php-cgi php-sqlite php-intl \
lighttpd \
base-devel git \
dnsmasq \
sudo \
inetutils >/dev/null 2>&1
# Clone Pi-hole from AUR or use manual install
if command -v yay >/dev/null 2>&1; then
log "Using yay to install Pi-hole from AUR..."
sudo -u nobody yay -S --noconfirm pi-hole-server pi-hole-ftl 2>/dev/null || true
elif command -v paru >/dev/null 2>&1; then
log "Using paru to install Pi-hole from AUR..."
sudo -u nobody paru -S --noconfirm pi-hole-server pi-hole-ftl 2>/dev/null || true
fi
# If AUR helpers failed or not available, try official installer with skip
if ! command -v pihole >/dev/null 2>&1; then
warn "AUR installation failed, trying official installer..."
export PIHOLE_SKIP_OS_CHECK=true
curl -sSL https://install.pi-hole.net | PIHOLE_SKIP_OS_CHECK=true bash /dev/stdin --unattended
fi
success "Pi-hole installed on Arch Linux"
}
# Install Pi-hole on openSUSE
install_pihole_opensuse() {
log "Installing Pi-hole on openSUSE..."
wait_for_zypper_lock
# Install dependencies
zypper install -y \
php8 php8-cgi php8-sqlite php8-intl \
lighttpd \
git curl \
dnsmasq \
iproute2 >/dev/null 2>&1
# Try official installer with skip
export PIHOLE_SKIP_OS_CHECK=true
curl -sSL https://install.pi-hole.net | PIHOLE_SKIP_OS_CHECK=true bash /dev/stdin --unattended
success "Pi-hole installed on openSUSE"
}
# Install Pi-hole on FreeBSD
install_pihole_freebsd() {
log "Installing Pi-hole on FreeBSD..."
echo ""
echo "========================================"
echo " FreeBSD Pi-hole Installation"
echo "========================================"
echo ""
echo "FreeBSD is not officially supported by Pi-hole."
echo ""
echo "Alternatives for FreeBSD:"
echo ""
echo "1. Use AdGuard Home (better FreeBSD support):"
echo " pkg install adguardhome"
echo " sysrc adguardhome_enable=YES"
echo " service adguardhome start"
echo " # Access at http://localhost:3000"
echo ""
echo "2. Use Unbound + blocklists manually"
echo ""
echo "3. Run Pi-hole in a Linux jail (bhyve/vm)"
echo ""
# Offer to install AdGuard Home instead
if [ "$UNATTENDED" != true ]; then
read -p "Install AdGuard Home instead? [Y/n] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
pkg install -y adguardhome
sysrc adguardhome_enable=YES
service adguardhome start
success "AdGuard Home installed! Access at http://localhost:3000"
exit 0
fi
fi
error "FreeBSD is not supported for Pi-hole baremetal installation"
}
# Configure firewall
configure_firewall() {
log "Configuring firewall..."
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then
firewall-cmd --permanent --add-service=dns 2>/dev/null || true
firewall-cmd --permanent --add-service=http 2>/dev/null || true
firewall-cmd --permanent --add-service=https 2>/dev/null || true
firewall-cmd --permanent --add-port=53/tcp 2>/dev/null || true
firewall-cmd --permanent --add-port=53/udp 2>/dev/null || true
firewall-cmd --permanent --add-port=80/tcp 2>/dev/null || true
firewall-cmd --permanent --add-port=4711/tcp 2>/dev/null || true # FTL
firewall-cmd --reload 2>/dev/null || true
success "Firewall configured (firewalld)"
elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q "active"; then
ufw allow 53/tcp 2>/dev/null || true
ufw allow 53/udp 2>/dev/null || true
ufw allow 80/tcp 2>/dev/null || true
ufw allow 4711/tcp 2>/dev/null || true
success "Firewall configured (ufw)"
elif command -v iptables >/dev/null 2>&1; then
# Basic iptables rules
iptables -A INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
iptables -A INPUT -p tcp --dport 4711 -j ACCEPT 2>/dev/null || true
success "Firewall rules added (iptables)"
else
warn "No firewall detected, skipping configuration"
fi
}
# Set admin password
set_admin_password() {
if [ -n "$ADMIN_PASSWORD" ]; then
log "Setting admin password..."
pihole -a -p "$ADMIN_PASSWORD"
success "Admin password set"
elif [ "$UNATTENDED" != true ]; then
echo ""
echo "Set your Pi-hole admin password:"
pihole -a -p
fi
}
# Create management script
create_management_script() {
log "Creating management script..."
cat > /usr/local/bin/pihole-manage << 'EOFSCRIPT'
#!/bin/bash
# Pi-hole management helper
case "${1:-help}" in
status)
echo "=== Pi-hole Status ==="
pihole status
echo ""
echo "=== Service Status ==="
systemctl status pihole-FTL --no-pager 2>/dev/null || service pihole-FTL status
;;
logs)
tail -f /var/log/pihole/pihole.log
;;
ftl-logs)
tail -f /var/log/pihole/FTL.log
;;
query)
shift
pihole -q "$@"
;;
update)
pihole -up
;;
restart)
pihole restartdns
;;
disable)
duration="${2:-}"
if [ -n "$duration" ]; then
pihole disable "$duration"
else
pihole disable
fi
;;
enable)
pihole enable
;;
stats)
pihole -c -e
;;
top-ads)
echo "=== Top Blocked Domains ==="
pihole -t
;;
top-clients)
sqlite3 /etc/pihole/pihole-FTL.db \
"SELECT client, COUNT(*) as count FROM queries GROUP BY client ORDER BY count DESC LIMIT 10;" 2>/dev/null || \
echo "Database query not available"
;;
whitelist)
shift
pihole -w "$@"
;;
blacklist)
shift
pihole -b "$@"
;;
gravity)
pihole -g
;;
backup)
timestamp=$(date +"%Y%m%d_%H%M%S")
backup_dir="/var/backups/pihole"
mkdir -p "$backup_dir"
pihole -a -t "$backup_dir/pihole_backup_$timestamp.tar.gz"
echo "Backup created: $backup_dir/pihole_backup_$timestamp.tar.gz"
;;
password)
pihole -a -p
;;
*)
echo "Pi-hole Management Helper"
echo ""
echo "Usage: pihole-manage <command>"
echo ""
echo "Commands:"
echo " status Show Pi-hole and service status"
echo " logs Tail the Pi-hole log"
echo " ftl-logs Tail the FTL log"
echo " query <d> Query the log for domain <d>"
echo " update Update Pi-hole"
echo " restart Restart DNS resolver"
echo " disable [t] Disable blocking (optionally for t seconds)"
echo " enable Enable blocking"
echo " stats Show statistics"
echo " top-ads Show top blocked domains"
echo " whitelist Add domain to whitelist"
echo " blacklist Add domain to blacklist"
echo " gravity Update blocklists"
echo " backup Create configuration backup"
echo " password Change admin password"
echo ""
echo "Pi-hole native commands: pihole -h"
;;
esac
EOFSCRIPT
chmod +x /usr/local/bin/pihole-manage
success "Management script created: pihole-manage"
}
# Show completion message
show_complete() {
local IP=$(echo "$IPV4_ADDRESS" | cut -d'/' -f1)
if [ -z "$IP" ]; then
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
echo ""
echo "========================================"
echo " Pi-hole Installation Complete!"
echo "========================================"
echo ""
echo "Access:"
echo " Admin Panel: http://$IP/admin"
echo " DNS Server: $IP"
echo ""
echo "Configuration:"
echo " Interface: $INTERFACE"
echo " IPv4: $IPV4_ADDRESS"
echo " Upstream DNS: $PIHOLE_DNS_1, $PIHOLE_DNS_2"
echo ""
echo "Commands:"
echo " pihole status - Check status"
echo " pihole -c - Console dashboard"
echo " pihole -up - Update Pi-hole"
echo " pihole -g - Update gravity (blocklists)"
echo " pihole-manage help - Management helper"
echo ""
echo "Client Configuration:"
echo " Set your devices/router DNS to: $IP"
echo ""
if [ -z "$ADMIN_PASSWORD" ] && [ "$UNATTENDED" = true ]; then
echo "⚠️ Set admin password:"
echo " pihole -a -p"
echo ""
fi
echo "Logs: /var/log/pihole/"
echo "Config: /etc/pihole/"
echo ""
}
# Main installation
main() {
echo ""
echo "========================================"
echo " Pi-hole Baremetal Installer"
echo "========================================"
echo ""
detect_os
install_prerequisites
detect_interface
detect_ip
detect_gateway
# Create config for unattended install
if [ "$UNATTENDED" = true ]; then
create_setupvars
fi
# Route to appropriate installer
case $OS in
ubuntu|debian|raspbian|fedora|centos|rhel|rocky|almalinux)
install_pihole_official
;;
arch|manjaro|endeavouros)
install_pihole_arch
;;
opensuse*|sles)
install_pihole_opensuse
;;
freebsd)
install_pihole_freebsd
;;
linuxmint|pop)
# Mint and Pop are Ubuntu-based, should work
install_pihole_official
;;
*)
if [ "$PIHOLE_SKIP_OS_CHECK" = true ]; then
warn "Attempting installation on unsupported OS: $OS"
install_pihole_official
else
error "Unsupported OS: $OS. Use --skip-os-check to force installation."
fi
;;
esac
configure_firewall
set_admin_password
create_management_script
show_complete
}
main "$@"