Files
CloudDeploy/deploy.sh
2026-03-09 11:22:40 +01:00

1387 lines
46 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# Ensure script is run as root (required for apt + /etc)
if [[ "$EUID" -ne 0 ]]; then
echo "Please run as root (or use sudo)." >&2
exit 1
fi
#set script directory
scriptdir="$(dirname "$(realpath "$0")")"
# Capture the start time (Unix timestamp)
START_TIME=$(date +%s)
# recursive replace function
replace_string_recursive() {
local DIRECTORY="$1"
local OLD_STRING="$2"
local NEW_STRING="$3"
if [ ! -d "$DIRECTORY" ]; then
echo "Error: Directory '$DIRECTORY' does not exist."
return 1
fi
echo "Starting replacement of '$OLD_STRING' with '$NEW_STRING' in '$DIRECTORY'..."
find "$DIRECTORY" -type f -exec sed -i "s|$OLD_STRING|$NEW_STRING|g" {} +
if [ $? -eq 0 ]; then
echo "Replacement completed successfully."
return 0
else
echo "An error occurred during replacement."
return 1
fi
}
# example use
# replace_string_recursive "/path/to/directory" "oldstring" "newstring"
# sqlite db replace function
replace_in_sqlite_db() {
local DB_PATH="$1"
local OLD_STRING="$2"
local NEW_STRING="$3"
# Check if the database file exists
if [ ! -f "$DB_PATH" ]; then
echo "Error: Database file '$DB_PATH' does not exist."
exit 1
fi
echo "Starting replacement of '$OLD_STRING' with '$NEW_STRING' in '$DB_PATH'..."
# Create a backup of the original database
local BACKUP_PATH="${DB_PATH}.bak"
cp "$DB_PATH" "$BACKUP_PATH"
# Escape single quotes for SQL
local OLD_STRING_ESC=$(printf '%s\n' "$OLD_STRING" | sed "s/'/''/g")
local NEW_STRING_ESC=$(printf '%s\n' "$NEW_STRING" | sed "s/'/''/g")
# Get a list of all tables in the database
local TABLES
TABLES=$(sqlite3 "$DB_PATH" ".tables")
# Loop through each table
for TABLE in $TABLES; do
echo "Processing table: $TABLE"
# Escape table name if it's a reserved keyword
local ESCAPED_TABLE
if [[ "$TABLE" =~ ^(index|group|order|table|view|database|schema|trigger|transaction|commit|rollback|savepoint|release|alter|create|drop|insert|update|delete|select|from|where|join|union|intersect|except|limit|offset|order|by|group|having|as|with|replace|cast|case|when|then|else|end|and|or|not|is|null|between|in|like|glob|match|regexp|collate|exists|unique|primary|key|foreign|references|check|constraint|default|collate|asc|desc|on|using|natural|left|right|full|outer|cross|inner)$ ]]; then
ESCAPED_TABLE="\"$TABLE\""
else
ESCAPED_TABLE="$TABLE"
fi
# Get a list of all non-BLOB columns in the table
sqlite3 "$DB_PATH" "PRAGMA table_info($ESCAPED_TABLE);" 2>/dev/null | awk -F'|' '
{
if ($3 != "BLOB") {
print $2
}
}' | while read -r COLUMN; do
if [ -n "$COLUMN" ]; then
echo " Processing column: $COLUMN"
# Escape column name if it's a reserved keyword
local ESCAPED_COLUMN
if [[ "$COLUMN" =~ ^(index|group|order|table|view|database|schema|trigger|transaction|commit|rollback|savepoint|release|alter|create|drop|insert|update|delete|select|from|where|join|union|intersect|except|limit|offset|order|by|group|having|as|with|replace|cast|case|when|then|else|end|and|or|not|is|null|between|in|like|glob|match|regexp|collate|exists|unique|primary|key|foreign|references|check|constraint|default|collate|asc|desc|on|using|natural|left|right|full|outer|cross|inner)$ ]]; then
ESCAPED_COLUMN="\"$COLUMN\""
else
ESCAPED_COLUMN="$COLUMN"
fi
# Update only rows where the column contains OLD_STRING
# Run each update in its own connection to avoid locking issues
sqlite3 "$DB_PATH" "
UPDATE $ESCAPED_TABLE
SET $ESCAPED_COLUMN = replace(CAST($ESCAPED_COLUMN AS TEXT), '$OLD_STRING_ESC', '$NEW_STRING_ESC')
WHERE CAST($ESCAPED_COLUMN AS TEXT) LIKE '%$OLD_STRING_ESC%';
"
fi
done
done
echo "Replacement completed in '$DB_PATH'. Original database backed up to '$BACKUP_PATH'."
}
#example usage: replace_in_sqlite_db "database.sqlite" "Europe/Amsterdam" "UTC"
get_country_from_timezone() {
local timezone="$1"
# Extract the region and city from the timezone (e.g., "America/New_York" -> "New_York")
local city=$(echo "$timezone" | cut -d'/' -f2)
# Use a predefined mapping for common cities (fallback if no better method is found)
# This is a minimal mapping; you can expand it as needed.
local declare -A city_to_country=(
["New_York"]="United States"
["Toronto"]="Canada"
["Amsterdam"]="Netherlands"
["London"]="United Kingdom"
["Tokyo"]="Japan"
["Paris"]="France"
["Berlin"]="Germany"
["Sydney"]="Australia"
["Melbourne"]="Australia"
)
# Check if the city exists in the mapping
if [ -n "${city_to_country[$city]}" ]; then
echo "${city_to_country[$city]}"
else
# Fallback: Try to extract country from the timezone file (less reliable)
# This is a placeholder; actual implementation would require parsing timezone files.
echo "Unknown"
fi
}
oc_installapp() {
local appid="$1"
docker exec owncloud_server occ market:install $appid
}
pause_if_enabled() {
if [ "$SHOULD_PAUSE" = true ]; then
read -n 1 -s -r -p "Press any key to continue..."
echo # Move to a new line after keypress
fi
}
# what this script needs to do:
#formatting variables:
green='\033[0;32m'
cyan='\033[0;36m'
cyanbold='\033[1;36m'
greenbold='\033[1;32m'
nc='\033[0m'
underline='\033[4m'
# Public IP (tries multiple services)
publicip="$(curl -fsS https://api.ipify.org || curl -fsS https://ifconfig.me || echo "UNKNOWN")"
# Local IP (first non-loopback)
localip="$(hostname -I | awk '{print $1}')"
# request all install parameters needed from user
# Selection menu for external access method
echo "--------------------------------------------------------"
echo -e "${cyan}Select your external access method:${nc}"
echo ""
echo "1. Public DNS - You will need to purchase a domain name from a DNS provider and configure it."
echo "2. Local DNS, Local IP - All local devices must use $localip as their DNS. Remote devices must use WireGuard VPN."
echo "3. Local DNS, Public IP - All local devices must use $localip as their DNS. Remote devices must use $publicip as their DNS, you must forward port 53 to $localip"
echo ""
while true; do
read -p "Enter your choice (1-3): " external_access_method
case "$external_access_method" in
(1|2|3)
echo "Selected option: $external_access_method"
break
;;
(*)
echo "Invalid choice. Please enter 1, 2, or 3."
;;
esac
done
# ---- User input ----
read -rp "Admin email: " adminemail < /dev/tty
read -rsp "Admin password: " adminpass < /dev/tty
echo
read -rp "Timezone (e.g. Europe/Amsterdam): " timezone < /dev/tty
read -rp "Domain (e.g. example.com): " domain < /dev/tty
# Prompt the user to set the pause behavior (defaults to true)
echo -e "${cyan}pause prompts${nc} will ask you to finish configuration for each application as the script goes."
echo "disabling these means you'll have to do this after the script finishes, but allows you to run the script unattended."
read -p "Enable pause prompts? [Y/n] " -n 1 -r
echo # Move to a new line
if [[ $REPLY =~ ^[Nn]$ ]]; then
SHOULD_PAUSE=false
else
SHOULD_PAUSE=true
fi
if [ "$external_access_method" -eq 1 ]; then
# Verify DNS records
echo -e "${cyan}Verifying DNS records...${nc}"
echo ""
# Check A record
echo -e "${cyan}Checking A record for @...${nc}"
a_record_check=$(dig +short A "$domain" @8.8.8.8 2>/dev/null | grep -c "^$publicip$" || echo "0")
if [ "$a_record_check" -eq 0 ]; then
echo -e "${cyan}Warning: A record for @ is not set or not pointing to $publicip${nc}"
else
echo -e "${cyan}A record for @ is correctly set to $publicip${nc}"
fi
# Check CNAME record
echo -e "${cyan}Checking CNAME record for *...${nc}"
cname_record_check=$(dig +short CNAME "*.$domain" @8.8.8.8 2>/dev/null | grep -c "$domain\.$" || echo "0")
if [ "$cname_record_check" -eq 0 ]; then
echo -e "${cyan}Warning: CNAME record for * is not set or not pointing to @${nc}"
else
echo -e "${cyan}CNAME record for * is correctly set to @${nc}"
fi
echo "--------------------------------------------------------"
echo -e "please double-check your ${cyan}DNS records${nc} to ensure they are set. the following dns records need to be set:"
echo ""
echo "|name |type |value "
echo "|@ |A |$publicip "
echo "|* |CNAME |@ "
echo ""
echo -e "once you've done this, press any key to ${underline}continue${nc}"
pause_if_enabled
fi
# Ask about static public IP if using public DNS
if [ "$external_access_method" -eq 1 ]; then
while true; do
read -p "Do you have a static public IP address? (yes/no): " static_ip_answer
case "$static_ip_answer" in
([yY]|[yY][eE][sS])
static_public_ip="yes"
break
;;
([nN]|[nN][oO])
static_public_ip="no"
break
;;
(*)
echo "Please answer with 'yes' or 'no'."
;;
esac
done
# Store this information for later use
echo "Static public IP setting: $static_public_ip"
fi
# generate random passwords for DB hosts and other secrets
# ---- Random generators ----
rand_hex() { openssl rand -hex 24; }
rand_b64() { openssl rand -base64 32; }
ownclouddbpass="$(rand_hex)"
ownclouddbrootpass="$(rand_hex)"
convertxJWT="$(rand_hex)"
bookstackkey="$(rand_b64)" # base64 as requested
bookstackdbpass="$(rand_hex)"
bookstackdbrootpass="$(rand_hex)"
onlyofficeJWT="$(rand_hex)"
# install docker
echo "Updating apt and installing prerequisites..."
apt update
apt install -y ca-certificates curl git openssl curl gawk coreutils grep jq sqlite3 iso-codes net-tools
echo "Setting up Docker GPG key..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "Adding Docker apt repository..."
. /etc/os-release
cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
echo "Updating apt and installing Docker..."
apt update
apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
echo "Docker installation complete."
docker --version
# install dockge
mkdir -p /opt/dockge
cd /opt/dockge
# Download your compose.yaml
curl "https://dockge.kuma.pet/compose.yaml?port=5001&stacksPath=%2Fopt%2Fstacks" --output compose.yaml
# write configuration to compose files
mkdir -p /opt/files
cp -r $scriptdir/stacks /opt
cp -r $scriptdir/setup /opt/files
chmod -R 775 /opt/stacks
chmod -R 775 /opt/files
cd /opt/stacks
git clone https://github.com/trantienloi2404/youtube-downloader.git downloader
#adminemail
replace_string_recursive "/opt/stacks" "?adminemail?" $adminemail > /dev/null
#adminpass
replace_string_recursive "/opt/stacks" "?adminpass?" $adminpass > /dev/null
#timezone
replace_string_recursive "/opt/stacks" "?timezone?" $timezone > /dev/null
#domain
replace_string_recursive "/opt/stacks" "?domain?" $domain > /dev/null
#publicip
replace_string_recursive "/opt/stacks" "?publicip?" $publicip > /dev/null
#localip
replace_string_recursive "/opt/stacks" "?localip?" $localip > /dev/null
#ownclouddbpass
replace_string_recursive "/opt/stacks" "?ownclouddbpass?" $ownclouddbpass > /dev/null
#ownclouddbrootpass
replace_string_recursive "/opt/stacks" "?ownclouddbrootpass?" $ownclouddbrootpass > /dev/null
#convertxJWT
replace_string_recursive "/opt/stacks" "?convertxJWT?" $convertxJWT > /dev/null
#bookstackkey
replace_string_recursive "/opt/stacks" "?bookstackkey?" $bookstackkey > /dev/null
replace_string_recursive "/opt/stacks" "?bookstackdbpass?" $bookstackdbpass > /dev/null
#bookstackdbrootpass
replace_string_recursive "/opt/stacks" "?bookstackdbrootpass?" $bookstackdbrootpass > /dev/null
#onlyofficeJWT
replace_string_recursive "/opt/stacks" "?onlyofficeJWT?" $onlyofficeJWT > /dev/null
#domain
replace_string_recursive "/opt/files" "?domain?" $domain > /dev/null
#publicip
replace_string_recursive "/opt/files" "?publicip?" $publicip > /dev/null
#localip
replace_string_recursive "/opt/files" "?localip?" $localip > /dev/null
#adminemail
replace_string_recursive "/opt/files" "?adminemail?" $adminemail > /dev/null
# install mailcow to /opt/stacks/mailcow so it shows up in dockge
umask 0022
#cd /opt/stacks
#git clone https://github.com/mailcow/mailcow-dockerized mailcow
#cd /opt/stacks/mailcow
#bash ./generate_config.sh < /dev/tty
#disable stub listener
echo "DNSStubListener=no" | tee -a /etc/systemd/resolved.conf
systemctl restart systemd-resolved
sleep 5
systemctl restart docker
sleep 5
# up dockge
cd /opt/dockge
docker compose up -d
echo -e "${cyan}Dockge${nc} has been installed and launched. go to http://$localip:5001 and configure your username and password, then press any key to continue"
pause_if_enabled
# call replacements for nginx db
replace_in_sqlite_db "/opt/stacks/npm/data/database.sqlite" "<domain>" $domain > /dev/null
replace_in_sqlite_db "/opt/stacks/npm/data/database.sqlite" "<localip>" $localip > /dev/null
replace_in_sqlite_db "/opt/stacks/npm/data/database.sqlite" "<publicip>" $publicip > /dev/null
cd /opt/stacks/npm
docker compose up -d
echo -e "${cyan}Nginx Proxy Manager${nc} has been installed and launched. go to http://$localip:81 and configure your username and password, then press any key to continue"
pause_if_enabled
echo ""
echo -e "Multiple applications will now be installed in quick succession without continuous user prompting."
echo -e "when each application is installed, you will see an output providing you the url to verify it is online"
echo -e "do not worry if you miss it, you will be reminded later"
echo -e "you can skip verifying as the script runs, and take a short coffee break here."
echo -e "the script will continue automatically in 30 seconds"
sleep 30
cd /opt/stacks/dozzle
docker compose up -d
echo -e "${cyan}dozzle${nc} has been launched from http://dozzle.$domain, verify it is online, do not worry if the UI gives a time-out error, this is due to docker being busy while this script runs."
cd /opt/stacks/convertx
docker compose up -d
echo -e "${cyan}convertx${nc} has been launched from http://convert.$domain, verify it is online"
cd /opt/stacks/it-tools
docker compose up -d
echo -e "${cyan}it-tools${nc} has been launched from http://tools.$domain, verify it is online"
cd /opt/stacks/onlyoffice
docker compose up -d
echo -e "${cyan}onlyoffice${nc} has been launched from http://office.$domain, verify it is online"
#owncloud
# todo: use OC config commands to install addons and configure onlyoffice
cd /opt/stacks/owncloud
docker compose up -d
echo "awaiting owncloud boot"
sleep 40
oc_installapp bookmarks
oc_installapp calendar
oc_installapp camerarawpreviews
oc_installapp checksum
oc_installapp contacts
oc_installapp drawio
oc_installapp extract
oc_installapp files_3dviewer
oc_installapp files_clipboard
oc_installapp files_textviewer
oc_installapp gallery
oc_installapp metadata
oc_installapp music
oc_installapp notes
oc_installapp onlyoffice
oc_installapp tasks
docker exec owncloud_server occ config:app:set onlyoffice DocumentServerUrl --value="https://office.$domain/"
docker exec owncloud_server occ config:app:set onlyoffice jwt_secret --value="$onlyofficeJWT"
docker restart owncloud_server
echo -e "${cyan}owncloud${nc} has been launched from http://cloud.$domain, check if it is online and working, it may take a bit to launch. 502 error = still starting"
cd /opt/stacks/site
docker compose up -d
echo -e "${cyan}site${nc} has been launched from http://www.$domain, verify it is online"
cd /opt/stacks/vaultwarden
docker compose up -d
echo -e "${cyan}vaultwarden${nc} has been launched from http://vault.$domain, verify it is online"
#pihole
cd /opt/stacks/pihole
docker compose up -d
echo -e "${cyan}pihole${nc} has been launched from http://dns.$domain, to use pihole as your DNS provider, set your DNS to $localip in your router for DHCP and on your device for any device with a static ip. Currently, the DNS is configured to use the joindns4.eu DNS, which *also* has built-in adblocking."
cd /opt/stacks/wireguard
docker compose up -d
echo -e "${cyan}wireguard${nc} has been launched from http://vpn.$domain, verify it is online"
cd /opt/stacks/downloader
echo "bringing up downloader, this may take a while."
echo ""
echo -e "recap of all available services up to now:"
echo -e "${cyan}owncloud${nc} has been launched from http://cloud.$domain, check if it is online and working."
echo -e "${cyan}vaultwarden${nc} has been launched from http://vault.$domain, verify it is online"
echo -e "${cyan}site${nc} has been launched from http://www.$domain, verify it is online"
echo -e "${cyan}onlyoffice${nc} has been launched from http://tools.$domain, verify it is online"
echo -e "${cyan}it-tools${nc} has been launched from http://tools.$domain, verify it is online"
echo -e "${cyan}wireguard${nc} has been launched from http://vpn.$domain, verify it is online"
echo -e "${cyan}convertx${nc} has been launched from http://convert.$domain, verify it is online"
echo -e "${cyan}dozzle${nc} has been launched from http://dozzle.$domain, verify it is online, do not worry if the UI gives a time-out error, this is due to docker being busy while this script runs."
echo -e "${cyan}Nginx Proxy Manager${nc} has been installed, configured and launched from http://proxy.$domain"
echo -e "${cyan}Dockge${nc} has been installed, configured and launched from http://docker.$domain"
echo -e "${cyan}pihole${nc} has been launched from http://dns.$domain, to use pihole as your DNS provider, set your DNS to $localip in your router for DHCP and on your device for any device with a static ip. Currently, the DNS is configured to use the joindns4.eu DNS, which *also* has built-in adblocking."
echo ""
echo "you may have to go to settings > additional in owncloud and click "save" for the onlyoffice server settings."
# docker compose up -d > /dev/null
# echo -e "${cyan}downloader${nc} has been launched from http://download.$domain, verify it is online"
#bookstack
cd /opt/stacks/bookstack
docker compose up -d
echo -e "${cyan}bookstack${nc} has been launched from http://docs.$domain, verify it is online (this may take a bit) and log in with email 'admin@admin.com' and password 'password', then reset this account to use $adminemail and your password. afterwards, press any key to continue"
pause_if_enabled
#browser
cd /opt/stacks/browser
docker compose up -d
echo -e "${cyan}filebrowser${nc} has been launched from http://browser.$domain, head to dockge (http://docker.$domain), open the filebrowser stack and check the logs for the initial admin password. make sure to change this in filebrowser's config, then press any key to continue"
pause_if_enabled
#jellyfin
cd /opt/stacks/jellyfin
docker compose up -d
mkdir /opt/stacks/jellyfin/media/shows
mkdir /opt/stacks/jellyfin/media/movies
echo -e "${cyan}jellyfin${nc} has launched from http://video.$domain, perform the initial setup and press any key to continue"
pause_if_enabled
#uptimekuma
cd /opt/stacks/kuma
docker compose up -d
echo -e "${cyan}uptime kuma${nc} has been launched from http://status.$domain, perform the initial setup, choosing embedded mariaDB, then press any key to continue"
pause_if_enabled
#dashboard
replace_in_sqlite_db "/opt/stacks/dashboard/config/www/app.sqlite" "<domain>" $domain
cd /opt/stacks/dashboard
docker compose up -d
echo -e "${cyan}dashboard${nc} has been launched from http://dash.$domain, verify it is online, check that its entries work and press any button to continue"
pause_if_enabled
cat >/opt/stacks/setup/npmcertlist.txt <<EOF
browser.$domain
cloud.$domain
convert.$domain
dash.$domain
dns.$domain
docker.$domain
docs.$domain
download.$domain
dozzle.$domain
office.$domain
proxy.$domain
status.$domain
tools.$domain
vault.$domain
video.$domain
vpn.$domain
www.$domain
test.$domain
temp.$domain
$domain
llm.$domain
mail.$domain
private.$domain
site.$domain
Add any other subdomains you want to use.
EOF
# Write PiHole config (only for options 2 and 3)
if [ "$external_access_method" -eq 2 ] || [ "$external_access_method" -eq 3 ]; then
echo "Writing PiHole configuration..."
# Use public IP for option 3, local IP for option 2
if [ "$external_access_method" -eq 3 ]; then
dns_ip="$publicip"
else
dns_ip="$localip"
fi
cat >/opt/stacks/pihole/dns/02-wildcard.conf <<EOF
address=/$domain/$dns_ip
address=/.$domain/$dns_ip
EOF
chmod -R 775 /opt
docker exec pihole pihole-FTL --config misc.etc_dnsmasq_d true
docker restart pihole
fi
# Enable DHCP on PiHole for options 2 and 3
if [ "$external_access_method" -eq 2 ] || [ "$external_access_method" -eq 3 ]; then
echo "Configuring DHCP on PiHole..."
echo "Gathering network information..."
# Get network information with error checking
gateway=$(ip route | awk '/default/ {print $3}' || echo "ERROR")
interface=$(ip route | awk '/default/ {print $5}' || echo "ERROR")
netmask=$(ifconfig $interface | awk '/netmask/ {print $4}' 2>/dev/null || echo "ERROR")
localip=$(hostname -I | awk '{print $1}' || echo "ERROR")
if [[ "$gateway" == "ERROR" || "$interface" == "ERROR" || "$netmask" == "ERROR" || "$localip" == "ERROR" ]]; then
echo "Error: Failed to get network information"
exit 1
fi
echo " Gateway: $gateway"
echo " Interface: $interface"
echo " Netmask: $netmask"
echo " Local IP: $localip"
# Function to convert IP to binary string with error checking
ip_to_bin() {
local ip=$1
local a b c d
# Validate IP format
if ! [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "Error: Invalid IP format '$ip'" >&2
return 1
fi
IFS=. read -r a b c d <<< "$ip"
# Convert each octet to binary with error checking
local bin_a=$(printf "%08d" "$(echo "obase=2; $a" | bc)")
local bin_b=$(printf "%08d" "$(echo "obase=2; $b" | bc)")
local bin_c=$(printf "%08d" "$(echo "obase=2; $c" | bc)")
local bin_d=$(printf "%08d" "$(echo "obase=2; $d" | bc)")
echo "${bin_a}${bin_b}${bin_c}${bin_d}"
}
# Function to convert binary string to IP with error checking
bin_to_ip() {
local bin=$1
# Validate binary string length
if [ ${#bin} -ne 32 ]; then
echo "Error: Invalid binary string length (expected 32, got ${#bin})" >&2
return 1
fi
local octet1=${bin:0:8}
local octet2=${bin:8:8}
local octet3=${bin:16:8}
local octet4=${bin:24:8}
# Convert each octet with error checking
local ip1=$(echo "ibase=2; $octet1" | bc)
local ip2=$(echo "ibase=2; $octet2" | bc)
local ip3=$(echo "ibase=2; $octet3" | bc)
local ip4=$(echo "ibase=2; $octet4" | bc)
echo "$ip1.$ip2.$ip3.$ip4"
}
# Function to add a number to an IP address (returns IP string)
add_to_ip() {
local ip=$1
local num=$2
# Convert IP to binary
local bin_ip=$(ip_to_bin "$ip" || return 1)
# Split the IP into 4 octets (8 bits each)
local octet1=${bin_ip:0:8}
local octet2=${bin_ip:8:8}
local octet3=${bin_ip:16:8}
local octet4=${bin_ip:24:8}
# Convert octets to decimal
local dec1=$((2#$octet1))
local dec2=$((2#$octet2))
local dec3=$((2#$octet3))
local dec4=$((2#$octet4))
# Add the number to the last octet
dec4=$((dec4 + num))
# Handle overflow
if [ $dec4 -gt 255 ]; then
dec4=$((dec4 - 256))
dec3=$((dec3 + 1))
fi
# Handle overflow in third octet
if [ $dec3 -gt 255 ]; then
dec3=$((dec3 - 256))
dec2=$((dec2 + 1))
fi
# Handle overflow in second octet (unlikely in our use case)
if [ $dec2 -gt 255 ]; then
dec2=$((dec2 - 256))
dec1=$((dec1 + 1))
fi
# Convert back to binary
octet1=$(printf "%08d" "$(echo "obase=2; $dec1" | bc)")
octet2=$(printf "%08d" "$(echo "obase=2; $dec2" | bc)")
octet3=$(printf "%08d" "$(echo "obase=2; $dec3" | bc)")
octet4=$(printf "%08d" "$(echo "obase=2; $dec4" | bc)")
# Convert back to IP
bin_to_ip "${octet1}${octet2}${octet3}${octet4}"
}
# Function to subtract a number from an IP address (returns IP string)
subtract_from_ip() {
local ip=$1
local num=$2
# Convert IP to binary
local bin_ip=$(ip_to_bin "$ip" || return 1)
# Split the IP into 4 octets (8 bits each)
local octet1=${bin_ip:0:8}
local octet2=${bin_ip:8:8}
local octet3=${bin_ip:16:8}
local octet4=${bin_ip:24:8}
# Convert octets to decimal
local dec1=$((2#$octet1))
local dec2=$((2#$octet2))
local dec3=$((2#$octet3))
local dec4=$((2#$octet4))
# Subtract the number from the last octet
dec4=$((dec4 - num))
# Handle underflow
if [ $dec4 -lt 0 ]; then
dec4=$((dec4 + 256))
dec3=$((dec3 - 1))
fi
# Handle underflow in third octet
if [ $dec3 -lt 0 ]; then
dec3=$((dec3 + 256))
dec2=$((dec2 - 1))
fi
# Handle underflow in second octet (unlikely in our use case)
if [ $dec2 -lt 0 ]; then
dec2=$((dec2 + 256))
dec1=$((dec1 - 1))
fi
# Convert back to binary
octet1=$(printf "%08d" "$(echo "obase=2; $dec1" | bc)")
octet2=$(printf "%08d" "$(echo "obase=2; $dec2" | bc)")
octet3=$(printf "%08d" "$(echo "obase=2; $dec3" | bc)")
octet4=$(printf "%08d" "$(echo "obase=2; $dec4" | bc)")
# Convert back to IP
bin_to_ip "${octet1}${octet2}${octet3}${octet4}"
}
# Function to compare two IP addresses (returns 0 if ip1 <= ip2, 1 if ip1 > ip2)
compare_ips() {
local ip1=$1
local ip2=$2
# Convert IPs to their components
IFS=. read -r a1 b1 c1 d1 <<< "$ip1"
IFS=. read -r a2 b2 c2 d2 <<< "$ip2"
# Compare each octet in order
if [ "$a1" -lt "$a2" ]; then return 0; fi
if [ "$a1" -gt "$a2" ]; then return 1; fi
if [ "$b1" -lt "$b2" ]; then return 0; fi
if [ "$b1" -gt "$b2" ]; then return 1; fi
if [ "$c1" -lt "$c2" ]; then return 0; fi
if [ "$c1" -gt "$c2" ]; then return 1; fi
if [ "$d1" -le "$d2" ]; then return 0; fi
return 1
}
# Function to check if an IP is within a range
is_ip_in_range() {
local ip=$1
local start=$2
local end=$3
compare_ips "$ip" "$start"
local ip_ge_start=$?
compare_ips "$ip" "$end"
local ip_le_end=$?
if [ "$ip_ge_start" -eq 0 ] && [ "$ip_le_end" -eq 0 ]; then
return 0 # in range
else
return 1 # out of range
fi
}
# Calculate network and broadcast addresses using binary operations
echo "Calculating network and broadcast addresses..."
gateway_bin=$(ip_to_bin "$gateway" || exit 1)
netmask_bin=$(ip_to_bin "$netmask" || exit 1)
localip_bin=$(ip_to_bin "$localip" || exit 1)
# Calculate network address (bitwise AND of gateway and netmask)
network_bin=""
for i in {0..31}; do
if [ "${gateway_bin:$i:1}" = "1" ] && [ "${netmask_bin:$i:1}" = "1" ]; then
network_bin="${network_bin}1"
else
network_bin="${network_bin}0"
fi
done
# Calculate broadcast address (bitwise OR of network and inverted netmask)
broadcast_bin=""
for i in {0..31}; do
if [ "${netmask_bin:$i:1}" = "0" ]; then
broadcast_bin="${broadcast_bin}1"
else
broadcast_bin="${broadcast_bin}${network_bin:$i:1}"
fi
done
network_address=$(bin_to_ip "$network_bin" || exit 1)
broadcast_address=$(bin_to_ip "$broadcast_bin" || exit 1)
echo " Network Address: $network_address"
echo " Broadcast Address: $broadcast_address"
# Calculate DHCP start: network_address + 10
echo "Calculating DHCP range..."
dhcp_start=$(add_to_ip "$network_address" 10 || exit 1)
echo " Initial DHCP start: $dhcp_start"
# Ensure DHCP start is not before network+1
network_plus_1=$(add_to_ip "$network_address" 1 || exit 1)
echo " Network+1: $network_plus_1"
if compare_ips "$dhcp_start" "$network_plus_1"; then
# dhcp_start <= network_plus_1, so we need to adjust
echo " Adjusting DHCP start to network+1: $network_plus_1"
dhcp_start="$network_plus_1"
fi
# Calculate DHCP end: dhcp_start + 100
dhcp_end=$(add_to_ip "$dhcp_start" 100 || exit 1)
echo " Initial DHCP end: $dhcp_end"
# Ensure DHCP end is not after broadcast-1
broadcast_minus_1=$(subtract_from_ip "$broadcast_address" 1 || exit 1)
echo " Broadcast-1: $broadcast_minus_1"
if ! compare_ips "$dhcp_end" "$broadcast_minus_1"; then
# dhcp_end > broadcast_minus_1, so we need to adjust
echo " Adjusting DHCP end to broadcast-1: $broadcast_minus_1"
dhcp_end="$broadcast_minus_1"
fi
# Exclude local IP from range
if is_ip_in_range "$localip" "$dhcp_start" "$dhcp_end"; then
echo "Local IP $localip is in DHCP range. Adjusting..."
# For simplicity, move the start up by 1
new_dhcp_start=$(add_to_ip "$localip" 1 || exit 1)
if compare_ips "$new_dhcp_start" "$dhcp_end"; then
echo " Adjusting DHCP start to $new_dhcp_start"
dhcp_start="$new_dhcp_start"
else
echo " Cannot adjust DHCP start without making it exceed end"
# Try moving the end down by 1 instead
new_dhcp_end=$(subtract_from_ip "$localip" 1 || exit 1)
if ! compare_ips "$dhcp_start" "$new_dhcp_end"; then
echo " Cannot adjust DHCP range without excluding valid IPs"
else
echo " Adjusting DHCP end to $new_dhcp_end"
dhcp_end="$new_dhcp_end"
fi
fi
fi
# Validate DHCP range
if [ -z "$dhcp_start" ] || [ -z "$dhcp_end" ]; then
echo "Error: Invalid DHCP range calculated. DHCP start or end is empty. Aborting."
exit 1
fi
echo "--- DHCP Configuration Summary ---"
echo " DHCP Range: $dhcp_start to $dhcp_end"
echo " Router: $gateway"
echo " Netmask: $netmask"
echo "----------------------------------"
# Apply the DHCP configuration
echo "Applying DHCP configuration..."
docker exec pihole pihole-FTL --config dhcp.active true || {
echo "Error: Failed to enable DHCP"
exit 1
}
docker exec pihole pihole-FTL --config dhcp.start "$dhcp_start" || {
echo "Error: Failed to set DHCP start"
exit 1
}
docker exec pihole pihole-FTL --config dhcp.end "$dhcp_end" || {
echo "Error: Failed to set DHCP end"
exit 1
}
docker exec pihole pihole-FTL --config dhcp.router "$gateway" || {
echo "Error: Failed to set DHCP router"
exit 1
}
docker exec pihole pihole-FTL --config dhcp.netmask "$netmask" || {
echo "Error: Failed to set DHCP netmask"
exit 1
}
# Restart PiHole to apply changes
echo "Restarting PiHole to apply changes..."
docker restart pihole || {
echo "Error: Failed to restart PiHole"
exit 1
}
echo "DHCP configuration applied successfully."
fi
# Generate post-install tasks file
echo "Generating post-install tasks file..."
tasks_file="/opt/files/post-install.txt"
# Start with common header
cat > "$tasks_file" <<EOF
POST-INSTALL TASKS FOR YOUR HOME CLOUD
=====================================
Generated: $(date)
Timezone: $timezone
Local IP: $localip
Public IP: $publicip
Domain: $domain
IMPORTANT: Follow these tasks in order as some services depend on others being configured first.
EOF
# Initialize section counter
section=1
# 1. DDNS Configuration (only for public DNS with non-static IP)
if [ "$external_access_method" -eq 1 ] && [ "$static_public_ip" != "yes" ]; then
cat >> "$tasks_file" <<EOF
$section. CONFIGURE DDNS
------------------------
RECOMMENDED FREE DDNS PROVIDERS
1. DuckDNS (https://www.duckdns.org) - Simple and free
2. No-IP (https://www.noip.com) - Free tier with monthly confirmation
3. Dynu (https://www.dynu.com) - Free with multiple domain options
4. FreeDNS (https://freedns.afraid.org) - Free subdomains available
IMPORTANT NOTES
1. Ensure your router has a public IP address (not behind CGNAT)
2. For Linux systems, verify ddclient is running properly
3. For routers, check the DDNS status in the admin panel
4. Test your configuration by pinging your new DDNS hostname
5. Some providers require periodic confirmation to maintain free service
--------------------------------------------------------------------------------
OPTION 1: CONFIGURING DDNS ON HOME ROUTERS
--------------------------------------------------------------------------------
ASUS ROUTERS
------------
1. Log in to your router's admin panel at http://$gateway
2. Navigate to WAN > DDNS
3. Select your DDNS provider (No-IP or ASUS DDNS)
4. Enter your DDNS hostname
5. Provide your DDNS credentials
6. Click Apply to save settings
TP-LINK ROUTERS
---------------
1. Log in to your router's admin panel at http://$gateway
2. Navigate to Advanced > Network > Dynamic DNS
3. Select your DDNS provider (No-IP or TP-Link DDNS)
4. Enter your DDNS hostname
5. Provide your DDNS credentials
6. Click Save to apply settings
--------------------------------------------------------------------------------
OPTION 2: CONFIGURING DDNS ON YOUR SERVER USING DDCLIENT
--------------------------------------------------------------------------------
1. INSTALL DDCLIENT
-------------------
sudo apt update && sudo apt install ddclient
2. CONFIGURE DDCLIENT
----------------------
Edit the configuration file:
sudo nano /etc/ddclient/ddclient.conf
For DuckDNS:
protocol=duckdns
use=web
server=www.duckdns.org
login=your_duckdns_token
password=
yourDDNSdomain.duckdns.org
For No-IP:
protocol=noip
use=web
server=dynupdate.no-ip.com
login=your_noip_username
password=your_noip_password
yourDDNSdomain.ddns.net
3. RESTART DDCLIENT
-------------------
sudo systemctl restart ddclient
sudo systemctl enable ddclient
4. VERIFY CONFIGURATION
-----------------------
sudo tail -n 20 /var/log/syslog | grep ddclient
EOF
section=$((section + 1))
fi
# 2. PiHole Configuration (for local DNS methods)
if [ "$external_access_method" -eq 2 ] || [ "$external_access_method" -eq 3 ]; then
cat >> "$tasks_file" <<EOF
$section. CONFIGURE PIHOLE
--------------------------
--------------------------------------------------------------------------------
1. DISABLING DHCP ON YOUR ROUTER
--------------------------------------------------------------------------------
your pihole is configured as a DHCP server, because of this, we want to disable the router's internal DHCP server.
ASUS ROUTERS:
-------------
1. Log in to router admin (http://$gateway)
2. Navigate to LAN > DHCP Server
3. Set "Enable the DHCP Server" to "No"
4. Click "Apply" to save changes
TP-LINK ROUTERS:
---------------
1. Log in to router admin (http://$gateway)
2. Go to Advanced > Network > DHCP Server
3. Toggle DHCP Server to "Disable"
4. Click "Save" to apply
--------------------------------------------------------------------------------
2. CONFIGURING STATIC IPS WITH CUSTOM DNS ($localip or $publicip)
--------------------------------------------------------------------------------
use $publicip if you are forwarding port 53, or $localip if not.
if you use $localip, the device will not have DNS outside of your local network.
you only need to do this for devices that have a static ip address, devices that get their ip assigned via DHCP (which is default for most devices, especially wireless ones) get their DNS address from the router.
WINDOWS:
--------
1. Open Network Settings (Win+I > Network & Internet)
2. Select your connection (Wi-Fi/Ethernet)
3. Click "Hardware properties" > "Edit" next to DNS
4. Set manual DNS to $localip or $publicip
5. Save changes
MACOS:
------
1. Open System Preferences > Network
2. Select your connection
3. Click "Advanced" > DNS tab
4. Add $localip or $publicip to DNS servers
5. Click "OK" > "Apply"
LINUX (NETWORK MANAGER):
------------------------
1. Edit connection settings:
nm-connection-editor
2. Select your connection
3. Go to IPv4/IPv6 settings
4. Set DNS to $localip or $publicip
5. Save and restart connection
ANDROID:
--------
1. Open Wi-Fi settings
2. Long-press your network > Modify network
3. Enable "Advanced options"
4. Set IP to static
5. Enter DNS as $localip or $publicip
IOS:
----
1. Open Wi-Fi settings
2. Tap (i) next to your network
3. Configure DNS > Manual
4. Add $localip or $publicip
5. Save changes
EOF
section=$((section + 1))
fi
# 5. Router Configuration (for public DNS)
if [ "$external_access_method" -eq 1 ]; then
cat >> "$tasks_file" <<EOF
$section. CONFIGURE PIHOLE IN YOUR ROUTER
-----------------------------------------
ASUS ROUTERS:
-------------
1. Log in to router admin (http://$gateway)
2. Navigate to LAN > DHCP Server
3. Under "DNS Server", enter $localip
4. Click "Apply" to save
TP-LINK ROUTERS:
---------------
1. Log in to router admin (http://$gateway)
2. Go to Advanced > Network > DHCP Server
3. Set "Primary DNS" to $localip
4. Click "Save" to apply
GENERAL NOTES:
--------------
- Changes affect all DHCP clients
- Some routers allow multiple DNS servers (comma-separated)
- Restart devices to receive new DNS settings
- after waiting up to 4 hours for changes to propagate: Verify DNS propagation with: nslookup $domain
EOF
section=$((section + 1))
fi
if [ "$SHOULD_PAUSE" -eq "false" ]; then
# 3. Nginx and Dockge Pre-Configuration
cat >> "$tasks_file" <<EOF
$section. NGINX AND DOCKGE PRE-CONFIGURATION
-------------------------------------------
NGINX:
------
go to http://$localip:81 and make the admin account
DOCKGE:
-------
go to http://$localip:5001 and make the admin account
FILEBROWSER:
------------
log into http://$localip:5001, go to the "browser" stack, in the console output, it should list the randomly generated admin password
log into http://browser.$domain using username "admin" and the randomly generated password
reset the password to your own password and change the username if desired.
EOF
section=$((section + 1))
fi
# 4. Certificate Configuration
if [ "$external_access_method" -eq 1 ]; then
# Public DNS method - Let's Encrypt
cat >> "$tasks_file" <<EOF
$section. CONFIGURE CERTIFICATES USING LET'S ENCRYPT
---------------------------------------------------
Make sure your DNS records are properly configured before proceeding.
# Step 1:
go to proxy.?domain? and log in with your admin account
# Step 2:
go to the "Certificates" tab and click Add Certificate > Let's Encrypt via HTTP
# Step 3:
copy the contents of the "npmcertlist.txt" file into the domain names field.
copy one line at a time if using ctrl+c/ctrl+v, be sure to press enter between each line.
if you make use of clickpaste (or any tool that allows you to paste by simulating keyboard input), you can use that to paste the entire list in one go
or you can type the entire list manually
# Step 4:
hit "save" and wait.
# Step 5:
go to the Hosts > proxy hosts tab and go through each of the hosts
# Step 6:
repeat for each host:
>go to the SSL tab
>select your certificate
>enable force SSL
in the case of owncloud you may also want to enable HTTP/2
then hit save.
once you've done this for all your sites, your entire cloud is now running with SSL encryption.
EOF
else
# Local DNS methods - Self-signed certs
cat >> "$tasks_file" <<EOF
$section. CONFIGURE CERTIFICATES USING GENCERTS.SH
------------------------------------------------
run the gencerts.sh script, this will generate the certs for you and put them in a folder inside browser.$domain which should now be reachable over http
on http://$localip:81, log in, go to certificates > add certificate > custom
fill in your main domain as the Name
for the certificate key, use the wildcard.key file
for the certificate, use the wildcard.crt file
for the intermediate certificate, use the intermediate.crt file
because this certificate is not backed by a public certificate authority like letsencrypt, you have to manually trust the root cert on each device you want to use the cloud on, or deal with "certificate untrusted" warnings.
below are guides for doing this:
windows:
download and double-click rootCA.pfx
select "Local Machine" and click next, password is empty.
choose "place all certificates in the following store" and choose "Trusted Root Certification Authorities"
click finish and confirm with "yes" if prompted.
macOS:
download and double-click rootCA.pfx
if prompted for a password, leave it blank and click yes
open Keychain Access (applications/utilities/keychain access)
locate the imported rootCA.pfx certificate in the login or system keychains.
double-click the certificate, expand the "trust" section and set "When using this certificate" to "always trust"
Linux:
download the rootCA.crt file
copy rootCA.crt to /usr/local/share/ca-certificates/ using the following command from the directory rootCA.crt is in, or by using your file manager.
sudo cp rootCA.crt /usr/local/share/ca-certificates/
then update the CA store by rebooting or running the following command:
sudo update-ca-certificates
android:
download rootCA.crt to your device
open settings > security > encryption & credentials > install a certificate
select rootCA.crt and set a Name
reboot if prompted
IOS:
download rootCA.crt to your device
open the file in safari and tap "install"
go to settings > general > VPN & Device management > configuration profile and install the certificate
enable full trust in settings > general > about > certificate trust settings
OWNCLOUD:
for owncloud's onlyoffice integration, you'll have to disable certificate validation in the settings menu.
now that you've set your DNS correctly and trusted the cert, you should be able to visit all of your sites via https://dash.$domain
the certificate is valid for 10 years, after which you can generate a new one with gencerts.sh
EOF
fi
section=$((section + 1))
# 6. Service Configurations
if [ "$SHOULD_PAUSE" -eq "false" ]; then
cat >> "$tasks_file" <<EOF
$section. CONFIGURE SERVICES
---------------------------
Complete the initial setup for these services:
JELLYFIN:
log into https://video.$domain and run through the first-time setup.
BOOKSTACK:
<include_content_here: instructions for initial BookStack setup including admin account creation>
EOF
section=$((section + 1))
fi
# 7. Uptime Kuma Configuration (last since it depends on DNS and HTTPS)
cat >> "$tasks_file" <<EOF
$section. CONFIGURE UPTIME KUMA
---------------------------
MONITORING:
-----------
# Step 1:
head to status.?domain? and log in.
# Step 2:
click on "Add New Monitor"
# Step 2.5 (optional):
Set monitor type to "Group", friendly name to the group name and retries to 10.
repeat this for any groups you want, for example: Frontend and Backend
# Step 3:
use the following settings:
type: HTTP(s)
Friendly Name: Name of your service (for example: Cloud)
url: the URL for the site (for example: https://cloud.?domain?)
Retries: 10
certificate expiry notification: on
cachebuster: on
group: optionally set this to the group you want
leave all the other settings as default
# Step 4:
repeat step 3 for every service, for an easy list of URLs, go to proxy.?domain? and log in, you can see the list of proxy hosts which is also all of your URLs.
# Step 5 (optional):
make a status page, instructions in Uptime Kuma - status page.txt
STATUS PAGE:
------------
# Step 1:
log in to status.?domain and click on "Status Pages" in the top right.
# Step 2:
make a new status page, Name it whatever you want and use slug "default"
# Step 3: add all of the monitors you want to the page.
# step 4: add a description if you'd like.
this status page is available at status.?domain?/status
any other status pages you make are available at status.?domain?/status/<slug>
EOF
# Add footer with additional information
cat >> "$tasks_file" <<EOF
ADDITIONAL INFORMATION
======================
ACCESS URLs:
- PiHole Admin: http://$localip/admin
- Nginx Proxy Manager: http://$localip:81
<include_content_here: list all other service URLs>
IMPORTANT NOTES:
- Your local network is: $network_address/$netmask
- Your broadcast address is: $broadcast_address
<include_content_here: any other important notes>
EOF
# Output success message
echo "Post-install tasks file generated: $tasks_file"
echo "Please follow the tasks in order as some services depend on others!"
echo "if you are using public DNS with a static ip, you can also already access this from http://browser.$domain, if you are using local DNS, you will need to do additional configuration"
echo "you can open this file on this machine using the command 'sudo nano $tasks_file'"
echo ""
echo "for your convenience, the tasks file will be copied to the current user's home directory"
cp $tasks_file ~/post-install.txt
# Capture the end time (Unix timestamp)
END_TIME=$(date +%s)
# Calculate the elapsed time
ELAPSED_TIME=$((END_TIME - START_TIME))
echo "Script took $ELAPSED_TIME seconds to complete."
echo "please reboot before further use"