From bf455c6e14385cc2fae67bd30a10452d0d181d1b Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 09:01:11 +0100 Subject: [PATCH 01/36] Enhance macOS compatibility in installation script - Added checks to ensure the script is executed, not sourced. - Improved OS detection and handling for macOS and Linux. - Updated directory paths for macOS. - Introduced cross-platform helper functions for downloading files and modifying files. - Added functionality to patch the .dappnode_profile for macOS compatibility. - Enhanced user shell configuration to source the profile correctly. - Cleaned up rc.local for Linux installations. - Improved logging and user feedback during installation. --- scripts/dappnode_install.sh | 335 ++++++++++++++++++++++++++++-------- 1 file changed, 266 insertions(+), 69 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index fc58ff1..0ecf75c 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -1,18 +1,59 @@ #!/bin/bash +# This installer is written for bash. It's safe to *run it from zsh* (it will execute via bash +# thanks to the shebang), but users sometimes invoke it as `zsh ./script.sh` or `source ./script.sh`. +# - If sourced, bail out (sourcing would pollute the current shell and can break it). +# - If invoked by a non-bash shell, re-exec with bash before hitting bash-specific builtins. +if (return 0 2>/dev/null); then + echo "This script must be executed, not sourced. Run: bash $0" + return 1 +fi + +if [ -z "${BASH_VERSION:-}" ]; then + exec /usr/bin/env bash "$0" "$@" +fi + +set -eo pipefail + +# Enable alias expansion in non-interactive bash scripts. +# Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. +shopt -s expand_aliases + +################## +# OS DETECTION # +################## +OS_TYPE="$(uname -s)" +IS_MACOS=false +IS_LINUX=false +if [[ "$OS_TYPE" == "Darwin" ]]; then + IS_MACOS=true +elif [[ "$OS_TYPE" == "Linux" ]]; then + IS_LINUX=true +else + echo "Unsupported operating system: $OS_TYPE" + exit 1 +fi + ############# # VARIABLES # ############# -# Dirs -DAPPNODE_DIR="/usr/src/dappnode" +# Dirs - macOS uses $HOME/dappnode, Linux uses /usr/src/dappnode +if $IS_MACOS; then + DAPPNODE_DIR="$HOME/dappnode" +else + DAPPNODE_DIR="/usr/src/dappnode" +fi DAPPNODE_CORE_DIR="${DAPPNODE_DIR}/DNCORE" LOGS_DIR="$DAPPNODE_DIR/logs" # Files CONTENT_HASH_FILE="${DAPPNODE_CORE_DIR}/packages-content-hash.csv" LOGFILE="${LOGS_DIR}/dappnode_install.log" -MOTD_FILE="/etc/motd" -UPDATE_MOTD_DIR="/etc/update-motd.d" DAPPNODE_PROFILE="${DAPPNODE_CORE_DIR}/.dappnode_profile" +# Linux-only paths +if $IS_LINUX; then + MOTD_FILE="/etc/motd" + UPDATE_MOTD_DIR="/etc/update-motd.d" +fi # Get URLs PROFILE_BRANCH=${PROFILE_BRANCH:-"master"} IPFS_ENDPOINT=${IPFS_ENDPOINT:-"http://ipfs.io"} @@ -20,12 +61,75 @@ IPFS_ENDPOINT=${IPFS_ENDPOINT:-"http://ipfs.io"} PROFILE_URL=${PROFILE_URL:-"https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_profile.sh"} DAPPNODE_ACCESS_CREDENTIALS="${DAPPNODE_DIR}/scripts/dappnode_access_credentials.sh" DAPPNODE_ACCESS_CREDENTIALS_URL="https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_access_credentials.sh" -WGET="wget -q --show-progress --progress=bar:force" -SWGET="wget -q -O-" # Other -CONTENT_HASH_PKGS=(geth besu nethermind erigon prysm teku lighthouse nimbus lodestar) -ARCH=$(dpkg --print-architecture) -WELCOME_MESSAGE="\nChoose a way to connect to your DAppNode, then go to \e[1mhttp://my.dappnode\e[0m\n\n\e[1m- Wifi\e[0m\t\tScan and connect to DAppNodeWIFI. Get wifi credentials with \e[32mdappnode_wifi\e[0m\n\n\e[1m- Local Proxy\e[0m\tConnect to the same router as your DAppNode. Then go to \e[1mhttp://dappnode.local\e[0m\n\n\e[1m- Wireguard\e[0m\tDownload Wireguard app on your device. Get your dappnode wireguard credentials with \e[32mdappnode_wireguard\e[0m\n\n\e[1m- Open VPN\e[0m\tDownload OPen VPN app on your device. Get your openVPN creds with \e[32mdappnode_openvpn\e[0m\n\n\nTo see a full list of commands available execute \e[32mdappnode_help\e[0m\n" + +# Architecture detection (cross-platform) +if $IS_MACOS; then + ARCH=$(uname -m) + [[ "$ARCH" == "x86_64" ]] && ARCH="amd64" + # arm64 is already correct for Apple Silicon +else + ARCH=$(dpkg --print-architecture) +fi + +############################## +# Cross-platform Helpers # +############################## + +# Download a file: download_file +download_file() { + local dest="$1" + local url="$2" + if $IS_MACOS; then + curl -sL -o "$dest" "$url" + else + wget -q --show-progress --progress=bar:force -O "$dest" "$url" + fi +} + +# Download content to stdout: download_stdout +download_stdout() { + local url="$1" + if $IS_MACOS; then + curl -sL "$url" + else + wget -q -O- "$url" + fi +} + +# Cross-platform in-place sed (macOS requires '' after -i) +sed_inplace() { + if $IS_MACOS; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +############################## +# Compose Patching Helpers # +############################## + +# Remove journald logging from compose files (not supported on macOS Docker Desktop) +remove_logging_section() { + local file="$1" + sed_inplace '/logging/d;/journald/d' "$file" +} + +# Replace Linux paths with macOS paths in compose files +patch_compose_paths() { + local file="$1" + sed_inplace "s|/usr/src/dappnode|${DAPPNODE_DIR}|g" "$file" +} + +# Patch .dappnode_profile for macOS compatibility +patch_profile_for_macos() { + local profile="$1" + # Replace GNU find -printf with POSIX-compatible -exec printf + sed_inplace 's/-printf "-f %p "/-exec printf -- "-f %s " {} \\;/' "$profile" + # Replace hardcoded Linux paths with $HOME-based paths + sed_inplace 's|/usr/src/dappnode|\$HOME/dappnode|g' "$profile" +} # Clean if update if [ "$UPDATE" = true ]; then @@ -49,6 +153,11 @@ mkdir -p $LOGS_DIR # TEMPORARY: think a way to integrate flags instead of use files to detect installation type is_iso_install() { + # ISO installs are Linux-only + if $IS_MACOS; then + IS_ISO_INSTALL=false + return + fi # Check old and new location of iso_install.log if [ -f "${DAPPNODE_DIR}/iso_install.log" ] || [ -f "${DAPPNODE_DIR}/logs/iso_install.log" ]; then IS_ISO_INSTALL=true @@ -126,7 +235,13 @@ if [[ -n "$STATIC_IP" ]]; then fi # Loads profile, if not exists it means it is script install so the versions will be fetched from the latest profile -[ -f $DAPPNODE_PROFILE ] || ${WGET} -O ${DAPPNODE_PROFILE} ${PROFILE_URL} +[ -f "$DAPPNODE_PROFILE" ] || download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" + +# Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) +if $IS_MACOS; then + patch_profile_for_macos "$DAPPNODE_PROFILE" +fi + # shellcheck disable=SC1090 source "${DAPPNODE_PROFILE}" @@ -153,6 +268,10 @@ dappnode_core_build() { for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" if [[ ${!ver} == dev:* ]]; then + if $IS_MACOS; then + echo "Development builds (dev:*) are not supported on macOS." + exit 1 + fi echo "Cloning & building DNP_${comp}..." if ! dpkg -s git >/dev/null 2>&1; then apt-get install -y git @@ -182,15 +301,28 @@ dappnode_core_download() { for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" if [[ ${!ver} != dev:* ]]; then - # Download DAppNode Core Images if it's needed + local file_var="${comp}_FILE" + local url_var="${comp}_URL" + local yml_file_var="${comp}_YML_FILE" + local yml_var="${comp}_YML" + local manifest_file_var="${comp}_MANIFEST_FILE" + local manifest_var="${comp}_MANIFEST" + + # Download DAppNode Core Images if needed echo "Downloading ${comp} tar..." - eval "[ -f \$${comp}_FILE ] || $WGET -O \$${comp}_FILE \$${comp}_URL || exit 1" - # Download DAppNode Core docker-compose yml files if it's needed + [ -f "${!file_var}" ] || download_file "${!file_var}" "${!url_var}" || exit 1 + # Download DAppNode Core docker-compose yml files if needed echo "Downloading ${comp} yml..." - eval "[ -f \$${comp}_YML_FILE ] || $WGET -O \$${comp}_YML_FILE \$${comp}_YML || exit 1" - # Download DAppNode Core manifest files if it's needed + [ -f "${!yml_file_var}" ] || download_file "${!yml_file_var}" "${!yml_var}" || exit 1 + # Download DAppNode Core manifest files if needed echo "Downloading ${comp} manifest..." - eval "[ -f \$${comp}_MANIFEST_FILE ] || $WGET -O \$${comp}_MANIFEST_FILE \$${comp}_MANIFEST || exit 1" + [ -f "${!manifest_file_var}" ] || download_file "${!manifest_file_var}" "${!manifest_var}" || exit 1 + + # macOS: patch compose files for Docker Desktop compatibility + if $IS_MACOS; then + remove_logging_section "${!yml_file_var}" + patch_compose_paths "${!yml_file_var}" + fi fi done } @@ -205,7 +337,6 @@ dappnode_core_load() { } customMotd() { - generateMotdText if [ -d "${UPDATE_MOTD_DIR}" ]; then @@ -216,6 +347,8 @@ customMotd() { # Debian distros use /etc/motd plain text file generateMotdText() { + local welcome_message + # Check and create the MOTD file if it does not exist if [ ! -f "${MOTD_FILE}" ]; then touch "${MOTD_FILE}" @@ -229,7 +362,8 @@ generateMotdText() { |___/\__,_| .__/ .__/_||_\___/\__,_\___| |_| |_| EOF - echo -e "$WELCOME_MESSAGE" >>"${MOTD_FILE}" + welcome_message="\nChoose a way to connect to your DAppNode, then go to \e[1mhttp://my.dappnode\e[0m\n\n\e[1m- Wifi\e[0m\t\tScan and connect to DAppNodeWIFI. Get wifi credentials with \e[32mdappnode_wifi\e[0m\n\n\e[1m- Local Proxy\e[0m\tConnect to the same router as your DAppNode. Then go to \e[1mhttp://dappnode.local\e[0m\n\n\e[1m- Wireguard\e[0m\tDownload Wireguard app on your device. Get your dappnode wireguard credentials with \e[32mdappnode_wireguard\e[0m\n\n\e[1m- Open VPN\e[0m\tDownload OPen VPN app on your device. Get your openVPN creds with \e[32mdappnode_openvpn\e[0m\n\n\nTo see a full list of commands available execute \e[32mdappnode_help\e[0m\n" + printf "%b" "$welcome_message" >>"${MOTD_FILE}" } # Ubuntu distros use /etc/update-motd.d/ to generate the motd @@ -269,6 +403,50 @@ addSwap() { fi } +# Add .dappnode_profile sourcing to the user's default shell configuration +add_profile_to_shell() { + local user_home + local shell_configs + + if $IS_MACOS; then + user_home="$HOME" + # macOS defaults to zsh + shell_configs=(".zshrc" ".zprofile") + else + # Linux: determine user home from /etc/passwd + local user_name + user_name=$(grep 1000 /etc/passwd | cut -f 1 -d:) + if [ -n "$user_name" ]; then + user_home="/home/$user_name" + else + user_home="/root" + fi + shell_configs=(".profile" ".bashrc") + fi + + for config_file in "${shell_configs[@]}"; do + local config_path="${user_home}/${config_file}" + local source_line + + # .profile may be evaluated by /bin/sh (dash on Debian/Ubuntu) where `source` is not valid. + # Use POSIX '.' there; use `source` elsewhere (bash/zsh). + if [ "$config_file" = ".profile" ]; then + source_line="[ -f \"${DAPPNODE_PROFILE}\" ] && . \"${DAPPNODE_PROFILE}\"" + else + source_line="[ -f \"${DAPPNODE_PROFILE}\" ] && source \"${DAPPNODE_PROFILE}\"" + fi + + # Create config file if it doesn't exist + [ ! -f "$config_path" ] && touch "$config_path" + # Add profile sourcing if not already present + if ! grep -q "${DAPPNODE_PROFILE}" "$config_path"; then + echo "######## DAPPNODE PROFILE ########" >> "$config_path" + echo "$source_line" >> "$config_path" + echo "" >> "$config_path" + fi + done +} + dappnode_start() { echo -e "\e[32mDAppNode starting...\e[0m" 2>&1 | tee -a $LOGFILE # shellcheck disable=SC1090 @@ -282,33 +460,20 @@ dappnode_start() { done echo -e "\e[32mDAppNode started\e[0m" 2>&1 | tee -a $LOGFILE - # Show credentials to the user on login - USER=$(grep 1000 /etc/passwd | cut -f 1 -d:) - [ -n "$USER" ] && USER_HOME=/home/$USER || USER_HOME=/root + # Add profile sourcing to user's shell configuration + add_profile_to_shell - # Add profile sourcing to both .profile and .bashrc for maximum compatibility - for config_file in .profile .bashrc; do - CONFIG_PATH="$USER_HOME/$config_file" - - # Create config file if it doesn't exist - [ ! -f "$CONFIG_PATH" ] && touch "$CONFIG_PATH" - - # Add profile sourcing if not already present - if ! grep -q "${DAPPNODE_PROFILE}" "$CONFIG_PATH"; then - echo "######## DAPPNODE PROFILE ########" >>"$CONFIG_PATH" - echo -e "source ${DAPPNODE_PROFILE}\n" >>"$CONFIG_PATH" - fi - done - - # Remove return from profile - sed -i '/return/d' $DAPPNODE_PROFILE | tee -a $LOGFILE + # Remove return from profile so it can be sourced in login shells + sed_inplace '/return/d' "$DAPPNODE_PROFILE" # Download access_credentials script - [ -f $DAPPNODE_ACCESS_CREDENTIALS ] || ${WGET} -O ${DAPPNODE_ACCESS_CREDENTIALS} ${DAPPNODE_ACCESS_CREDENTIALS_URL} + [ -f "$DAPPNODE_ACCESS_CREDENTIALS" ] || download_file "${DAPPNODE_ACCESS_CREDENTIALS}" "${DAPPNODE_ACCESS_CREDENTIALS_URL}" - # Delete dappnode_install.sh execution from rc.local if exists, and is not the unattended firstboot - if [ -f "/etc/rc.local" ] && [ ! -f "/usr/src/dappnode/.firstboot" ]; then - sed -i '/\/usr\/src\/dappnode\/scripts\/dappnode_install.sh/d' /etc/rc.local 2>&1 | tee -a $LOGFILE + # Linux-only: clean up rc.local + if $IS_LINUX; then + if [ -f "/etc/rc.local" ] && [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then + sed_inplace '/\/usr\/src\/dappnode\/scripts\/dappnode_install.sh/d' /etc/rc.local 2>&1 | tee -a $LOGFILE + fi fi # Display help message to the user @@ -323,8 +488,9 @@ installExtraDpkg() { grabContentHashes() { if [ ! -f "${CONTENT_HASH_FILE}" ]; then - for comp in "${CONTENT_HASH_PKGS[@]}"; do - CONTENT_HASH=$(eval "${SWGET}" https://github.com/dappnode/DAppNodePackage-"${comp}"/releases/latest/download/content-hash) + local content_hash_pkgs=(geth besu nethermind erigon prysm teku lighthouse nimbus lodestar) + for comp in "${content_hash_pkgs[@]}"; do + CONTENT_HASH=$(download_stdout "https://github.com/dappnode/DAppNodePackage-${comp}/releases/latest/download/content-hash") if [ -z "$CONTENT_HASH" ]; then echo "ERROR! Failed to find content hash of ${comp}." 2>&1 | tee -a $LOGFILE exit 1 @@ -382,31 +548,35 @@ echo -e "\e[32m\n##############################################\e[0m" 2>&1 | tee echo -e "\e[32m#### DAPPNODE INSTALLER ####\e[0m" 2>&1 | tee -a $LOGFILE echo -e "\e[32m##############################################\e[0m" 2>&1 | tee -a $LOGFILE -echo -e "\e[32mCreating swap memory...\e[0m" 2>&1 | tee -a $LOGFILE -addSwap +# --- Linux-only setup steps --- +if $IS_LINUX; then + echo -e "\e[32mCreating swap memory...\e[0m" 2>&1 | tee -a $LOGFILE + addSwap + + echo -e "\e[32mCustomizing login...\e[0m" 2>&1 | tee -a $LOGFILE + customMotd -echo -e "\e[32mCustomizing login...\e[0m" 2>&1 | tee -a $LOGFILE -customMotd + echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE + installExtraDpkg -echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE -installExtraDpkg + echo -e "\e[32mGrabbing latest content hashes...\e[0m" 2>&1 | tee -a $LOGFILE + grabContentHashes -echo -e "\e[32mGrabbing latest content hashes...\e[0m" 2>&1 | tee -a $LOGFILE -grabContentHashes + if [ "$ARCH" == "amd64" ]; then + echo -e "\e[32mInstalling SGX modules...\e[0m" 2>&1 | tee -a $LOGFILE + installSgx -if [ "$ARCH" == "amd64" ]; then - echo -e "\e[32mInstalling SGX modules...\e[0m" 2>&1 | tee -a $LOGFILE - installSgx + echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE + installExtraDpkg # TODO: Why is this being called twice? + fi - echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE - installExtraDpkg # TODO: Why is this being called twice? + echo -e "\e[32mAdding user to docker group...\e[0m" 2>&1 | tee -a $LOGFILE + addUserToDockerGroup fi -echo -e "\e[32mAdding user to docker group...\e[0m" 2>&1 | tee -a $LOGFILE -addUserToDockerGroup - +# --- Common steps (Linux and macOS) --- echo -e "\e[32mCreating dncore_network if needed...\e[0m" 2>&1 | tee -a $LOGFILE -docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a $LOGFILE +docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a $LOGFILE || true echo -e "\e[32mBuilding DAppNode Core if needed...\e[0m" 2>&1 | tee -a $LOGFILE dappnode_core_build @@ -417,18 +587,45 @@ dappnode_core_download echo -e "\e[32mLoading DAppNode Core...\e[0m" 2>&1 | tee -a $LOGFILE dappnode_core_load -if [ ! -f "/usr/src/dappnode/.firstboot" ]; then +# --- Start DAppNode --- +if $IS_LINUX; then + if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then + echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE + dappnode_start + fi + + # Run test in interactive terminal (first boot only) + if [ -f "${DAPPNODE_DIR}/.firstboot" ]; then + apt-get update + apt-get install -y kbd + openvt -s -w -- sudo -u root "${DAPPNODE_DIR}/scripts/dappnode_test_install.sh" + exit 0 + fi +fi + +if $IS_MACOS; then echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE dappnode_start -fi -# Run test in interactive terminal -if [ -f "/usr/src/dappnode/.firstboot" ]; then - # ensure openvt is installed prior to using it - apt-get update - apt-get install -y kbd - openvt -s -w -- sudo -u root /usr/src/dappnode/scripts/dappnode_test_install.sh - exit 0 + echo -e "\n\e[33mWaiting for VPN initialization...\e[0m" + sleep 10 + + echo -e "\n\e[32m##############################################\e[0m" + echo -e "\e[32m# DAppNode VPN Access Credentials #\e[0m" + echo -e "\e[32m##############################################\e[0m" + echo -e "\n\e[1mYour DAppNode is ready! Connect using your preferred VPN client.\e[0m" + echo -e "\e[1mChoose either Wireguard (recommended) or OpenVPN and import the\e[0m" + echo -e "\e[1mcredentials below into your VPN app to access your DAppNode.\e[0m\n" + + echo -e "\e[1m--- Wireguard ---\e[0m" + dappnode_wireguard --localhost 2>&1 || \ + echo -e "\e[33mWireguard credentials not yet available. Try later with: dappnode_wireguard --localhost\e[0m" + + echo -e "\n\e[1m--- OpenVPN ---\e[0m" + dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ + echo -e "\e[33mOpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost\e[0m" + + echo -e "\n\e[32mImport the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode\e[0m" fi exit 0 From f4b69b96cba83e4eb04dcc149c54f73181f466fc Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 10:15:21 +0100 Subject: [PATCH 02/36] Add macOS support for installation scripts and update DAppNode core start function --- .github/workflows/test.yml | 15 ++++++++++++++- scripts/dappnode_install.sh | 24 ++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7c85e1..70395d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,13 +31,26 @@ jobs: run: | sudo /bin/bash ./scripts/dappnode_uninstall.sh y + scripts-macos: + name: test scripts (macOS) + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install Docker + uses: docker-practice/actions-setup-docker@master # TODO: use pre-install script instead + - name: Install DAppNode + run: | + /bin/bash ./scripts/dappnode_install.sh + iso: name: test Debian and Ubuntu ISO runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Create Debian ISO - name: create Debian ISO diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 0ecf75c..2d09102 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -122,6 +122,7 @@ patch_compose_paths() { sed_inplace "s|/usr/src/dappnode|${DAPPNODE_DIR}|g" "$file" } +# TODO: remove once profile macos-compatibility published # Patch .dappnode_profile for macOS compatibility patch_profile_for_macos() { local profile="$1" @@ -238,6 +239,7 @@ fi [ -f "$DAPPNODE_PROFILE" ] || download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" # Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) +# TODO: remove once profile macos-compatibility published if $IS_MACOS; then patch_profile_for_macos "$DAPPNODE_PROFILE" fi @@ -447,17 +449,11 @@ add_profile_to_shell() { done } -dappnode_start() { +dappnode_core_start() { echo -e "\e[32mDAppNode starting...\e[0m" 2>&1 | tee -a $LOGFILE - # shellcheck disable=SC1090 - source "${DAPPNODE_PROFILE}" >/dev/null 2>&1 - - # Execute `compose-up` independently - # To execute `compose-up` against more than 1 compose, composes files must share compose file version (e.g 3.5) - for comp in "${DNCORE_YMLS_ARRAY[@]}"; do - docker compose -f "$comp" up -d 2>&1 | tee -a $LOGFILE - echo "${comp} started" 2>&1 | tee -a $LOGFILE - done + + # Use DNCORE_YMLS from the profile (populated after re-sourcing post-download) + docker compose $DNCORE_YMLS up -d 2>&1 | tee -a $LOGFILE echo -e "\e[32mDAppNode started\e[0m" 2>&1 | tee -a $LOGFILE # Add profile sourcing to user's shell configuration @@ -584,6 +580,10 @@ dappnode_core_build echo -e "\e[32mDownloading DAppNode Core...\e[0m" 2>&1 | tee -a $LOGFILE dappnode_core_download +# Re-source profile now that compose files exist, so DNCORE_YMLS is populated +# shellcheck disable=SC1090 +source "${DAPPNODE_PROFILE}" + echo -e "\e[32mLoading DAppNode Core...\e[0m" 2>&1 | tee -a $LOGFILE dappnode_core_load @@ -591,7 +591,7 @@ dappnode_core_load if $IS_LINUX; then if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE - dappnode_start + dappnode_core_start fi # Run test in interactive terminal (first boot only) @@ -605,7 +605,7 @@ fi if $IS_MACOS; then echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE - dappnode_start + dappnode_core_start echo -e "\n\e[33mWaiting for VPN initialization...\e[0m" sleep 10 From 09ffb8130f4af3878226d979a283f8d1412290e3 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 10:25:30 +0100 Subject: [PATCH 03/36] Normalize component names to lowercase in download URL and file paths --- scripts/dappnode_install.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 2d09102..b8ec11d 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -258,12 +258,13 @@ for comp in "${PKGS[@]}"; do if [[ ${!ver} == /ipfs/* ]]; then DOWNLOAD_URL="${IPFS_ENDPOINT}/api/v0/cat?arg=${!ver%:*}" fi - eval "${comp}_URL=\"${DOWNLOAD_URL}/${comp,,}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" + comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') + eval "${comp}_URL=\"${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" eval "${comp}_YML=\"${DOWNLOAD_URL}/docker-compose.yml\"" eval "${comp}_MANIFEST=\"${DOWNLOAD_URL}/dappnode_package.json\"" - eval "${comp}_YML_FILE=\"${DAPPNODE_CORE_DIR}/docker-compose-${comp,,}.yml\"" - eval "${comp}_FILE=\"${DAPPNODE_CORE_DIR}/${comp,,}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" - eval "${comp}_MANIFEST_FILE=\"${DAPPNODE_CORE_DIR}/dappnode_package-${comp,,}.json\"" + eval "${comp}_YML_FILE=\"${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml\"" + eval "${comp}_FILE=\"${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" + eval "${comp}_MANIFEST_FILE=\"${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json\"" done dappnode_core_build() { From 84871ee7944b454c46ec63ded70855bd4d3111c5 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 10:29:21 +0100 Subject: [PATCH 04/36] Update macOS test script to install Docker using Homebrew --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70395d2..bf4cc5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,10 @@ jobs: - name: Checkout uses: actions/checkout@v6 - name: Install Docker - uses: docker-practice/actions-setup-docker@master # TODO: use pre-install script instead + run: | + brew install --cask docker + open -a Docker + while ! docker info > /dev/null 2>&1; do sleep 2; done # TODO: use pre-install script instead - name: Install DAppNode run: | /bin/bash ./scripts/dappnode_install.sh From 4d4d36375cfe3cdecd60acbf35c455a6f14d340c Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 10:29:52 +0100 Subject: [PATCH 05/36] Normalize component names to lowercase in Docker image loading logic --- scripts/dappnode_install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index b8ec11d..0b5a3f9 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -334,7 +334,8 @@ dappnode_core_load() { for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" if [[ ${!ver} != dev:* ]]; then - eval "[ ! -z \$(docker images -q ${comp,,}.dnp.dappnode.eth:${!ver##*:}) ] || docker load -i \$${comp}_FILE 2>&1 | tee -a \$LOGFILE" + comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') + eval "[ ! -z \$(docker images -q ${comp_lower}.dnp.dappnode.eth:${!ver##*:}) ] || docker load -i \$${comp}_FILE 2>&1 | tee -a \$LOGFILE" fi done } From b4229ef779097ed10f9b6ad8f4c90fa7bc2e4530 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 27 Feb 2026 10:46:31 +0100 Subject: [PATCH 06/36] Remove macOS test scripts from workflow configuration --- .github/workflows/test.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf4cc5f..174f84c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,22 +31,6 @@ jobs: run: | sudo /bin/bash ./scripts/dappnode_uninstall.sh y - scripts-macos: - name: test scripts (macOS) - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Install Docker - run: | - brew install --cask docker - open -a Docker - while ! docker info > /dev/null 2>&1; do sleep 2; done # TODO: use pre-install script instead - - name: Install DAppNode - run: | - /bin/bash ./scripts/dappnode_install.sh - iso: name: test Debian and Ubuntu ISO runs-on: ubuntu-latest From 68d27362eeb2b4ec9d25c59a18ff85aedb1e448f Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Mon, 2 Mar 2026 10:24:30 +0100 Subject: [PATCH 07/36] Update IPFS endpoint and enhance logging in installation script --- scripts/dappnode_install.sh | 177 +++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 42 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 0b5a3f9..1a6d95b 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -56,7 +56,7 @@ if $IS_LINUX; then fi # Get URLs PROFILE_BRANCH=${PROFILE_BRANCH:-"master"} -IPFS_ENDPOINT=${IPFS_ENDPOINT:-"http://ipfs.io"} +IPFS_ENDPOINT=${IPFS_ENDPOINT:-"https://ipfs-gateway-dev.dappnode.net"} # PROFILE_URL env is used to fetch the core packages versions that will be used to build the release in script install method PROFILE_URL=${PROFILE_URL:-"https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_profile.sh"} DAPPNODE_ACCESS_CREDENTIALS="${DAPPNODE_DIR}/scripts/dappnode_access_credentials.sh" @@ -72,6 +72,21 @@ else ARCH=$(dpkg --print-architecture) fi +# Color output helpers +color_echo() { + local color="$1"; shift + if $IS_LINUX; then + case "$color" in + green) code="\e[32m" ;; + yellow) code="\e[33m" ;; + *) code="" ;; + esac + echo -e "${code}$*\e[0m" + else + echo "$*" + fi +} + ############################## # Cross-platform Helpers # ############################## @@ -80,6 +95,7 @@ fi download_file() { local dest="$1" local url="$2" + echo "[DEBUG] Downloading from $url to $dest" 2>&1 | tee -a $LOGFILE if $IS_MACOS; then curl -sL -o "$dest" "$url" else @@ -91,12 +107,66 @@ download_file() { download_stdout() { local url="$1" if $IS_MACOS; then - curl -sL "$url" + curl -fsSL "$url" else wget -q -O- "$url" fi } +# Normalize IPFS refs and (if needed) infer the missing : from dappnode_package.json +# Accepts: +# - /ipfs/: +# - /ipfs/ (version inferred) +# - ipfs/[:] (leading slash normalized) +normalize_ipfs_version_ref() { + local raw_ref="$1" + local comp="$2" + local ref="$raw_ref" + + if [[ "$ref" == ipfs/* ]]; then + ref="/$ref" + fi + + # If it already has :, we're done + if [[ "$ref" == /ipfs/*:* ]]; then + echo "$ref" + return 0 + fi + + # If it's an IPFS ref without a :, infer it from the manifest in the CID + if [[ "$ref" == /ipfs/* ]]; then + local cid_path="$ref" + local manifest_url="${IPFS_ENDPOINT%/}${cid_path}/dappnode_package.json" + local manifest + manifest="$(download_stdout "$manifest_url" 2>/dev/null || true)" + if [[ -z "$manifest" ]]; then + echo "[ERROR] Could not fetch IPFS manifest for ${comp} from: $manifest_url" 1>&2 + echo "[ERROR] Provide ${comp}_VERSION as /ipfs/: (example: /ipfs/Qm...:0.2.11)" 1>&2 + return 1 + fi + + local inferred_version + inferred_version="$( + echo "$manifest" | + tr -d '\r' | + grep -m1 '"version"' | + sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^\"]+)".*/\1/' + )" + + if [[ -z "$inferred_version" || "$inferred_version" == "$manifest" ]]; then + echo "[ERROR] Could not infer version for ${comp} from IPFS manifest: $manifest_url" 1>&2 + echo "[ERROR] Provide ${comp}_VERSION as /ipfs/:" 1>&2 + return 1 + fi + + echo "${cid_path}:${inferred_version}" + return 0 + fi + + # Not an IPFS ref; return as-is + echo "$raw_ref" +} + # Cross-platform in-place sed (macOS requires '' after -i) sed_inplace() { if $IS_MACOS; then @@ -196,18 +266,25 @@ determine_packages() { is_port_used if [ "$IS_ISO_INSTALL" == "false" ]; then if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER) else - PKGS=(HTTPS BIND IPFS WIREGUARD DAPPMANAGER WIFI) + PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) fi else if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS WIREGUARD DAPPMANAGER WIFI) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER) else - PKGS=(HTTPS BIND IPFS WIREGUARD DAPPMANAGER WIFI) + PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) fi fi - echo -e "\e[32mPackages to be installed: ${PKGS[*]}\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Packages to be installed: ${PKGS[*]}" 2>&1 | tee -a $LOGFILE + + # Debug: print all PKGS and their version variables + echo "[DEBUG] PKGS: ${PKGS[*]}" 2>&1 | tee -a $LOGFILE + for comp in "${PKGS[@]}"; do + ver_var="${comp}_VERSION" + echo "[DEBUG] $ver_var = ${!ver_var}" 2>&1 | tee -a $LOGFILE + done } function valid_ip() { @@ -235,8 +312,14 @@ if [[ -n "$STATIC_IP" ]]; then fi fi -# Loads profile, if not exists it means it is script install so the versions will be fetched from the latest profile -[ -f "$DAPPNODE_PROFILE" ] || download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" + +# If LOCAL_PROFILE_PATH is set, use it as the profile source instead of downloading +if [ -n "$LOCAL_PROFILE_PATH" ]; then + echo "Using local profile: $LOCAL_PROFILE_PATH" | tee -a $LOGFILE + cp "$LOCAL_PROFILE_PATH" "$DAPPNODE_PROFILE" +elif [ ! -f "$DAPPNODE_PROFILE" ]; then + download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" +fi # Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) # TODO: remove once profile macos-compatibility published @@ -254,16 +337,26 @@ source "${DAPPNODE_PROFILE}" determine_packages for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" - DOWNLOAD_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${!ver}" - if [[ ${!ver} == /ipfs/* ]]; then - DOWNLOAD_URL="${IPFS_ENDPOINT}/api/v0/cat?arg=${!ver%:*}" + echo "[DEBUG] Processing $comp: ${!ver}" 2>&1 | tee -a $LOGFILE + + raw_version_ref="${!ver}" + if [[ "$raw_version_ref" == /ipfs/* || "$raw_version_ref" == ipfs/* ]]; then + resolved_ref="$(normalize_ipfs_version_ref "$raw_version_ref" "$comp")" || exit 1 + eval "${comp}_VERSION=\"${resolved_ref}\"" + raw_version_ref="$resolved_ref" + echo "[DEBUG] Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})" 2>&1 | tee -a $LOGFILE + DOWNLOAD_URL="${IPFS_ENDPOINT%/}${raw_version_ref%:*}" + version_for_filenames="${raw_version_ref##*:}" + else + version_for_filenames="${raw_version_ref##*:}" + DOWNLOAD_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${version_for_filenames}" fi comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') - eval "${comp}_URL=\"${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" + eval "${comp}_URL=\"${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz\"" eval "${comp}_YML=\"${DOWNLOAD_URL}/docker-compose.yml\"" eval "${comp}_MANIFEST=\"${DOWNLOAD_URL}/dappnode_package.json\"" eval "${comp}_YML_FILE=\"${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml\"" - eval "${comp}_FILE=\"${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${!ver##*:}_linux-${ARCH}.txz\"" + eval "${comp}_FILE=\"${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz\"" eval "${comp}_MANIFEST_FILE=\"${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json\"" done @@ -542,57 +635,57 @@ addUserToDockerGroup() { #### SCRIPT START #### ############################################## -echo -e "\e[32m\n##############################################\e[0m" 2>&1 | tee -a $LOGFILE -echo -e "\e[32m#### DAPPNODE INSTALLER ####\e[0m" 2>&1 | tee -a $LOGFILE -echo -e "\e[32m##############################################\e[0m" 2>&1 | tee -a $LOGFILE +color_echo green "\n##############################################" 2>&1 | tee -a $LOGFILE +color_echo green "#### DAPPNODE INSTALLER ####" 2>&1 | tee -a $LOGFILE +color_echo green "##############################################" 2>&1 | tee -a $LOGFILE # --- Linux-only setup steps --- if $IS_LINUX; then - echo -e "\e[32mCreating swap memory...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Creating swap memory..." 2>&1 | tee -a $LOGFILE addSwap - echo -e "\e[32mCustomizing login...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Customizing login..." 2>&1 | tee -a $LOGFILE customMotd - echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Installing extra packages..." 2>&1 | tee -a $LOGFILE installExtraDpkg - echo -e "\e[32mGrabbing latest content hashes...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Grabbing latest content hashes..." 2>&1 | tee -a $LOGFILE grabContentHashes if [ "$ARCH" == "amd64" ]; then - echo -e "\e[32mInstalling SGX modules...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Installing SGX modules..." 2>&1 | tee -a $LOGFILE installSgx - echo -e "\e[32mInstalling extra packages...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Installing extra packages..." 2>&1 | tee -a $LOGFILE installExtraDpkg # TODO: Why is this being called twice? fi - echo -e "\e[32mAdding user to docker group...\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "Adding user to docker group..." 2>&1 | tee -a $LOGFILE addUserToDockerGroup fi # --- Common steps (Linux and macOS) --- -echo -e "\e[32mCreating dncore_network if needed...\e[0m" 2>&1 | tee -a $LOGFILE +color_echo green "Creating dncore_network if needed..." 2>&1 | tee -a $LOGFILE docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a $LOGFILE || true -echo -e "\e[32mBuilding DAppNode Core if needed...\e[0m" 2>&1 | tee -a $LOGFILE +color_echo green "Building DAppNode Core if needed..." 2>&1 | tee -a $LOGFILE dappnode_core_build -echo -e "\e[32mDownloading DAppNode Core...\e[0m" 2>&1 | tee -a $LOGFILE +color_echo green "Downloading DAppNode Core..." 2>&1 | tee -a $LOGFILE dappnode_core_download # Re-source profile now that compose files exist, so DNCORE_YMLS is populated # shellcheck disable=SC1090 source "${DAPPNODE_PROFILE}" -echo -e "\e[32mLoading DAppNode Core...\e[0m" 2>&1 | tee -a $LOGFILE +color_echo green "Loading DAppNode Core..." 2>&1 | tee -a $LOGFILE dappnode_core_load # --- Start DAppNode --- if $IS_LINUX; then if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then - echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "DAppNode installed" 2>&1 | tee -a $LOGFILE dappnode_core_start fi @@ -606,28 +699,28 @@ if $IS_LINUX; then fi if $IS_MACOS; then - echo -e "\e[32mDAppNode installed\e[0m" 2>&1 | tee -a $LOGFILE + color_echo green "DAppNode installed" 2>&1 | tee -a $LOGFILE dappnode_core_start - echo -e "\n\e[33mWaiting for VPN initialization...\e[0m" + color_echo yellow "\nWaiting for VPN initialization..." sleep 10 - echo -e "\n\e[32m##############################################\e[0m" - echo -e "\e[32m# DAppNode VPN Access Credentials #\e[0m" - echo -e "\e[32m##############################################\e[0m" - echo -e "\n\e[1mYour DAppNode is ready! Connect using your preferred VPN client.\e[0m" - echo -e "\e[1mChoose either Wireguard (recommended) or OpenVPN and import the\e[0m" - echo -e "\e[1mcredentials below into your VPN app to access your DAppNode.\e[0m\n" + color_echo green "\n##############################################" + color_echo green "# DAppNode VPN Access Credentials #" + color_echo green "##############################################" + echo -e "\nYour DAppNode is ready! Connect using your preferred VPN client." + echo -e "Choose either Wireguard (recommended) or OpenVPN and import the" + echo -e "credentials below into your VPN app to access your DAppNode.\n" - echo -e "\e[1m--- Wireguard ---\e[0m" + echo -e "--- Wireguard ---" dappnode_wireguard --localhost 2>&1 || \ - echo -e "\e[33mWireguard credentials not yet available. Try later with: dappnode_wireguard --localhost\e[0m" + color_echo yellow "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" - echo -e "\n\e[1m--- OpenVPN ---\e[0m" + echo -e "\n--- OpenVPN ---" dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ - echo -e "\e[33mOpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost\e[0m" + color_echo yellow "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" - echo -e "\n\e[32mImport the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode\e[0m" + echo -e "\nImport the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" fi exit 0 From eea27846324e67458b6a26a4afd9435a1eef5883 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Mon, 2 Mar 2026 13:42:13 +0100 Subject: [PATCH 08/36] Refactor logging in installation script to remove color output and improve readability --- scripts/dappnode_install.sh | 134 +++++++++++++++++------------------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 1a6d95b..2db78af 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -72,20 +72,7 @@ else ARCH=$(dpkg --print-architecture) fi -# Color output helpers -color_echo() { - local color="$1"; shift - if $IS_LINUX; then - case "$color" in - green) code="\e[32m" ;; - yellow) code="\e[33m" ;; - *) code="" ;; - esac - echo -e "${code}$*\e[0m" - else - echo "$*" - fi -} + ############################## # Cross-platform Helpers # @@ -95,7 +82,7 @@ color_echo() { download_file() { local dest="$1" local url="$2" - echo "[DEBUG] Downloading from $url to $dest" 2>&1 | tee -a $LOGFILE + echo "Downloading from $url to $dest" 2>&1 | tee -a "$LOGFILE" if $IS_MACOS; then curl -sL -o "$dest" "$url" else @@ -277,13 +264,13 @@ determine_packages() { PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) fi fi - color_echo green "Packages to be installed: ${PKGS[*]}" 2>&1 | tee -a $LOGFILE + echo "Packages to be installed: ${PKGS[*]}" 2>&1 | tee -a "$LOGFILE" # Debug: print all PKGS and their version variables - echo "[DEBUG] PKGS: ${PKGS[*]}" 2>&1 | tee -a $LOGFILE + echo "PKGS: ${PKGS[*]}" 2>&1 | tee -a "$LOGFILE" for comp in "${PKGS[@]}"; do ver_var="${comp}_VERSION" - echo "[DEBUG] $ver_var = ${!ver_var}" 2>&1 | tee -a $LOGFILE + echo "$ver_var = ${!ver_var}" 2>&1 | tee -a "$LOGFILE" done } @@ -315,7 +302,7 @@ fi # If LOCAL_PROFILE_PATH is set, use it as the profile source instead of downloading if [ -n "$LOCAL_PROFILE_PATH" ]; then - echo "Using local profile: $LOCAL_PROFILE_PATH" | tee -a $LOGFILE + echo "Using local profile: $LOCAL_PROFILE_PATH" | tee -a "$LOGFILE" cp "$LOCAL_PROFILE_PATH" "$DAPPNODE_PROFILE" elif [ ! -f "$DAPPNODE_PROFILE" ]; then download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" @@ -337,14 +324,14 @@ source "${DAPPNODE_PROFILE}" determine_packages for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" - echo "[DEBUG] Processing $comp: ${!ver}" 2>&1 | tee -a $LOGFILE + echo "Processing $comp: ${!ver}" 2>&1 | tee -a "$LOGFILE" raw_version_ref="${!ver}" if [[ "$raw_version_ref" == /ipfs/* || "$raw_version_ref" == ipfs/* ]]; then resolved_ref="$(normalize_ipfs_version_ref "$raw_version_ref" "$comp")" || exit 1 eval "${comp}_VERSION=\"${resolved_ref}\"" raw_version_ref="$resolved_ref" - echo "[DEBUG] Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})" 2>&1 | tee -a $LOGFILE + echo "Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})" 2>&1 | tee -a "$LOGFILE" DOWNLOAD_URL="${IPFS_ENDPOINT%/}${raw_version_ref%:*}" version_for_filenames="${raw_version_ref##*:}" else @@ -428,7 +415,7 @@ dappnode_core_load() { ver="${comp}_VERSION" if [[ ${!ver} != dev:* ]]; then comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') - eval "[ ! -z \$(docker images -q ${comp_lower}.dnp.dappnode.eth:${!ver##*:}) ] || docker load -i \$${comp}_FILE 2>&1 | tee -a \$LOGFILE" + eval "[ ! -z \$(docker images -q ${comp_lower}.dnp.dappnode.eth:${!ver##*:}) ] || docker load -i \$${comp}_FILE 2>&1 | tee -a \"\$LOGFILE\"" fi done } @@ -459,7 +446,7 @@ generateMotdText() { |___/\__,_| .__/ .__/_||_\___/\__,_\___| |_| |_| EOF - welcome_message="\nChoose a way to connect to your DAppNode, then go to \e[1mhttp://my.dappnode\e[0m\n\n\e[1m- Wifi\e[0m\t\tScan and connect to DAppNodeWIFI. Get wifi credentials with \e[32mdappnode_wifi\e[0m\n\n\e[1m- Local Proxy\e[0m\tConnect to the same router as your DAppNode. Then go to \e[1mhttp://dappnode.local\e[0m\n\n\e[1m- Wireguard\e[0m\tDownload Wireguard app on your device. Get your dappnode wireguard credentials with \e[32mdappnode_wireguard\e[0m\n\n\e[1m- Open VPN\e[0m\tDownload OPen VPN app on your device. Get your openVPN creds with \e[32mdappnode_openvpn\e[0m\n\n\nTo see a full list of commands available execute \e[32mdappnode_help\e[0m\n" + welcome_message="\nChoose a way to connect to your DAppNode, then go to http://my.dappnode\n\n- Wifi\t\tScan and connect to DAppNodeWIFI. Get wifi credentials with dappnode_wifi\n\n- Local Proxy\tConnect to the same router as your DAppNode. Then go to http://dappnode.local\n\n- Wireguard\tDownload Wireguard app on your device. Get your dappnode wireguard credentials with dappnode_wireguard\n\n- Open VPN\tDownload Open VPN app on your device. Get your openVPN creds with dappnode_openvpn\n\n\nTo see a full list of commands available execute dappnode_help\n" printf "%b" "$welcome_message" >>"${MOTD_FILE}" } @@ -486,7 +473,7 @@ addSwap() { # if not then create it if [ "$IS_SWAP" -eq 0 ]; then - echo -e '\e[32mSwap not found. Adding swapfile.\e[0m' + echo 'Swap not found. Adding swapfile.' #RAM=$(awk '/MemTotal/ {print $2}' /proc/meminfo) #SWAP=$(($RAM * 2)) SWAP=8388608 @@ -496,7 +483,7 @@ addSwap() { swapon /swapfile echo '/swapfile none swap defaults 0 0' >>/etc/fstab else - echo -e '\e[32mSwap found. No changes made.\e[0m' + echo 'Swap found. No changes made.' fi } @@ -545,11 +532,11 @@ add_profile_to_shell() { } dappnode_core_start() { - echo -e "\e[32mDAppNode starting...\e[0m" 2>&1 | tee -a $LOGFILE + echo "DAppNode starting..." 2>&1 | tee -a "$LOGFILE" # Use DNCORE_YMLS from the profile (populated after re-sourcing post-download) - docker compose $DNCORE_YMLS up -d 2>&1 | tee -a $LOGFILE - echo -e "\e[32mDAppNode started\e[0m" 2>&1 | tee -a $LOGFILE + docker compose $DNCORE_YMLS up -d 2>&1 | tee -a "$LOGFILE" + echo "DAppNode started" 2>&1 | tee -a "$LOGFILE" # Add profile sourcing to user's shell configuration add_profile_to_shell @@ -563,17 +550,17 @@ dappnode_core_start() { # Linux-only: clean up rc.local if $IS_LINUX; then if [ -f "/etc/rc.local" ] && [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then - sed_inplace '/\/usr\/src\/dappnode\/scripts\/dappnode_install.sh/d' /etc/rc.local 2>&1 | tee -a $LOGFILE + sed_inplace '/\/usr\/src\/dappnode\/scripts\/dappnode_install.sh/d' /etc/rc.local 2>&1 | tee -a "$LOGFILE" fi fi # Display help message to the user - echo -e "Execute \e[32mdappnode_help\e[0m to see a full list with commands available" + echo "Execute dappnode_help to see a full list with commands available" } installExtraDpkg() { if [ -d "/usr/src/dappnode/extra_dpkg" ]; then - dpkg -i /usr/src/dappnode/iso/extra_dpkg/*.deb 2>&1 | tee -a $LOGFILE + dpkg -i /usr/src/dappnode/iso/extra_dpkg/*.deb 2>&1 | tee -a "$LOGFILE" fi } @@ -583,7 +570,7 @@ grabContentHashes() { for comp in "${content_hash_pkgs[@]}"; do CONTENT_HASH=$(download_stdout "https://github.com/dappnode/DAppNodePackage-${comp}/releases/latest/download/content-hash") if [ -z "$CONTENT_HASH" ]; then - echo "ERROR! Failed to find content hash of ${comp}." 2>&1 | tee -a $LOGFILE + echo "ERROR! Failed to find content hash of ${comp}." 2>&1 | tee -a "$LOGFILE" exit 1 fi echo "${comp}.dnp.dappnode.eth,${CONTENT_HASH}" >>${CONTENT_HASH_FILE} @@ -595,15 +582,15 @@ grabContentHashes() { installSgx() { if [ -d "/usr/src/dappnode/iso/sgx" ]; then # from sgx_linux_x64_driver_2.5.0_2605efa.bin - /usr/src/dappnode/iso/sgx/sgx_linux_x64_driver.bin 2>&1 | tee -a $LOGFILE - /usr/src/dappnode/iso/sgx/enable_sgx 2>&1 | tee -a $LOGFILE + /usr/src/dappnode/iso/sgx/sgx_linux_x64_driver.bin 2>&1 | tee -a "$LOGFILE" + /usr/src/dappnode/iso/sgx/enable_sgx 2>&1 | tee -a "$LOGFILE" fi } # /extra_dpkg will only be installed on ISO's dappnode not on standalone script installExtraDpkg() { if [ -d "/usr/src/dappnode/iso/extra_dpkg" ]; then - dpkg -i /usr/src/dappnode/extra_dpkg/*.deb 2>&1 | tee -a $LOGFILE + dpkg -i /usr/src/dappnode/extra_dpkg/*.deb 2>&1 | tee -a "$LOGFILE" fi } @@ -615,12 +602,12 @@ addUserToDockerGroup() { # If USER is not found, warn the user and return if [ -z "$USER" ]; then - echo -e "\e[33mWARN: Default user not found. Could not add it to the docker group.\e[0m" 2>&1 | tee -a $LOGFILE + echo "WARN: Default user not found. Could not add it to the docker group." 2>&1 | tee -a "$LOGFILE" return fi if groups "$USER" | grep &>/dev/null '\bdocker\b'; then - echo -e "\e[32mUser $USER is already in the docker group\e[0m" 2>&1 | tee -a $LOGFILE + echo "User $USER is already in the docker group" 2>&1 | tee -a "$LOGFILE" return fi @@ -628,64 +615,65 @@ addUserToDockerGroup() { # but it's not working in the Ubuntu ISO because the late-commands in the autoinstall.yaml # file are executed before the user is created. usermod -aG docker "$USER" - echo -e "\e[32mUser $USER added to the docker group\e[0m" 2>&1 | tee -a $LOGFILE + echo "User $USER added to the docker group" 2>&1 | tee -a "$LOGFILE" } ############################################## #### SCRIPT START #### ############################################## -color_echo green "\n##############################################" 2>&1 | tee -a $LOGFILE -color_echo green "#### DAPPNODE INSTALLER ####" 2>&1 | tee -a $LOGFILE -color_echo green "##############################################" 2>&1 | tee -a $LOGFILE +echo "" 2>&1 | tee -a "$LOGFILE" +echo "##############################################" 2>&1 | tee -a "$LOGFILE" +echo "#### DAPPNODE INSTALLER ####" 2>&1 | tee -a "$LOGFILE" +echo "##############################################" 2>&1 | tee -a "$LOGFILE" # --- Linux-only setup steps --- if $IS_LINUX; then - color_echo green "Creating swap memory..." 2>&1 | tee -a $LOGFILE + echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" addSwap - color_echo green "Customizing login..." 2>&1 | tee -a $LOGFILE + echo "Customizing login..." 2>&1 | tee -a "$LOGFILE" customMotd - color_echo green "Installing extra packages..." 2>&1 | tee -a $LOGFILE + echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" installExtraDpkg - color_echo green "Grabbing latest content hashes..." 2>&1 | tee -a $LOGFILE + echo "Grabbing latest content hashes..." 2>&1 | tee -a "$LOGFILE" grabContentHashes if [ "$ARCH" == "amd64" ]; then - color_echo green "Installing SGX modules..." 2>&1 | tee -a $LOGFILE + echo "Installing SGX modules..." 2>&1 | tee -a "$LOGFILE" installSgx - color_echo green "Installing extra packages..." 2>&1 | tee -a $LOGFILE + echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" installExtraDpkg # TODO: Why is this being called twice? fi - color_echo green "Adding user to docker group..." 2>&1 | tee -a $LOGFILE + echo "Adding user to docker group..." 2>&1 | tee -a "$LOGFILE" addUserToDockerGroup fi # --- Common steps (Linux and macOS) --- -color_echo green "Creating dncore_network if needed..." 2>&1 | tee -a $LOGFILE -docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a $LOGFILE || true +echo "Creating dncore_network if needed..." 2>&1 | tee -a "$LOGFILE" +docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a "$LOGFILE" || true -color_echo green "Building DAppNode Core if needed..." 2>&1 | tee -a $LOGFILE +echo "Building DAppNode Core if needed..." 2>&1 | tee -a "$LOGFILE" dappnode_core_build -color_echo green "Downloading DAppNode Core..." 2>&1 | tee -a $LOGFILE +echo "Downloading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" dappnode_core_download # Re-source profile now that compose files exist, so DNCORE_YMLS is populated # shellcheck disable=SC1090 source "${DAPPNODE_PROFILE}" -color_echo green "Loading DAppNode Core..." 2>&1 | tee -a $LOGFILE +echo "Loading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" dappnode_core_load # --- Start DAppNode --- if $IS_LINUX; then if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then - color_echo green "DAppNode installed" 2>&1 | tee -a $LOGFILE + echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start fi @@ -699,28 +687,34 @@ if $IS_LINUX; then fi if $IS_MACOS; then - color_echo green "DAppNode installed" 2>&1 | tee -a $LOGFILE + echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start - color_echo yellow "\nWaiting for VPN initialization..." - sleep 10 - - color_echo green "\n##############################################" - color_echo green "# DAppNode VPN Access Credentials #" - color_echo green "##############################################" - echo -e "\nYour DAppNode is ready! Connect using your preferred VPN client." - echo -e "Choose either Wireguard (recommended) or OpenVPN and import the" - echo -e "credentials below into your VPN app to access your DAppNode.\n" - - echo -e "--- Wireguard ---" + echo "" + echo "Waiting for VPN initialization..." + sleep 20 + + echo "" + echo "##############################################" + echo "# DAppNode VPN Access Credentials #" + echo "##############################################" + echo "" + echo "Your DAppNode is ready! Connect using your preferred VPN client." + echo "Choose either Wireguard (recommended) or OpenVPN and import the" + echo "credentials below into your VPN app to access your DAppNode." + echo "" + + echo "--- Wireguard ---" dappnode_wireguard --localhost 2>&1 || \ - color_echo yellow "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" + echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" - echo -e "\n--- OpenVPN ---" + echo "" + echo "--- OpenVPN ---" dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ - color_echo yellow "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" + echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" - echo -e "\nImport the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" + echo "" + echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" fi exit 0 From a511f2c456d1bccd14eee5c637cbde5326437ada Mon Sep 17 00:00:00 2001 From: Marc Font <36164126+Marketen@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:46:29 +0100 Subject: [PATCH 09/36] uninstall script in macos (#695) * uninstall script in macos * comment reexec under bash * comment * remove colors logic --------- Co-authored-by: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> --- scripts/dappnode_uninstall.sh | 111 +++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 21 deletions(-) diff --git a/scripts/dappnode_uninstall.sh b/scripts/dappnode_uninstall.sh index 66bc9f3..29da4a5 100755 --- a/scripts/dappnode_uninstall.sh +++ b/scripts/dappnode_uninstall.sh @@ -1,21 +1,72 @@ #!/usr/bin/env bash -DAPPNODE_DIR="/usr/src/dappnode" + +# This uninstaller is written for bash. It's safe to *run it from zsh* (it will execute via bash +# thanks to the shebang), but users sometimes invoke it as `zsh ./script.sh` or `source ./script.sh`. +# - If sourced, bail out (sourcing would pollute the current shell and can break it). +# - If invoked by a non-bash shell, re-exec with bash before hitting bash-specific builtins. +if (return 0 2>/dev/null); then + echo "This script must be executed, not sourced. Run: bash $0" + return 1 +fi + +if [ -z "${BASH_VERSION:-}" ]; then + exec /usr/bin/env bash "$0" "$@" +fi + +################## +# OS DETECTION # +################## +OS_TYPE="$(uname -s)" +IS_MACOS=false +IS_LINUX=false +if [[ "$OS_TYPE" == "Darwin" ]]; then + IS_MACOS=true +elif [[ "$OS_TYPE" == "Linux" ]]; then + IS_LINUX=true +else + echo "Unsupported operating system: $OS_TYPE" + exit 1 +fi + +############# +# VARIABLES # +############# +# Dirs — macOS uses $HOME/dappnode, Linux uses /usr/src/dappnode (mirrors install script) +if $IS_MACOS; then + DAPPNODE_DIR="$HOME/dappnode" +else + DAPPNODE_DIR="/usr/src/dappnode" +fi DAPPNODE_CORE_DIR="${DAPPNODE_DIR}/DNCORE" PROFILE_FILE="${DAPPNODE_CORE_DIR}/.dappnode_profile" input=$1 # Allow to call script with argument (must be Y/N) -[ -f $PROFILE_FILE ] || ( - echo "Error: DAppNode profile does not exist." +############################## +# Cross-platform Helpers # +############################## + +# Cross-platform in-place sed (macOS requires '' after -i) +sed_inplace() { + if $IS_MACOS; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +[ -f "$PROFILE_FILE" ] || { + echo "Error: DAppNode profile does not exist at ${PROFILE_FILE}." exit 1 -) +} uninstall() { - echo -e "\e[32mUninstalling DAppNode\e[0m" + echo "Uninstalling DAppNode" # shellcheck disable=SC1090 source "${PROFILE_FILE}" &>/dev/null DAPPNODE_CONTAINERS="$(docker ps -a --format '{{.Names}}' | grep DAppNode)" - echo -e "\e[32mRemoving DAppNode containers: \e[0m\n${DAPPNODE_CONTAINERS}" + echo "Removing DAppNode containers: " + echo "${DAPPNODE_CONTAINERS}" for container in $DAPPNODE_CONTAINERS; do # Stop DAppNode container docker stop "$container" &>/dev/null @@ -24,40 +75,58 @@ uninstall() { done DAPPNODE_IMAGES="$(docker image ls -a | grep "dappnode")" - echo -e "\e[32mRemoving DAppNode images: \e[0m\n${DAPPNODE_IMAGES}" + echo "Removing DAppNode images: " + echo "${DAPPNODE_IMAGES}" for image in $DAPPNODE_IMAGES; do # Remove DAppNode images docker image rm "$image" &>/dev/null done DAPPNODE_VOLUMES="$(docker volume ls | grep "dappnode\|dncore")" - echo -e "\e[32mRemoving DAppNode volumes: \e[0m\n${DAPPNODE_VOLUMES}" + echo "Removing DAppNode volumes: " + echo "${DAPPNODE_VOLUMES}" for volume in $DAPPNODE_VOLUMES; do # Remove DAppNode volumes docker volume rm "$volume" &>/dev/null done # Remove dncore_network - echo -e "\e[32mRemoving docker dncore_network\e[0m" + echo "Removing docker dncore_network" docker network remove dncore_network || echo "dncore_network already removed" - # Remove dir - echo -e "\e[32mRemoving DAppNode directory\e[0m" - rm -rf /usr/src/dappnode + # Remove DAppNode directory + echo "Removing DAppNode directory: ${DAPPNODE_DIR}" + rm -rf "${DAPPNODE_DIR}" # Remove profile file references from shell config files - USER=$(grep 1000 /etc/passwd | cut -f 1 -d:) - [ -n "$USER" ] && USER_HOME=/home/$USER || USER_HOME=/root - - for config_file in .profile .bashrc; do - CONFIG_PATH="$USER_HOME/$config_file" - if [ -f "$CONFIG_PATH" ]; then - sed -i '/######## DAPPNODE PROFILE ########/d' "$CONFIG_PATH" - sed -i '/.*dappnode_profile/d' "$CONFIG_PATH" + local user_home + local shell_configs + + if $IS_MACOS; then + user_home="$HOME" + # macOS defaults to zsh — matches install script + shell_configs=(".zshrc" ".zprofile") + else + local user_name + user_name=$(grep 1000 /etc/passwd | cut -f 1 -d:) + if [ -n "$user_name" ]; then + user_home="/home/$user_name" + else + user_home="/root" + fi + shell_configs=(".profile" ".bashrc") + fi + + # Remove Dappnode profile references from shell config files + for config_file in "${shell_configs[@]}"; do + local config_path="${user_home}/${config_file}" + if [ -f "$config_path" ]; then + sed_inplace '/######## DAPPNODE PROFILE ########/d' "$config_path" + sed_inplace '/.*dappnode_profile/d' "$config_path" fi done - echo -e "\e[32mDAppNode uninstalled!\e[0m" + echo "DAppNode uninstalled!" } if [ $# -eq 0 ]; then From d2c17832d6877a4346e481afc7373a8f9d23632d Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Mon, 2 Mar 2026 14:18:08 +0100 Subject: [PATCH 10/36] Enhance installation script with improved error handling, logging, and CLI wrappers for VPN commands --- scripts/dappnode_install.sh | 528 ++++++++++++++++++++++-------------- 1 file changed, 322 insertions(+), 206 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 2db78af..7c92566 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -13,12 +13,95 @@ if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@" fi -set -eo pipefail +set -Eeuo pipefail + +# Optional env inputs (avoid unbound-variable errors under `set -u`) +: "${UPDATE:=false}" +: "${STATIC_IP:=}" +: "${LOCAL_PROFILE_PATH:=}" # Enable alias expansion in non-interactive bash scripts. # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. shopt -s expand_aliases +############################## +# Core CLI wrappers (script) # +############################## + +# Note: aliases sourced from `.dappnode_profile` are not reliably usable inside this installer. +# Bash parses scripts before those aliases are defined, so later calls can become "command not found". +# Provide function wrappers so the installer can always run these commands. + +dappnode_wireguard() { + docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "$@" +} + +dappnode_openvpn_get() { + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get "$@" +} + +dappnode_openvpn() { + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth getAdminCredentials "$@" +} + +############################## +# Logging / Errors # +############################## + +log() { + # LOGFILE is created after dir bootstrap; until then we just print to stdout. + if [[ -n "${LOGFILE:-}" && -d "${LOGS_DIR:-}" ]]; then + printf '%s\n' "$*" | tee -a "$LOGFILE" + else + printf '%s\n' "$*" + fi +} + +warn() { + log "[WARN] $*" +} + +die() { + log "[ERROR] $*" + exit 1 +} + +require_cmd() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" +} + +require_downloader() { + if command -v curl >/dev/null 2>&1; then + return 0 + fi + if command -v wget >/dev/null 2>&1; then + return 0 + fi + die "Missing required downloader: install curl or wget" +} + +check_prereqs() { + require_cmd docker + require_downloader + + # Ensure compose is available (Docker Desktop / modern docker engine) + if ! docker compose version >/dev/null 2>&1; then + die "Docker Compose not available (expected: 'docker compose'). Update Docker or install the compose plugin." + fi +} + +# Build docker compose "-f " args from downloaded compose files. +# This avoids depending on alias expansion or profile-generated strings. +build_dncore_compose_args() { + DNCORE_COMPOSE_ARGS=() + local file + while IFS= read -r file; do + [[ -n "$file" ]] || continue + DNCORE_COMPOSE_ARGS+=( -f "$file" ) + done < <(find "${DAPPNODE_CORE_DIR}" -name 'docker-compose-*.yml' -print 2>/dev/null | sort) +} + ################## # OS DETECTION # ################## @@ -30,8 +113,7 @@ if [[ "$OS_TYPE" == "Darwin" ]]; then elif [[ "$OS_TYPE" == "Linux" ]]; then IS_LINUX=true else - echo "Unsupported operating system: $OS_TYPE" - exit 1 + die "Unsupported operating system: $OS_TYPE" fi ############# @@ -65,11 +147,11 @@ DAPPNODE_ACCESS_CREDENTIALS_URL="https://github.com/dappnode/DAppNode/releases/l # Architecture detection (cross-platform) if $IS_MACOS; then - ARCH=$(uname -m) + ARCH="$(uname -m)" [[ "$ARCH" == "x86_64" ]] && ARCH="amd64" # arm64 is already correct for Apple Silicon else - ARCH=$(dpkg --print-architecture) + ARCH="$(dpkg --print-architecture)" fi @@ -82,22 +164,23 @@ fi download_file() { local dest="$1" local url="$2" - echo "Downloading from $url to $dest" 2>&1 | tee -a "$LOGFILE" - if $IS_MACOS; then - curl -sL -o "$dest" "$url" - else - wget -q --show-progress --progress=bar:force -O "$dest" "$url" + log "Downloading from $url to $dest" + mkdir -p "$(dirname "$dest")" + if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$dest" "$url" + return fi + wget -q --show-progress --progress=bar:force -O "$dest" "$url" } # Download content to stdout: download_stdout download_stdout() { local url="$1" - if $IS_MACOS; then + if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" - else - wget -q -O- "$url" + return fi + wget -q -O- "$url" } # Normalize IPFS refs and (if needed) infer the missing : from dappnode_package.json @@ -189,25 +272,30 @@ patch_profile_for_macos() { sed_inplace 's|/usr/src/dappnode|\$HOME/dappnode|g' "$profile" } -# Clean if update -if [ "$UPDATE" = true ]; then - echo "Cleaning for update..." - rm -rf $LOGFILE - rm -rf ${DAPPNODE_CORE_DIR}/docker-compose-*.yml - rm -rf ${DAPPNODE_CORE_DIR}/dappnode_package-*.json - rm -rf ${DAPPNODE_CORE_DIR}/*.tar.xz - rm -rf ${DAPPNODE_CORE_DIR}/*.txz - rm -rf ${DAPPNODE_CORE_DIR}/.dappnode_profile - rm -rf ${CONTENT_HASH_FILE} -fi +bootstrap_filesystem() { + # Clean if update + if [[ "${UPDATE}" == "true" ]]; then + echo "Cleaning for update..." + rm -f "${LOGFILE}" || true + rm -f "${DAPPNODE_CORE_DIR}"/docker-compose-*.yml || true + rm -f "${DAPPNODE_CORE_DIR}"/dappnode_package-*.json || true + rm -f "${DAPPNODE_CORE_DIR}"/*.tar.xz || true + rm -f "${DAPPNODE_CORE_DIR}"/*.txz || true + rm -f "${DAPPNODE_CORE_DIR}/.dappnode_profile" || true + rm -f "${CONTENT_HASH_FILE}" || true + fi -# Create necessary directories -mkdir -p $DAPPNODE_DIR -mkdir -p $DAPPNODE_CORE_DIR -mkdir -p "${DAPPNODE_DIR}/scripts" -mkdir -p "${DAPPNODE_CORE_DIR}/scripts" -mkdir -p "${DAPPNODE_DIR}/config" -mkdir -p $LOGS_DIR + # Create necessary directories + mkdir -p "${DAPPNODE_DIR}" + mkdir -p "${DAPPNODE_CORE_DIR}" + mkdir -p "${DAPPNODE_DIR}/scripts" + mkdir -p "${DAPPNODE_CORE_DIR}/scripts" + mkdir -p "${DAPPNODE_DIR}/config" + mkdir -p "${LOGS_DIR}" + + # Ensure the log file path exists before first use by helpers. + touch "${LOGFILE}" || true +} # TEMPORARY: think a way to integrate flags instead of use files to detect installation type is_iso_install() { @@ -229,8 +317,14 @@ is_iso_install() { is_port_used() { # Check if port 80 or 443 is in use at all local port80_used port443_used - lsof -i -P -n | grep ":80 (LISTEN)" &>/dev/null && port80_used=true || port80_used=false - lsof -i -P -n | grep ":443 (LISTEN)" &>/dev/null && port443_used=true || port443_used=false + if command -v lsof >/dev/null 2>&1; then + lsof -i -P -n | grep ":80 (LISTEN)" &>/dev/null && port80_used=true || port80_used=false + lsof -i -P -n | grep ":443 (LISTEN)" &>/dev/null && port443_used=true || port443_used=false + else + warn "lsof not found; assuming ports 80/443 are in use (HTTPS will be skipped)" + IS_PORT_USED=true + return + fi if [ "$port80_used" = false ] && [ "$port443_used" = false ]; then IS_PORT_USED=false @@ -264,88 +358,93 @@ determine_packages() { PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) fi fi - echo "Packages to be installed: ${PKGS[*]}" 2>&1 | tee -a "$LOGFILE" + log "Packages to be installed: ${PKGS[*]}" # Debug: print all PKGS and their version variables - echo "PKGS: ${PKGS[*]}" 2>&1 | tee -a "$LOGFILE" + log "PKGS: ${PKGS[*]}" for comp in "${PKGS[@]}"; do + local ver_var ver_var="${comp}_VERSION" - echo "$ver_var = ${!ver_var}" 2>&1 | tee -a "$LOGFILE" + log "$ver_var = ${!ver_var-}" done } -function valid_ip() { - local ip=$1 - local stat=1 - - if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - OIFS=$IFS - IFS='.' - ip=("$ip") - IFS=$OIFS - [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && - ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] - stat=$? +valid_ip() { + local ip="$1" + if [[ ! "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then + return 1 fi - return $stat + + local IFS='.' + # shellcheck disable=SC2206 + local octets=( $ip ) + [[ ${#octets[@]} -eq 4 ]] || return 1 + [[ ${octets[0]} -le 255 && ${octets[1]} -le 255 && ${octets[2]} -le 255 && ${octets[3]} -le 255 ]] } -if [[ -n "$STATIC_IP" ]]; then +configure_static_ip() { + if [[ -z "${STATIC_IP}" ]]; then + return 0 + fi + if valid_ip "$STATIC_IP"; then - echo "$STATIC_IP" >${DAPPNODE_DIR}/config/static_ip + echo "$STATIC_IP" >"${DAPPNODE_DIR}/config/static_ip" else - echo "The static IP provided: ${STATIC_IP} is not valid." - exit 1 + die "The static IP provided (${STATIC_IP}) is not valid." fi -fi +} +ensure_profile_loaded() { + # If LOCAL_PROFILE_PATH is set, use it as the profile source instead of downloading + if [[ -n "${LOCAL_PROFILE_PATH}" ]]; then + log "Using local profile: ${LOCAL_PROFILE_PATH}" + cp "$LOCAL_PROFILE_PATH" "$DAPPNODE_PROFILE" + elif [[ ! -f "$DAPPNODE_PROFILE" ]]; then + download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" + fi -# If LOCAL_PROFILE_PATH is set, use it as the profile source instead of downloading -if [ -n "$LOCAL_PROFILE_PATH" ]; then - echo "Using local profile: $LOCAL_PROFILE_PATH" | tee -a "$LOGFILE" - cp "$LOCAL_PROFILE_PATH" "$DAPPNODE_PROFILE" -elif [ ! -f "$DAPPNODE_PROFILE" ]; then - download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" -fi + # Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) + # TODO: remove once profile macos-compatibility published + if $IS_MACOS; then + patch_profile_for_macos "$DAPPNODE_PROFILE" + fi + + # shellcheck disable=SC1090 + source "${DAPPNODE_PROFILE}" +} -# Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) -# TODO: remove once profile macos-compatibility published -if $IS_MACOS; then - patch_profile_for_macos "$DAPPNODE_PROFILE" -fi -# shellcheck disable=SC1090 -source "${DAPPNODE_PROFILE}" - -# The indirect variable expansion used in ${!ver##*:} allows us to use versions like 'dev:development' -# If such variable with 'dev:'' suffix is used, then the component is built from specified branch or commit. -# you can also specify an IPFS version like /ipfs/QmWg8P2b9JKQ8thAVz49J8SbJbCoi2MwkHnUqMtpzDTtxR:0.2.7, it's important -# to include the exact version also in the IPFS hash format since it's needed to be able to download it -determine_packages -for comp in "${PKGS[@]}"; do - ver="${comp}_VERSION" - echo "Processing $comp: ${!ver}" 2>&1 | tee -a "$LOGFILE" - - raw_version_ref="${!ver}" - if [[ "$raw_version_ref" == /ipfs/* || "$raw_version_ref" == ipfs/* ]]; then - resolved_ref="$(normalize_ipfs_version_ref "$raw_version_ref" "$comp")" || exit 1 - eval "${comp}_VERSION=\"${resolved_ref}\"" - raw_version_ref="$resolved_ref" - echo "Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})" 2>&1 | tee -a "$LOGFILE" - DOWNLOAD_URL="${IPFS_ENDPOINT%/}${raw_version_ref%:*}" - version_for_filenames="${raw_version_ref##*:}" - else - version_for_filenames="${raw_version_ref##*:}" - DOWNLOAD_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${version_for_filenames}" - fi - comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') - eval "${comp}_URL=\"${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz\"" - eval "${comp}_YML=\"${DOWNLOAD_URL}/docker-compose.yml\"" - eval "${comp}_MANIFEST=\"${DOWNLOAD_URL}/dappnode_package.json\"" - eval "${comp}_YML_FILE=\"${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml\"" - eval "${comp}_FILE=\"${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz\"" - eval "${comp}_MANIFEST_FILE=\"${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json\"" -done + +resolve_packages() { + # The indirect variable expansion used in ${!ver##*:} allows us to use versions like 'dev:development' + # If such variable with 'dev:'' suffix is used, then the component is built from specified branch or commit. + # you can also specify an IPFS version like /ipfs/: (the exact version is required). + determine_packages + for comp in "${PKGS[@]}"; do + ver="${comp}_VERSION" + log "Processing $comp: ${!ver-}" + + raw_version_ref="${!ver-}" + if [[ "$raw_version_ref" == /ipfs/* || "$raw_version_ref" == ipfs/* ]]; then + resolved_ref="$(normalize_ipfs_version_ref "$raw_version_ref" "$comp")" || exit 1 + printf -v "${comp}_VERSION" '%s' "$resolved_ref" + raw_version_ref="$resolved_ref" + log "Using IPFS for ${comp}: ${raw_version_ref%:*} (version ${raw_version_ref##*:})" + DOWNLOAD_URL="${IPFS_ENDPOINT%/}${raw_version_ref%:*}" + version_for_filenames="${raw_version_ref##*:}" + else + version_for_filenames="${raw_version_ref##*:}" + DOWNLOAD_URL="https://github.com/dappnode/DNP_${comp}/releases/download/v${version_for_filenames}" + fi + comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')" + printf -v "${comp}_URL" '%s' "${DOWNLOAD_URL}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz" + printf -v "${comp}_YML" '%s' "${DOWNLOAD_URL}/docker-compose.yml" + printf -v "${comp}_MANIFEST" '%s' "${DOWNLOAD_URL}/dappnode_package.json" + printf -v "${comp}_YML_FILE" '%s' "${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml" + printf -v "${comp}_FILE" '%s' "${DAPPNODE_CORE_DIR}/${comp_lower}.dnp.dappnode.eth_${version_for_filenames}_linux-${ARCH}.txz" + printf -v "${comp}_MANIFEST_FILE" '%s' "${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json" + done +} dappnode_core_build() { for comp in "${PKGS[@]}"; do @@ -359,23 +458,27 @@ dappnode_core_build() { if ! dpkg -s git >/dev/null 2>&1; then apt-get install -y git fi - TMPDIR=$(mktemp -d) - pushd "$TMPDIR" || { + local tmpdir + tmpdir="$(mktemp -d)" + pushd "$tmpdir" >/dev/null || { echo "Error on pushd" exit 1 } git clone -b "${!ver##*:}" https://github.com/dappnode/DNP_"${comp}" # Change version in YAML to the custom one - DOCKER_VER=$(echo "${!ver##*:}" | sed 's/\//_/g') - sed -i "s~^\(\s*image\s*:\s*\).*~\1${comp,,}.dnp.dappnode.eth:${DOCKER_VER}~" DNP_"${comp}"/docker-compose.yml + local docker_ver comp_lower + docker_ver="$(echo "${!ver##*:}" | sed 's/\//_/g')" + comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')" + sed_inplace "s~^\(\s*image\s*:\s*\).*~\1${comp_lower}.dnp.dappnode.eth:${docker_ver}~" "DNP_${comp}/docker-compose.yml" docker compose -f ./DNP_"${comp}"/docker-compose.yml build - cp ./DNP_"${comp}"/docker-compose.yml "${DAPPNODE_CORE_DIR}"/docker-compose-"${comp,,}".yml - cp ./DNP_"${comp}"/dappnode_package.json "${DAPPNODE_CORE_DIR}"/dappnode_package-"${comp,,}".json - rm -r ./DNP_"${comp}" - popd || { + cp "./DNP_${comp}/docker-compose.yml" "${DAPPNODE_CORE_DIR}/docker-compose-${comp_lower}.yml" + cp "./DNP_${comp}/dappnode_package.json" "${DAPPNODE_CORE_DIR}/dappnode_package-${comp_lower}.json" + rm -rf "./DNP_${comp}" + popd >/dev/null || { echo "Error on popd" exit 1 } + rm -rf "$tmpdir" fi done } @@ -414,8 +517,13 @@ dappnode_core_load() { for comp in "${PKGS[@]}"; do ver="${comp}_VERSION" if [[ ${!ver} != dev:* ]]; then - comp_lower=$(echo "$comp" | tr '[:upper:]' '[:lower:]') - eval "[ ! -z \$(docker images -q ${comp_lower}.dnp.dappnode.eth:${!ver##*:}) ] || docker load -i \$${comp}_FILE 2>&1 | tee -a \"\$LOGFILE\"" + local comp_lower image file_var + comp_lower="$(echo "$comp" | tr '[:upper:]' '[:lower:]')" + image="${comp_lower}.dnp.dappnode.eth:${!ver##*:}" + file_var="${comp}_FILE" + if [[ -z "$(docker images -q "$image" 2>/dev/null)" ]]; then + docker load -i "${!file_var}" 2>&1 | tee -a "$LOGFILE" + fi fi done } @@ -452,6 +560,7 @@ EOF # Ubuntu distros use /etc/update-motd.d/ to generate the motd modifyMotdGeneration() { + local disabled_motd_dir disabled_motd_dir="${UPDATE_MOTD_DIR}/disabled" mkdir -p "${disabled_motd_dir}" @@ -459,8 +568,9 @@ modifyMotdGeneration() { # Move all the files in /etc/update-motd.d/ to /etc/update-motd.d/disabled/ # Except for the files listed in "files_to_keep" files_to_keep="00-header 50-landscape-sysinfo 98-reboot-required" - for file in ${UPDATE_MOTD_DIR}/*; do - base_file=$(basename "${file}") + local file base_file + for file in "${UPDATE_MOTD_DIR}"/*; do + base_file="$(basename "${file}")" if [ -f "${file}" ] && ! echo "${files_to_keep}" | grep -qw "${base_file}"; then mv "${file}" "${disabled_motd_dir}/" fi @@ -477,7 +587,7 @@ addSwap() { #RAM=$(awk '/MemTotal/ {print $2}' /proc/meminfo) #SWAP=$(($RAM * 2)) SWAP=8388608 - fallocate -l ${SWAP}k /swapfile + fallocate -l "${SWAP}k" /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile @@ -494,8 +604,8 @@ add_profile_to_shell() { if $IS_MACOS; then user_home="$HOME" - # macOS defaults to zsh - shell_configs=(".zshrc" ".zprofile") + # macOS defaults to zsh, but some users still run bash. + shell_configs=(".zshrc" ".zprofile" ".bashrc" ".bash_profile") else # Linux: determine user home from /etc/passwd local user_name @@ -534,8 +644,12 @@ add_profile_to_shell() { dappnode_core_start() { echo "DAppNode starting..." 2>&1 | tee -a "$LOGFILE" - # Use DNCORE_YMLS from the profile (populated after re-sourcing post-download) - docker compose $DNCORE_YMLS up -d 2>&1 | tee -a "$LOGFILE" + if [[ ${#DNCORE_COMPOSE_ARGS[@]:-0} -eq 0 ]]; then + build_dncore_compose_args + fi + [[ ${#DNCORE_COMPOSE_ARGS[@]} -gt 0 ]] || die "No docker-compose-*.yml files found in ${DAPPNODE_CORE_DIR}" + + docker compose "${DNCORE_COMPOSE_ARGS[@]}" up -d 2>&1 | tee -a "$LOGFILE" echo "DAppNode started" 2>&1 | tee -a "$LOGFILE" # Add profile sourcing to user's shell configuration @@ -558,12 +672,6 @@ dappnode_core_start() { echo "Execute dappnode_help to see a full list with commands available" } -installExtraDpkg() { - if [ -d "/usr/src/dappnode/extra_dpkg" ]; then - dpkg -i /usr/src/dappnode/iso/extra_dpkg/*.deb 2>&1 | tee -a "$LOGFILE" - fi -} - grabContentHashes() { if [ ! -f "${CONTENT_HASH_FILE}" ]; then local content_hash_pkgs=(geth besu nethermind erigon prysm teku lighthouse nimbus lodestar) @@ -573,7 +681,7 @@ grabContentHashes() { echo "ERROR! Failed to find content hash of ${comp}." 2>&1 | tee -a "$LOGFILE" exit 1 fi - echo "${comp}.dnp.dappnode.eth,${CONTENT_HASH}" >>${CONTENT_HASH_FILE} + echo "${comp}.dnp.dappnode.eth,${CONTENT_HASH}" >>"${CONTENT_HASH_FILE}" done fi } @@ -590,7 +698,7 @@ installSgx() { # /extra_dpkg will only be installed on ISO's dappnode not on standalone script installExtraDpkg() { if [ -d "/usr/src/dappnode/iso/extra_dpkg" ]; then - dpkg -i /usr/src/dappnode/extra_dpkg/*.deb 2>&1 | tee -a "$LOGFILE" + dpkg -i /usr/src/dappnode/iso/extra_dpkg/*.deb 2>&1 | tee -a "$LOGFILE" fi } @@ -598,123 +706,131 @@ installExtraDpkg() { # Explained in: https://docs.docker.com/engine/install/linux-postinstall/ addUserToDockerGroup() { # UID is provided to the first regular user created in the system - USER=$(grep 1000 "/etc/passwd" | cut -f 1 -d:) + local user + user=$(grep 1000 "/etc/passwd" | cut -f 1 -d:) # If USER is not found, warn the user and return - if [ -z "$USER" ]; then + if [ -z "$user" ]; then echo "WARN: Default user not found. Could not add it to the docker group." 2>&1 | tee -a "$LOGFILE" return fi - if groups "$USER" | grep &>/dev/null '\bdocker\b'; then - echo "User $USER is already in the docker group" 2>&1 | tee -a "$LOGFILE" + if groups "$user" | grep &>/dev/null '\bdocker\b'; then + echo "User $user is already in the docker group" 2>&1 | tee -a "$LOGFILE" return fi # This step is already done in the dappnode_install_pre.sh script, # but it's not working in the Ubuntu ISO because the late-commands in the autoinstall.yaml # file are executed before the user is created. - usermod -aG docker "$USER" - echo "User $USER added to the docker group" 2>&1 | tee -a "$LOGFILE" + usermod -aG docker "$user" + echo "User $user added to the docker group" 2>&1 | tee -a "$LOGFILE" } ############################################## #### SCRIPT START #### ############################################## -echo "" 2>&1 | tee -a "$LOGFILE" -echo "##############################################" 2>&1 | tee -a "$LOGFILE" -echo "#### DAPPNODE INSTALLER ####" 2>&1 | tee -a "$LOGFILE" -echo "##############################################" 2>&1 | tee -a "$LOGFILE" +main() { + bootstrap_filesystem + check_prereqs + configure_static_ip + ensure_profile_loaded + resolve_packages -# --- Linux-only setup steps --- -if $IS_LINUX; then - echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" - addSwap + echo "" 2>&1 | tee -a "$LOGFILE" + echo "##############################################" 2>&1 | tee -a "$LOGFILE" + echo "#### DAPPNODE INSTALLER ####" 2>&1 | tee -a "$LOGFILE" + echo "##############################################" 2>&1 | tee -a "$LOGFILE" - echo "Customizing login..." 2>&1 | tee -a "$LOGFILE" - customMotd + # --- Linux-only setup steps --- + if $IS_LINUX; then + echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" + addSwap - echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" - installExtraDpkg + echo "Customizing login..." 2>&1 | tee -a "$LOGFILE" + customMotd - echo "Grabbing latest content hashes..." 2>&1 | tee -a "$LOGFILE" - grabContentHashes + echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" + installExtraDpkg - if [ "$ARCH" == "amd64" ]; then - echo "Installing SGX modules..." 2>&1 | tee -a "$LOGFILE" - installSgx + echo "Grabbing latest content hashes..." 2>&1 | tee -a "$LOGFILE" + grabContentHashes - echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" - installExtraDpkg # TODO: Why is this being called twice? + if [ "$ARCH" == "amd64" ]; then + echo "Installing SGX modules..." 2>&1 | tee -a "$LOGFILE" + installSgx + + echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" + installExtraDpkg # TODO: Why is this being called twice? + fi + + echo "Adding user to docker group..." 2>&1 | tee -a "$LOGFILE" + addUserToDockerGroup fi - echo "Adding user to docker group..." 2>&1 | tee -a "$LOGFILE" - addUserToDockerGroup -fi + # --- Common steps (Linux and macOS) --- + echo "Creating dncore_network if needed..." 2>&1 | tee -a "$LOGFILE" + docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a "$LOGFILE" || true -# --- Common steps (Linux and macOS) --- -echo "Creating dncore_network if needed..." 2>&1 | tee -a "$LOGFILE" -docker network create --driver bridge --subnet 172.33.0.0/16 dncore_network 2>&1 | tee -a "$LOGFILE" || true + echo "Building DAppNode Core if needed..." 2>&1 | tee -a "$LOGFILE" + dappnode_core_build -echo "Building DAppNode Core if needed..." 2>&1 | tee -a "$LOGFILE" -dappnode_core_build + echo "Downloading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" + dappnode_core_download -echo "Downloading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" -dappnode_core_download + # Build compose args now that compose files exist + build_dncore_compose_args -# Re-source profile now that compose files exist, so DNCORE_YMLS is populated -# shellcheck disable=SC1090 -source "${DAPPNODE_PROFILE}" + echo "Loading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" + dappnode_core_load -echo "Loading DAppNode Core..." 2>&1 | tee -a "$LOGFILE" -dappnode_core_load + # --- Start DAppNode --- + if $IS_LINUX; then + if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then + echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" + dappnode_core_start + fi -# --- Start DAppNode --- -if $IS_LINUX; then - if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then + # Run test in interactive terminal (first boot only) + if [ -f "${DAPPNODE_DIR}/.firstboot" ]; then + apt-get update + apt-get install -y kbd + openvt -s -w -- sudo -u root "${DAPPNODE_DIR}/scripts/dappnode_test_install.sh" + exit 0 + fi + fi + + if $IS_MACOS; then echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start - fi - # Run test in interactive terminal (first boot only) - if [ -f "${DAPPNODE_DIR}/.firstboot" ]; then - apt-get update - apt-get install -y kbd - openvt -s -w -- sudo -u root "${DAPPNODE_DIR}/scripts/dappnode_test_install.sh" - exit 0 + echo "" + echo "Waiting for VPN initialization..." + sleep 20 + + echo "" + echo "##############################################" + echo "# DAppNode VPN Access Credentials #" + echo "##############################################" + echo "" + echo "Your DAppNode is ready! Connect using your preferred VPN client." + echo "Choose either Wireguard (recommended) or OpenVPN and import the" + echo "credentials below into your VPN app to access your DAppNode." + echo "" + + echo "--- Wireguard ---" + dappnode_wireguard --localhost 2>&1 || \ + echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" + + echo "" + echo "--- OpenVPN ---" + dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ + echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" + + echo "" + echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" fi -fi - -if $IS_MACOS; then - echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" - dappnode_core_start - - echo "" - echo "Waiting for VPN initialization..." - sleep 20 - - echo "" - echo "##############################################" - echo "# DAppNode VPN Access Credentials #" - echo "##############################################" - echo "" - echo "Your DAppNode is ready! Connect using your preferred VPN client." - echo "Choose either Wireguard (recommended) or OpenVPN and import the" - echo "credentials below into your VPN app to access your DAppNode." - echo "" - - echo "--- Wireguard ---" - dappnode_wireguard --localhost 2>&1 || \ - echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" - - echo "" - echo "--- OpenVPN ---" - dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ - echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" - - echo "" - echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" -fi +} -exit 0 +main "$@" From e541023eb472c5f9b17e215ae824dfa7a887a4ad Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Mon, 2 Mar 2026 16:07:06 +0100 Subject: [PATCH 11/36] Remove core CLI wrappers from installation script to streamline execution --- scripts/dappnode_install.sh | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 7c92566..e6f4b2f 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -24,26 +24,6 @@ set -Eeuo pipefail # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. shopt -s expand_aliases -############################## -# Core CLI wrappers (script) # -############################## - -# Note: aliases sourced from `.dappnode_profile` are not reliably usable inside this installer. -# Bash parses scripts before those aliases are defined, so later calls can become "command not found". -# Provide function wrappers so the installer can always run these commands. - -dappnode_wireguard() { - docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "$@" -} - -dappnode_openvpn_get() { - docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get "$@" -} - -dappnode_openvpn() { - docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth getAdminCredentials "$@" -} - ############################## # Logging / Errors # ############################## @@ -154,8 +134,6 @@ else ARCH="$(dpkg --print-architecture)" fi - - ############################## # Cross-platform Helpers # ############################## From 950af70a76096a0098f49398f3daec52e954d468 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 07:42:21 +0100 Subject: [PATCH 12/36] Add macOS-specific environment variable injection for dappmanager compose --- scripts/dappnode_install.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index e6f4b2f..5cb34cc 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -240,6 +240,37 @@ patch_compose_paths() { sed_inplace "s|/usr/src/dappnode|${DAPPNODE_DIR}|g" "$file" } +# Patch dappmanager compose for macOS: inject env vars the container needs +# to know the host core-dir path and to skip host-only operations, +# and fix the DNCORE volume mount to use the macOS host path. +patch_dappmanager_compose_for_macos() { + local file="$1" + + # Replace the host side of the DNCORE volume mount with the actual DAPPNODE_CORE_DIR value + # e.g. /usr/src/dappnode/DNCORE/:/usr/src/app/DNCORE/ -> $HOME/dappnode/DNCORE/:/usr/src/app/DNCORE/ + sed_inplace "s|[^[:space:]]*:/usr/src/app/DNCORE/|${DAPPNODE_CORE_DIR}/:/usr/src/app/DNCORE/|" "$file" + + local envs_to_add=() + + # DAPPNODE_CORE_DIR: lets the container know the host's DNCORE path + if ! grep -q "DAPPNODE_CORE_DIR" "$file"; then + envs_to_add+=(" - DAPPNODE_CORE_DIR=${DAPPNODE_CORE_DIR}") + fi + + # DISABLE_HOST_SCRIPTS: tells the container to skip host-only scripts + if ! grep -q "DISABLE_HOST_SCRIPTS" "$file"; then + envs_to_add+=(" - DISABLE_HOST_SCRIPTS=${DISABLE_HOST_SCRIPTS:-true}") + fi + + [[ ${#envs_to_add[@]} -gt 0 ]] || return 0 + + local tmp="${file}.tmp" + local insert_text + insert_text=$(printf '%s\n' "${envs_to_add[@]}") + + awk -v ins="$insert_text" '/DISABLE_UPNP/ { print; print ins; next } { print }' "$file" > "$tmp" && mv "$tmp" "$file" +} + # TODO: remove once profile macos-compatibility published # Patch .dappnode_profile for macOS compatibility patch_profile_for_macos() { @@ -486,6 +517,10 @@ dappnode_core_download() { if $IS_MACOS; then remove_logging_section "${!yml_file_var}" patch_compose_paths "${!yml_file_var}" + # Inject macOS-specific env vars into the dappmanager compose + if [[ "$comp" == "DAPPMANAGER" ]]; then + patch_dappmanager_compose_for_macos "${!yml_file_var}" + fi fi fi done From 41da667727ce5a52acd934813b4cc64755a5336a Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 07:46:42 +0100 Subject: [PATCH 13/36] Fix DISABLE_HOST_SCRIPTS environment variable handling in macOS patch function --- scripts/dappnode_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 5cb34cc..2b2ea60 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -259,7 +259,7 @@ patch_dappmanager_compose_for_macos() { # DISABLE_HOST_SCRIPTS: tells the container to skip host-only scripts if ! grep -q "DISABLE_HOST_SCRIPTS" "$file"; then - envs_to_add+=(" - DISABLE_HOST_SCRIPTS=${DISABLE_HOST_SCRIPTS:-true}") + envs_to_add+=(" - DISABLE_HOST_SCRIPTS=${DISABLE_HOST_SCRIPTS}") fi [[ ${#envs_to_add[@]} -gt 0 ]] || return 0 From 26851ee51e5107efe8f958af761d54a9cab682d1 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 08:29:16 +0100 Subject: [PATCH 14/36] Initialize DNCORE_COMPOSE_ARGS array to prevent unbound variable errors --- scripts/dappnode_install.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 2b2ea60..a14d00a 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -24,6 +24,9 @@ set -Eeuo pipefail # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. shopt -s expand_aliases +# Ensure array is always defined (avoid `set -u` edge cases) +DNCORE_COMPOSE_ARGS=() + ############################## # Logging / Errors # ############################## @@ -657,7 +660,7 @@ add_profile_to_shell() { dappnode_core_start() { echo "DAppNode starting..." 2>&1 | tee -a "$LOGFILE" - if [[ ${#DNCORE_COMPOSE_ARGS[@]:-0} -eq 0 ]]; then + if [[ ${#DNCORE_COMPOSE_ARGS[@]} -eq 0 ]]; then build_dncore_compose_args fi [[ ${#DNCORE_COMPOSE_ARGS[@]} -gt 0 ]] || die "No docker-compose-*.yml files found in ${DAPPNODE_CORE_DIR}" From 6947f701bf8e9a515169ab17aa4634e1b0e64637 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 08:57:23 +0100 Subject: [PATCH 15/36] Refactor macOS patch function to handle environment variable insertion with temporary file for compatibility --- scripts/dappnode_install.sh | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index a14d00a..e57198f 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -268,10 +268,23 @@ patch_dappmanager_compose_for_macos() { [[ ${#envs_to_add[@]} -gt 0 ]] || return 0 local tmp="${file}.tmp" - local insert_text - insert_text=$(printf '%s\n' "${envs_to_add[@]}") + local insert_file="${file}.envinsert" - awk -v ins="$insert_text" '/DISABLE_UPNP/ { print; print ins; next } { print }' "$file" > "$tmp" && mv "$tmp" "$file" + # macOS ships BSD awk, which can error with "newline in string" if a -v argument contains + # literal newlines. Write the insertion block to a temp file and have awk read it. + printf '%s\n' "${envs_to_add[@]}" >"$insert_file" + + awk -v insfile="$insert_file" ' + /^[[:space:]]*environment:[[:space:]]*$/ { + print + while ((getline line < insfile) > 0) print line + close(insfile) + next + } + { print } + ' "$file" >"$tmp" && mv "$tmp" "$file" + + rm -f "$insert_file" || true } # TODO: remove once profile macos-compatibility published @@ -836,12 +849,12 @@ main() { echo "" echo "--- Wireguard ---" - dappnode_wireguard --localhost 2>&1 || \ + docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials --localhost 2>&1 || \ echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" echo "" echo "--- OpenVPN ---" - dappnode_openvpn_get dappnode_admin --localhost 2>&1 || \ + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin --localhost 2>&1 || \ echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" echo "" From adc867ed15b7e9d4ca5b1d78fe3877011097b569 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 16:38:23 +0100 Subject: [PATCH 16/36] Increase sleep duration to 30 seconds for VPN initialization in installation script --- scripts/dappnode_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index e57198f..2ab507e 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -836,7 +836,7 @@ main() { echo "" echo "Waiting for VPN initialization..." - sleep 20 + sleep 30 echo "" echo "##############################################" From 70bb92cd06d81a99a08ed52f1d17fa9ecc7af3e4 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 3 Mar 2026 19:27:18 +0100 Subject: [PATCH 17/36] Implement wait_for_internal_ip function to ensure dappmanager publishes INTERNAL_IP before proceeding --- scripts/dappnode_install.sh | 46 ++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 2ab507e..5906076 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -74,6 +74,50 @@ check_prereqs() { fi } +# Wait until dappmanager publishes INTERNAL_IP via its local HTTP endpoint. +# Runs the curl inside the provided container and exits with error on timeout. +# Usage: wait_for_internal_ip [timeout_seconds] [initial_sleep_seconds] +wait_for_internal_ip() { + local container_name="$1" + local timeout_seconds="${2:-120}" + local initial_sleep_seconds="${3:-10}" + local url="http://127.0.0.1/global-envs/INTERNAL_IP" + + echo "Waiting for dappmanager to publish INTERNAL_IP..." + sleep "$initial_sleep_seconds" + + local start_seconds http_code value result + start_seconds=$SECONDS + http_code="" + value="" + + while true; do + if (( SECONDS - start_seconds >= timeout_seconds )); then + die "Timed out after ${timeout_seconds}s waiting for INTERNAL_IP from dappmanager (expected HTTP 200 with a non-empty value). Last seen: code=${http_code:-?}, value=${value:-}" + fi + + # Must be executed inside the dappmanager container + # Wait until we get HTTP 200 and a non-empty value back. + # Return format is: + # \n + # Parse in bash (not inside container sh) to avoid shell portability issues. + result="$( + docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$url' 2>/dev/null || true" 2>/dev/null || true + )" + + http_code="$(printf '%s\n' "$result" | tail -n 1 | tr -d '\r')" + value="$(printf '%s\n' "$result" | head -n 1 | tr -d '\r' | xargs)" + + if [[ "$http_code" == "200" && -n "$value" && "$value" != "null" ]]; then + echo "INTERNAL_IP is ready: $value" + return 0 + fi + + echo "INTERNAL_IP not ready yet (code=${http_code:-?}). Retrying..." + sleep 2 + done +} + # Build docker compose "-f " args from downloaded compose files. # This avoids depending on alias expansion or profile-generated strings. build_dncore_compose_args() { @@ -836,7 +880,7 @@ main() { echo "" echo "Waiting for VPN initialization..." - sleep 30 + wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 10 echo "" echo "##############################################" From 4aa292390a9c34e431a4b749f62bb963b05d0a13 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 08:22:10 +0100 Subject: [PATCH 18/36] Add macOS server detection and adjust package selection for always-on Macs --- scripts/dappnode_install.sh | 49 +++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 5906076..ebb5dd6 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -185,6 +185,28 @@ fi # Cross-platform Helpers # ############################## +# macOS: determine whether we're running on a server-suitable always-on Mac. +# Heuristic: treat Mac mini / Mac Studio / Mac Pro as always-on capable. +# Returns 0 (true) if server-class, 1 otherwise. +is_always_on_mac() { + # Non-macOS hosts are not considered always-on Macs + if ! $IS_MACOS; then + return 1 + fi + + local model + model="$(sysctl -n hw.model 2>/dev/null)" || return 1 + + case "$model" in + Macmini*|MacStudio*|MacPro*) + return 0 + ;; + *) + return 1 + ;; + esac +} + # Download a file: download_file download_file() { local dest="$1" @@ -412,19 +434,30 @@ is_port_used() { # Determine packages to be installed determine_packages() { - is_iso_install - is_port_used - if [ "$IS_ISO_INSTALL" == "false" ]; then - if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER) + # macOS: package selection depends on whether the Mac is suitable to run always-on. + # - non-server mac: BIND VPN WIREGUARD DAPPMANAGER + # - server mac: BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS + # NOTE: HTTPS may be skipped if ports 80/443 are already in use. + if $IS_MACOS; then + is_port_used + + if is_always_on_mac; then + if [ "$IS_PORT_USED" == "true" ]; then + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) + else + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS) + fi else - PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) + PKGS=(BIND VPN WIREGUARD DAPPMANAGER) fi else + # Linux / ISO logic + is_iso_install + is_port_used if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) else - PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER) + PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) fi fi log "Packages to be installed: ${PKGS[*]}" From abfef5255523e6aef8e57438c0c020e4815774a5 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 08:38:03 +0100 Subject: [PATCH 19/36] Add patch for macOS non-server to use remote IPFS services in maindb.json --- scripts/dappnode_install.sh | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index ebb5dd6..6a5cbeb 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -363,6 +363,55 @@ patch_profile_for_macos() { sed_inplace 's|/usr/src/dappnode|\$HOME/dappnode|g' "$profile" } +# macOS (non-server): ensure dappmanager uses remote IPFS services. +# This updates ${DAPPNODE_CORE_DIR}/maindb.json with: +# "ipfs-gateway": "https://ipfs-gateway.dappnode.net" +# "ipfs-client-target": "remote" +patch_maindb_for_macos_nonserver() { + if ! $IS_MACOS; then + return 0 + fi + + local maindb_file="${DAPPNODE_CORE_DIR}/maindb.json" + if [[ ! -f "$maindb_file" ]]; then + warn "macOS non-server: maindb.json not found at ${maindb_file}; skipping" + return 0 + fi + + log "macOS non-server: patching maindb.json to use remote IPFS" + + if ! command -v osascript >/dev/null 2>&1; then + die "macOS non-server: cannot patch maindb.json (missing 'osascript')" + fi + + osascript -l JavaScript - "$maindb_file" <<'JXA' +ObjC.import('Foundation'); + +function run(argv) { + var path = argv[0]; + if (!path) throw new Error('Missing maindb.json path'); + + var data = $.NSData.dataWithContentsOfFile(path); + if (!data) throw new Error('Unable to read file: ' + path); + + var content = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)); + var obj = JSON.parse(content); + if (Object.prototype.toString.call(obj) !== '[object Object]') { + throw new Error('maindb.json is not a JSON object: ' + path); + } + + obj['ipfs-gateway'] = 'https://ipfs-gateway.dappnode.net'; + obj['ipfs-client-target'] = 'remote'; + + var out = JSON.stringify(obj, null, 2) + '\n'; + var outStr = $.NSString.stringWithString(out); + var ok = outStr.writeToFileAtomicallyEncodingError(path, true, $.NSUTF8StringEncoding, null); + if (!ok) throw new Error('Unable to write file: ' + path); +} +JXA + return 0 +} + bootstrap_filesystem() { # Clean if update if [[ "${UPDATE}" == "true" ]]; then @@ -911,6 +960,11 @@ main() { echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start + # macOS non-server machines should not try to use local IPFS. + if ! is_always_on_mac; then + patch_maindb_for_macos_nonserver + fi + echo "" echo "Waiting for VPN initialization..." wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 10 From 8a863abfc3b7bf145689d5d99d8cf90f7729a051 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 08:46:09 +0100 Subject: [PATCH 20/36] Enhance wait_for_internal_ip function to check for HOSTNAME alongside INTERNAL_IP and improve error messaging; add wait for maindb.json file existence in macOS non-server patch function --- scripts/dappnode_install.sh | 54 ++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 6a5cbeb..9f73d07 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -81,39 +81,49 @@ wait_for_internal_ip() { local container_name="$1" local timeout_seconds="${2:-120}" local initial_sleep_seconds="${3:-10}" - local url="http://127.0.0.1/global-envs/INTERNAL_IP" + local internal_ip_url="http://127.0.0.1/global-envs/INTERNAL_IP" + local hostname_url="http://127.0.0.1/global-envs/HOSTNAME" - echo "Waiting for dappmanager to publish INTERNAL_IP..." + echo "Waiting for dappmanager to publish INTERNAL_IP and HOSTNAME..." sleep "$initial_sleep_seconds" - local start_seconds http_code value result + local start_seconds internal_http_code internal_value internal_result + local hostname_http_code hostname_value hostname_result start_seconds=$SECONDS - http_code="" - value="" + internal_http_code="" + internal_value="" + hostname_http_code="" + hostname_value="" while true; do if (( SECONDS - start_seconds >= timeout_seconds )); then - die "Timed out after ${timeout_seconds}s waiting for INTERNAL_IP from dappmanager (expected HTTP 200 with a non-empty value). Last seen: code=${http_code:-?}, value=${value:-}" + die "Timed out after ${timeout_seconds}s waiting for INTERNAL_IP and HOSTNAME from dappmanager (expected HTTP 200 with non-empty values). Last seen: INTERNAL_IP code=${internal_http_code:-?} value=${internal_value:-}; HOSTNAME code=${hostname_http_code:-?} value=${hostname_value:-}" fi - # Must be executed inside the dappmanager container - # Wait until we get HTTP 200 and a non-empty value back. + # Must be executed inside the dappmanager container. # Return format is: # \n # Parse in bash (not inside container sh) to avoid shell portability issues. - result="$( - docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$url' 2>/dev/null || true" 2>/dev/null || true + + internal_result="$( + docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$internal_ip_url' 2>/dev/null || true" 2>/dev/null || true )" + internal_http_code="$(printf '%s\n' "$internal_result" | tail -n 1 | tr -d '\r')" + internal_value="$(printf '%s\n' "$internal_result" | head -n 1 | tr -d '\r' | xargs)" - http_code="$(printf '%s\n' "$result" | tail -n 1 | tr -d '\r')" - value="$(printf '%s\n' "$result" | head -n 1 | tr -d '\r' | xargs)" + hostname_result="$( + docker exec -i "$container_name" sh -lc "curl -sS -w '\n%{http_code}' '$hostname_url' 2>/dev/null || true" 2>/dev/null || true + )" + hostname_http_code="$(printf '%s\n' "$hostname_result" | tail -n 1 | tr -d '\r')" + hostname_value="$(printf '%s\n' "$hostname_result" | head -n 1 | tr -d '\r' | xargs)" - if [[ "$http_code" == "200" && -n "$value" && "$value" != "null" ]]; then - echo "INTERNAL_IP is ready: $value" + if [[ "$internal_http_code" == "200" && -n "$internal_value" && "$internal_value" != "null" && "$hostname_http_code" == "200" && -n "$hostname_value" && "$hostname_value" != "null" ]]; then + echo "INTERNAL_IP is ready: $internal_value" + echo "HOSTNAME is ready: $hostname_value" return 0 fi - echo "INTERNAL_IP not ready yet (code=${http_code:-?}). Retrying..." + echo "INTERNAL_IP/HOSTNAME not ready yet (INTERNAL_IP code=${internal_http_code:-?}, HOSTNAME code=${hostname_http_code:-?}). Retrying..." sleep 2 done } @@ -373,10 +383,16 @@ patch_maindb_for_macos_nonserver() { fi local maindb_file="${DAPPNODE_CORE_DIR}/maindb.json" - if [[ ! -f "$maindb_file" ]]; then - warn "macOS non-server: maindb.json not found at ${maindb_file}; skipping" - return 0 - fi + # Wait (up to 2 minutes) for maindb.json to exist. + local start_seconds + start_seconds=$SECONDS + while [[ ! -f "$maindb_file" ]]; do + if (( SECONDS - start_seconds >= 120 )); then + warn "macOS non-server: maindb.json not found at ${maindb_file} after 120s; skipping" + return 0 + fi + sleep 2 + done log "macOS non-server: patching maindb.json to use remote IPFS" From c2ddd68cfc041fcec8a1bc82e7399b596bbd6439 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 11:36:00 +0100 Subject: [PATCH 21/36] Remove macOS non-server patch for remote IPFS services from installation script --- scripts/dappnode_install.sh | 60 ------------------------------------- 1 file changed, 60 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 9f73d07..de3c1bd 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -373,61 +373,6 @@ patch_profile_for_macos() { sed_inplace 's|/usr/src/dappnode|\$HOME/dappnode|g' "$profile" } -# macOS (non-server): ensure dappmanager uses remote IPFS services. -# This updates ${DAPPNODE_CORE_DIR}/maindb.json with: -# "ipfs-gateway": "https://ipfs-gateway.dappnode.net" -# "ipfs-client-target": "remote" -patch_maindb_for_macos_nonserver() { - if ! $IS_MACOS; then - return 0 - fi - - local maindb_file="${DAPPNODE_CORE_DIR}/maindb.json" - # Wait (up to 2 minutes) for maindb.json to exist. - local start_seconds - start_seconds=$SECONDS - while [[ ! -f "$maindb_file" ]]; do - if (( SECONDS - start_seconds >= 120 )); then - warn "macOS non-server: maindb.json not found at ${maindb_file} after 120s; skipping" - return 0 - fi - sleep 2 - done - - log "macOS non-server: patching maindb.json to use remote IPFS" - - if ! command -v osascript >/dev/null 2>&1; then - die "macOS non-server: cannot patch maindb.json (missing 'osascript')" - fi - - osascript -l JavaScript - "$maindb_file" <<'JXA' -ObjC.import('Foundation'); - -function run(argv) { - var path = argv[0]; - if (!path) throw new Error('Missing maindb.json path'); - - var data = $.NSData.dataWithContentsOfFile(path); - if (!data) throw new Error('Unable to read file: ' + path); - - var content = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)); - var obj = JSON.parse(content); - if (Object.prototype.toString.call(obj) !== '[object Object]') { - throw new Error('maindb.json is not a JSON object: ' + path); - } - - obj['ipfs-gateway'] = 'https://ipfs-gateway.dappnode.net'; - obj['ipfs-client-target'] = 'remote'; - - var out = JSON.stringify(obj, null, 2) + '\n'; - var outStr = $.NSString.stringWithString(out); - var ok = outStr.writeToFileAtomicallyEncodingError(path, true, $.NSUTF8StringEncoding, null); - if (!ok) throw new Error('Unable to write file: ' + path); -} -JXA - return 0 -} - bootstrap_filesystem() { # Clean if update if [[ "${UPDATE}" == "true" ]]; then @@ -976,11 +921,6 @@ main() { echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start - # macOS non-server machines should not try to use local IPFS. - if ! is_always_on_mac; then - patch_maindb_for_macos_nonserver - fi - echo "" echo "Waiting for VPN initialization..." wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 10 From 817ec20c0b9534559f2029338bdbe7843add4a36 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 11:52:02 +0100 Subject: [PATCH 22/36] Remove default PROFILE_BRANCH assignment from installation script --- scripts/dappnode_install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index de3c1bd..b1661d3 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -174,7 +174,6 @@ if $IS_LINUX; then UPDATE_MOTD_DIR="/etc/update-motd.d" fi # Get URLs -PROFILE_BRANCH=${PROFILE_BRANCH:-"master"} IPFS_ENDPOINT=${IPFS_ENDPOINT:-"https://ipfs-gateway-dev.dappnode.net"} # PROFILE_URL env is used to fetch the core packages versions that will be used to build the release in script install method PROFILE_URL=${PROFILE_URL:-"https://github.com/dappnode/DAppNode/releases/latest/download/dappnode_profile.sh"} From 5c8391683b55013e0de833eeedc4cacc5c8b62aa Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 11:55:46 +0100 Subject: [PATCH 23/36] Add support for minimal installation mode and enhance argument parsing in installer script --- scripts/dappnode_install.sh | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index b1661d3..de2c476 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -19,6 +19,7 @@ set -Eeuo pipefail : "${UPDATE:=false}" : "${STATIC_IP:=}" : "${LOCAL_PROFILE_PATH:=}" +: "${MINIMAL:=false}" # Enable alias expansion in non-interactive bash scripts. # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. @@ -49,6 +50,70 @@ die() { exit 1 } +usage() { + cat <<'EOF' +Usage: dappnode_install.sh [options] + +Options: + --update Clean existing downloaded artifacts before installing (equivalent: UPDATE=true) + --static-ip Set a static IP (equivalent: STATIC_IP=...) + --local-profile-path Use a local .dappnode_profile instead of downloading (equivalent: LOCAL_PROFILE_PATH=...) + --ipfs-endpoint Override IPFS gateway endpoint (equivalent: IPFS_ENDPOINT=...) + --profile-url Override profile download URL (equivalent: PROFILE_URL=...) + --minimal Force minimal package set: BIND VPN WIREGUARD DAPPMANAGER (equivalent: MINIMAL=true) + -h, --help Show this help + +Environment variables (also supported): + UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL +EOF +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --update) + UPDATE=true + shift + ;; + --static-ip) + [[ $# -ge 2 ]] || die "--static-ip requires an IPv4 argument" + STATIC_IP="$2" + shift 2 + ;; + --local-profile-path) + [[ $# -ge 2 ]] || die "--local-profile-path requires a path argument" + LOCAL_PROFILE_PATH="$2" + shift 2 + ;; + --ipfs-endpoint) + [[ $# -ge 2 ]] || die "--ipfs-endpoint requires a URL argument" + IPFS_ENDPOINT="$2" + shift 2 + ;; + --profile-url) + [[ $# -ge 2 ]] || die "--profile-url requires a URL argument" + PROFILE_URL="$2" + shift 2 + ;; + --minimal) + MINIMAL=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + *) + die "Unknown option: $1 (use --help)" + ;; + esac + done +} + require_cmd() { local cmd="$1" command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" @@ -443,6 +508,20 @@ is_port_used() { # Determine packages to be installed determine_packages() { + # Global override: minimal install, regardless of OS. + if [[ "${MINIMAL}" == "true" ]]; then + PKGS=(BIND VPN WIREGUARD DAPPMANAGER) + log "Minimal mode enabled; overriding packages" + log "Packages to be installed: ${PKGS[*]}" + log "PKGS: ${PKGS[*]}" + for comp in "${PKGS[@]}"; do + local ver_var + ver_var="${comp}_VERSION" + log "$ver_var = ${!ver_var-}" + done + return 0 + fi + # macOS: package selection depends on whether the Mac is suitable to run always-on. # - non-server mac: BIND VPN WIREGUARD DAPPMANAGER # - server mac: BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS @@ -847,6 +926,8 @@ addUserToDockerGroup() { ############################################## main() { + parse_args "$@" + bootstrap_filesystem check_prereqs configure_static_ip From 007f6d0b132e6d53eb52db36ab96acd5ed78ad6b Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 12:13:01 +0100 Subject: [PATCH 24/36] Increase wait time for VPN initialization in installer script --- scripts/dappnode_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index de2c476..219c653 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -1003,7 +1003,7 @@ main() { echo "" echo "Waiting for VPN initialization..." - wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 10 + wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 echo "" echo "##############################################" From 240f02ac8a1e32470ccb3fe3934e08b74a7f1d4d Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 12:27:22 +0100 Subject: [PATCH 25/36] Add function to print VPN access credentials after core startup --- scripts/dappnode_install.sh | 58 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 219c653..b8b1ee3 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -193,6 +193,36 @@ wait_for_internal_ip() { done } +# Print VPN access credentials (Wireguard + OpenVPN) after core has started. +# Works on both Linux and macOS as long as the relevant containers are running. +print_vpn_access_credentials() { + echo "" + echo "Waiting for VPN initialization..." + wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 + + echo "" + echo "##############################################" + echo "# DAppNode VPN Access Credentials #" + echo "##############################################" + echo "" + echo "Your DAppNode is ready! Connect using your preferred VPN client." + echo "Choose either Wireguard (recommended) or OpenVPN and import the" + echo "credentials below into your VPN app to access your DAppNode." + echo "" + + echo "--- Wireguard ---" + docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials --localhost 2>&1 || \ + echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" + + echo "" + echo "--- OpenVPN ---" + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin --localhost 2>&1 || \ + echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" + + echo "" + echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" +} + # Build docker compose "-f " args from downloaded compose files. # This avoids depending on alias expansion or profile-generated strings. build_dncore_compose_args() { @@ -986,6 +1016,7 @@ main() { if [ ! -f "${DAPPNODE_DIR}/.firstboot" ]; then echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start + print_vpn_access_credentials fi # Run test in interactive terminal (first boot only) @@ -1000,32 +1031,7 @@ main() { if $IS_MACOS; then echo "DAppNode installed" 2>&1 | tee -a "$LOGFILE" dappnode_core_start - - echo "" - echo "Waiting for VPN initialization..." - wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 - - echo "" - echo "##############################################" - echo "# DAppNode VPN Access Credentials #" - echo "##############################################" - echo "" - echo "Your DAppNode is ready! Connect using your preferred VPN client." - echo "Choose either Wireguard (recommended) or OpenVPN and import the" - echo "credentials below into your VPN app to access your DAppNode." - echo "" - - echo "--- Wireguard ---" - docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials --localhost 2>&1 || \ - echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" - - echo "" - echo "--- OpenVPN ---" - docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin --localhost 2>&1 || \ - echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" - - echo "" - echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" + print_vpn_access_credentials fi } From 7545f0320a29aa18b90013f1db7cfcb03c5e43f7 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Wed, 4 Mar 2026 12:30:19 +0100 Subject: [PATCH 26/36] Refactor Linux setup steps to conditionally execute based on MINIMAL mode --- scripts/dappnode_install.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index b8b1ee3..2073838 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -971,25 +971,27 @@ main() { # --- Linux-only setup steps --- if $IS_LINUX; then - echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" - addSwap + if [[ "${MINIMAL}" != "true" ]]; then + echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" + addSwap - echo "Customizing login..." 2>&1 | tee -a "$LOGFILE" - customMotd + echo "Customizing login..." 2>&1 | tee -a "$LOGFILE" + customMotd - echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" - installExtraDpkg + echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" + installExtraDpkg - echo "Grabbing latest content hashes..." 2>&1 | tee -a "$LOGFILE" - grabContentHashes + echo "Grabbing latest content hashes..." 2>&1 | tee -a "$LOGFILE" + grabContentHashes - if [ "$ARCH" == "amd64" ]; then + if [ "$ARCH" == "amd64" ]; then echo "Installing SGX modules..." 2>&1 | tee -a "$LOGFILE" installSgx echo "Installing extra packages..." 2>&1 | tee -a "$LOGFILE" installExtraDpkg # TODO: Why is this being called twice? fi + fi echo "Adding user to docker group..." 2>&1 | tee -a "$LOGFILE" addUserToDockerGroup From 9c0a696ac1e88db08639e6c9df26e2b21e81b9ba Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:12:27 +0100 Subject: [PATCH 27/36] Add support for Notifications and Premium packages in installation scripts (#696) --- .dappnode_profile | 2 ++ .github/workflows/release.yml | 17 +++++++++++++++-- scripts/dappnode_install.sh | 13 +++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.dappnode_profile b/.dappnode_profile index 538e868..ece7e1b 100755 --- a/.dappnode_profile +++ b/.dappnode_profile @@ -14,6 +14,8 @@ export DAPPMANAGER_VERSION="${DAPPMANAGER_VERSION:-0.2.99}" export WIFI_VERSION="${WIFI_VERSION:-0.2.9}" export WIREGUARD_VERSION="${WIREGUARD_VERSION:-0.1.3}" export HTTPS_VERSION="${HTTPS:-0.2.2}" +export NOTIFICATIONS_VERSION="/ipfs/QmQubxH4WgqEFxvFntef4H2DmuU6mxBRS6XeuShc3owNi9" +export PREMIUM_VERSION="/ipfs/QmXDddoa56HkrvtkGsa3mozpsn9SzQZtoVBWgagcW6SsHy" export DAPPNODE_DIR="/usr/src/dappnode" export DAPPNODE_CORE_DIR="${DAPPNODE_DIR}/DNCORE" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94223b5..ad7c934 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,12 @@ on: vpn: description: "Version of the OpenVPN Package. Only numbers" required: true + notifications: + description: "IPFS hash of the Notifications Package. Must start with /ipfs/" + required: true + premium: + description: "IPFS hash of the Premium Package. Must start with /ipfs/" + required: true jobs: set-versions: @@ -39,13 +45,16 @@ jobs: wireguard: ${{ steps.set_outputs.outputs.wireguard }} https: ${{ steps.set_outputs.outputs.https }} vpn: ${{ steps.set_outputs.outputs.vpn }} + notifications: ${{ steps.set_outputs.outputs.notifications }} + premium: ${{ steps.set_outputs.outputs.premium }} core: ${{ steps.set_outputs.outputs.core }} steps: - name: Check versions regex run: | [[ "${{ github.event.inputs.bind }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.ipfs }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.dappmanager }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && \ [[ "${{ github.event.inputs.wifi }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.wireguard }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.https }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && \ - [[ "${{ github.event.inputs.vpn }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.core }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "versions introduced in wrong format"; exit 1; } + [[ "${{ github.event.inputs.vpn }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "${{ github.event.inputs.core }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && \ + [[ "${{ github.event.inputs.notifications }}" =~ ^/ipfs/.+$ ]] && [[ "${{ github.event.inputs.premium }}" =~ ^/ipfs/.+$ ]] || { echo "versions introduced in wrong format"; exit 1; } - name: Checkout uses: actions/checkout@v4 - name: Set new versions @@ -56,7 +65,9 @@ jobs: sed -i -e "/DAPPMANAGER_VERSION/s/[0-9]*\.[0-9]*\.[0-9]*/"${{ github.event.inputs.dappmanager }}"/" .dappnode_profile sed -i -e "/WIFI_VERSION/s/[0-9]*\.[0-9]*\.[0-9]*/"${{ github.event.inputs.wifi }}"/" .dappnode_profile sed -i -e "/WIREGUARD_VERSION/s/[0-9]*\.[0-9]*\.[0-9]*/"${{ github.event.inputs.wireguard }}"/" .dappnode_profile - sed -i -e "/HTTPS_VERSION/s/[0-9]*\.[0-9]*\.[0-9]*/"${{ github.event.inputs.https }}"/" .dappnode_profile + sed -i -e "/HTTPS_VERSION/s/[0-9]*\.[0-9]*\.[0-9]*/${{ github.event.inputs.https }}/" .dappnode_profile + sed -i -e "s|^export NOTIFICATIONS_VERSION=.*|export NOTIFICATIONS_VERSION=\"${{ github.event.inputs.notifications }}\"|" .dappnode_profile + sed -i -e "s|^export PREMIUM_VERSION=.*|export PREMIUM_VERSION=\"${{ github.event.inputs.premium }}\"|" .dappnode_profile cat .dappnode_profile - name: Create dappnode_profile.sh run: cp .dappnode_profile dappnode_profile.sh @@ -76,6 +87,8 @@ jobs: echo "wireguard=${{ github.event.inputs.wireguard }}" >> $GITHUB_OUTPUT echo "https=${{ github.event.inputs.https }}" >> $GITHUB_OUTPUT echo "vpn=${{ github.event.inputs.vpn }}" >> $GITHUB_OUTPUT + echo "notifications=${{ github.event.inputs.notifications }}" >> $GITHUB_OUTPUT + echo "premium=${{ github.event.inputs.premium }}" >> $GITHUB_OUTPUT build-debian-attended: name: Build Debian attended ISO diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 2073838..ea9fc86 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -540,7 +540,7 @@ is_port_used() { determine_packages() { # Global override: minimal install, regardless of OS. if [[ "${MINIMAL}" == "true" ]]; then - PKGS=(BIND VPN WIREGUARD DAPPMANAGER) + PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) log "Minimal mode enabled; overriding packages" log "Packages to be installed: ${PKGS[*]}" log "PKGS: ${PKGS[*]}" @@ -561,23 +561,24 @@ determine_packages() { if is_always_on_mac; then if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) else - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS NOTIFICATIONS PREMIUM) fi else - PKGS=(BIND VPN WIREGUARD DAPPMANAGER) + PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) fi else # Linux / ISO logic is_iso_install is_port_used if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) else - PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI) + PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) fi fi + log "Packages to be installed: ${PKGS[*]}" # Debug: print all PKGS and their version variables From 0a01230dde8e6d4927f07c5e8981eb24c3206fdc Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Thu, 5 Mar 2026 08:16:00 +0100 Subject: [PATCH 28/36] Add local profile path argument to DAppNode installation script --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 174f84c..deb5465 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: sudo /bin/bash ./scripts/dappnode_install_pre.sh UPDATE - name: Install DAppNode run: | - sudo /bin/bash ./scripts/dappnode_install.sh + sudo /bin/bash ./scripts/dappnode_install.sh --local-profile-path "$PWD/.dappnode_profile" - name: Show installation logs run: | cat /usr/src/dappnode/logs/install.log From a9b8931eebf9f251716a3ba37412531b17b8b1b5 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Thu, 5 Mar 2026 08:46:09 +0100 Subject: [PATCH 29/36] Add support for macOS in VPN access credential retrieval --- scripts/dappnode_install.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index ea9fc86..a19f628 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -196,6 +196,11 @@ wait_for_internal_ip() { # Print VPN access credentials (Wireguard + OpenVPN) after core has started. # Works on both Linux and macOS as long as the relevant containers are running. print_vpn_access_credentials() { + local localhost_flag=() + if $IS_MACOS; then + localhost_flag=(--localhost) + fi + echo "" echo "Waiting for VPN initialization..." wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 @@ -211,13 +216,13 @@ print_vpn_access_credentials() { echo "" echo "--- Wireguard ---" - docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials --localhost 2>&1 || \ - echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard --localhost" + docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "${localhost_flag[@]}" 2>&1 || \ + echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard${localhost_flag:+ ${localhost_flag[*]}}" echo "" echo "--- OpenVPN ---" - docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin --localhost 2>&1 || \ - echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin --localhost" + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin "${localhost_flag[@]}" 2>&1 || \ + echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin${localhost_flag:+ ${localhost_flag[*]}}" echo "" echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" From 692c4794d403659800ccf9074800bb531b6427df Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 08:24:04 +0100 Subject: [PATCH 30/36] Add support for custom package selection in installation script --- scripts/dappnode_install.sh | 77 ++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index a19f628..cb9fa48 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -20,6 +20,7 @@ set -Eeuo pipefail : "${STATIC_IP:=}" : "${LOCAL_PROFILE_PATH:=}" : "${MINIMAL:=false}" +: "${PACKAGES:=}" # Enable alias expansion in non-interactive bash scripts. # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. @@ -61,10 +62,11 @@ Options: --ipfs-endpoint Override IPFS gateway endpoint (equivalent: IPFS_ENDPOINT=...) --profile-url Override profile download URL (equivalent: PROFILE_URL=...) --minimal Force minimal package set: BIND VPN WIREGUARD DAPPMANAGER (equivalent: MINIMAL=true) + --packages Override package selection (comma or space separated), e.g. BIND,IPFS,VPN -h, --help Show this help Environment variables (also supported): - UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL + UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES EOF } @@ -99,6 +101,15 @@ parse_args() { MINIMAL=true shift ;; + --packages) + [[ $# -ge 2 ]] || die "--packages requires a package list argument" + PACKAGES="$2" + shift 2 + ;; + --packages=*) + PACKAGES="${1#*=}" + shift + ;; -h|--help) usage exit 0 @@ -543,6 +554,70 @@ is_port_used() { # Determine packages to be installed determine_packages() { + # Explicit package list override from flag/env always has top priority. + # It supersedes MINIMAL and any OS/port-based package determination. + if [[ -n "${PACKAGES//[[:space:],]/}" ]]; then + local raw token normalized + local custom_pkgs=() + + raw="${PACKAGES//,/ }" + for token in $raw; do + normalized="$(echo "$token" | tr '[:lower:]' '[:upper:]')" + case "$normalized" in + HTTPS|BIND|IPFS|VPN|WIREGUARD|DAPPMANAGER|WIFI|NOTIFICATIONS|PREMIUM) + ;; + *) + die "Unknown package in --packages/PACKAGES: '$token'. Allowed: HTTPS,BIND,IPFS,VPN,WIREGUARD,DAPPMANAGER,WIFI,NOTIFICATIONS,PREMIUM" + ;; + esac + + local exists=false + local pkg + for pkg in "${custom_pkgs[@]}"; do + if [[ "$pkg" == "$normalized" ]]; then + exists=true + break + fi + done + + if [[ "$exists" == "false" ]]; then + custom_pkgs+=("$normalized") + fi + done + + [[ ${#custom_pkgs[@]} -gt 0 ]] || die "--packages/PACKAGES was provided but no valid packages were found" + + # DAPPMANAGER is required for a functional install; ensure it's present on explicit overrides. + local has_dappmanager=false + local pkg + for pkg in "${custom_pkgs[@]}"; do + if [[ "$pkg" == "DAPPMANAGER" ]]; then + has_dappmanager=true + break + fi + done + if [[ "$has_dappmanager" == "false" ]]; then + custom_pkgs+=("DAPPMANAGER") + log "--packages/PACKAGES did not include DAPPMANAGER; appending it automatically" + fi + + if [[ "${MINIMAL}" == "true" ]]; then + log "Custom packages provided; overriding --minimal/MINIMAL" + fi + MINIMAL=false + PKGS=("${custom_pkgs[@]}") + + log "Packages override enabled via --packages/PACKAGES" + log "Packages to be installed: ${PKGS[*]}" + log "PKGS: ${PKGS[*]}" + for comp in "${PKGS[@]}"; do + local ver_var + ver_var="${comp}_VERSION" + log "$ver_var = ${!ver_var-}" + done + return 0 + fi + # Global override: minimal install, regardless of OS. if [[ "${MINIMAL}" == "true" ]]; then PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) From b281bceae952ce7453385a2670311f3378d8c947 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 09:41:44 +0100 Subject: [PATCH 31/36] Add interactive setup wizard to installation script --- scripts/dappnode_install.sh | 245 +++++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index cb9fa48..638a4d1 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -21,6 +21,16 @@ set -Eeuo pipefail : "${LOCAL_PROFILE_PATH:=}" : "${MINIMAL:=false}" : "${PACKAGES:=}" +: "${INTERACTIVE:=false}" + +# Track CLI-provided values so interactive mode does not override explicit flags. +CLI_SET_UPDATE=false +CLI_SET_STATIC_IP=false +CLI_SET_LOCAL_PROFILE_PATH=false +CLI_SET_MINIMAL=false +CLI_SET_PACKAGES=false +CLI_SET_IPFS_ENDPOINT=false +CLI_SET_PROFILE_URL=false # Enable alias expansion in non-interactive bash scripts. # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. @@ -63,10 +73,11 @@ Options: --profile-url Override profile download URL (equivalent: PROFILE_URL=...) --minimal Force minimal package set: BIND VPN WIREGUARD DAPPMANAGER (equivalent: MINIMAL=true) --packages Override package selection (comma or space separated), e.g. BIND,IPFS,VPN + --interactive Run an interactive setup wizard (equivalent: INTERACTIVE=true) -h, --help Show this help Environment variables (also supported): - UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES + UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES, INTERACTIVE EOF } @@ -75,39 +86,51 @@ parse_args() { case "$1" in --update) UPDATE=true + CLI_SET_UPDATE=true shift ;; --static-ip) [[ $# -ge 2 ]] || die "--static-ip requires an IPv4 argument" STATIC_IP="$2" + CLI_SET_STATIC_IP=true shift 2 ;; --local-profile-path) [[ $# -ge 2 ]] || die "--local-profile-path requires a path argument" LOCAL_PROFILE_PATH="$2" + CLI_SET_LOCAL_PROFILE_PATH=true shift 2 ;; --ipfs-endpoint) [[ $# -ge 2 ]] || die "--ipfs-endpoint requires a URL argument" IPFS_ENDPOINT="$2" + CLI_SET_IPFS_ENDPOINT=true shift 2 ;; --profile-url) [[ $# -ge 2 ]] || die "--profile-url requires a URL argument" PROFILE_URL="$2" + CLI_SET_PROFILE_URL=true shift 2 ;; --minimal) MINIMAL=true + CLI_SET_MINIMAL=true shift ;; --packages) [[ $# -ge 2 ]] || die "--packages requires a package list argument" PACKAGES="$2" + CLI_SET_PACKAGES=true shift 2 ;; --packages=*) PACKAGES="${1#*=}" + CLI_SET_PACKAGES=true + shift + ;; + --interactive) + INTERACTIVE=true shift ;; -h|--help) @@ -194,6 +217,7 @@ wait_for_internal_ip() { hostname_value="$(printf '%s\n' "$hostname_result" | head -n 1 | tr -d '\r' | xargs)" if [[ "$internal_http_code" == "200" && -n "$internal_value" && "$internal_value" != "null" && "$hostname_http_code" == "200" && -n "$hostname_value" && "$hostname_value" != "null" ]]; then + sleep 2 # Extra buffer to ensure values are fully propagated before we proceed echo "INTERNAL_IP is ready: $internal_value" echo "HOSTNAME is ready: $hostname_value" return 0 @@ -683,6 +707,224 @@ valid_ip() { [[ ${octets[0]} -le 255 && ${octets[1]} -le 255 && ${octets[2]} -le 255 && ${octets[3]} -le 255 ]] } +is_interactive_tty() { + [[ -t 0 && -t 1 ]] +} + +prompt_yes_no() { + local question="$1" + local default_answer="$2" + local answer normalized + + while true; do + if [[ "$default_answer" == "y" ]]; then + read -r -p "$question [Y/n]: " answer + answer="${answer:-y}" + else + read -r -p "$question [y/N]: " answer + answer="${answer:-n}" + fi + + normalized="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')" + + case "$normalized" in + y|yes) + return 0 + ;; + n|no) + return 1 + ;; + *) + echo "Please answer 'y' or 'n'." + ;; + esac + done +} + +print_package_descriptions() { + echo "" + echo "Package quick guide:" + echo " HTTPS Reverse proxy + HTTPS entrypoint for web access" + echo " BIND DNS resolver used by DAppNode services" + echo " IPFS Distributed storage and content fetch layer" + echo " VPN OpenVPN access to your DAppNode" + echo " WIREGUARD WireGuard access to your DAppNode" + echo " DAPPMANAGER Core DAppNode management service (required)" + echo " WIFI Wi-Fi access point support" + echo " NOTIFICATIONS Notification service package" + echo " PREMIUM Premium features package" +} + +interactive_configure_install() { + if [[ "${INTERACTIVE}" != "true" ]]; then + return 0 + fi + + if ! is_interactive_tty; then + warn "--interactive requested but no TTY detected; continuing in non-interactive mode" + INTERACTIVE=false + return 0 + fi + + echo "" + echo "##############################################" + echo "#### DAPPNODE INTERACTIVE SETUP ####" + echo "##############################################" + + # --update + if [[ "$CLI_SET_UPDATE" == "false" ]]; then + echo "" + echo "Update mode:" + echo " - Yes: removes previously downloaded installer artifacts before running" + echo " - No : keeps existing artifacts and reuses what is already present" + if [[ "$UPDATE" == "true" ]]; then + prompt_yes_no "Clean existing artifacts before installing?" "y" && UPDATE=true || UPDATE=false + else + prompt_yes_no "Clean existing artifacts before installing?" "n" && UPDATE=true || UPDATE=false + fi + else + log "Skipping update prompt: value set via CLI flag" + fi + + # Package mode + if [[ "$CLI_SET_PACKAGES" == "true" ]]; then + log "Skipping package selection prompts: --packages was provided" + print_package_descriptions + elif [[ "$CLI_SET_MINIMAL" == "true" ]]; then + log "Skipping package mode prompt: --minimal was provided" + print_package_descriptions + else + local package_mode + while true; do + print_package_descriptions + echo "" + echo "Package selection mode:" + echo " 1) Automatic (recommended)" + echo " Installer chooses packages based on OS/machine and open ports" + echo " 2) Minimal" + echo " Installs: BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM" + echo " 3) Custom package list" + echo " Enter only the packages you want (DAPPMANAGER is auto-added if missing)" + read -r -p "Choose package mode [1/2/3] (default 1): " package_mode + package_mode="${package_mode:-1}" + + case "$package_mode" in + 1) + MINIMAL=false + PACKAGES="" + break + ;; + 2) + MINIMAL=true + PACKAGES="" + break + ;; + 3) + MINIMAL=false + while true; do + echo "Example: BIND,IPFS,VPN,WIREGUARD,DAPPMANAGER" + read -r -p "Enter packages (comma or space separated): " PACKAGES + if [[ -n "${PACKAGES//[[:space:],]/}" ]]; then + break + fi + echo "Package list cannot be empty." + done + break + ;; + *) + echo "Please choose 1, 2, or 3." + ;; + esac + done + fi + + # Static IP + if [[ "$CLI_SET_STATIC_IP" == "false" ]]; then + local STATIC_IP_INPUT + echo "" + echo "Static IP option:" + echo " - Use this only if you need a fixed LAN IP for your DAppNode" + echo " - If unsure, skip and use automatic DHCP" + if [[ -n "$STATIC_IP" ]]; then + if prompt_yes_no "Use static IP ($STATIC_IP)?" "y"; then + while true; do + read -r -p "Static IP [${STATIC_IP}]: " STATIC_IP_INPUT + STATIC_IP_INPUT="${STATIC_IP_INPUT:-$STATIC_IP}" + if valid_ip "$STATIC_IP_INPUT"; then + STATIC_IP="$STATIC_IP_INPUT" + break + fi + echo "Invalid IPv4 address." + done + else + STATIC_IP="" + fi + else + if prompt_yes_no "Configure a static IP?" "n"; then + while true; do + read -r -p "Static IP (IPv4): " STATIC_IP_INPUT + if valid_ip "$STATIC_IP_INPUT"; then + STATIC_IP="$STATIC_IP_INPUT" + break + fi + echo "Invalid IPv4 address." + done + fi + fi + else + log "Skipping static IP prompt: value set via CLI flag" + fi + + # Optional URL/path overrides + if [[ "$CLI_SET_LOCAL_PROFILE_PATH" == "false" ]]; then + local LOCAL_PROFILE_PATH_INPUT + echo "" + echo "Optional overrides:" + echo " - Leave empty to use default values" + read -r -p "Local profile path override (leave empty to keep current): " LOCAL_PROFILE_PATH_INPUT + if [[ -n "$LOCAL_PROFILE_PATH_INPUT" ]]; then + LOCAL_PROFILE_PATH="$LOCAL_PROFILE_PATH_INPUT" + fi + else + log "Skipping local profile path prompt: value set via CLI flag" + fi + + if [[ "$CLI_SET_IPFS_ENDPOINT" == "false" ]]; then + local IPFS_ENDPOINT_INPUT + read -r -p "IPFS endpoint override (leave empty to keep current): " IPFS_ENDPOINT_INPUT + if [[ -n "$IPFS_ENDPOINT_INPUT" ]]; then + IPFS_ENDPOINT="$IPFS_ENDPOINT_INPUT" + fi + else + log "Skipping IPFS endpoint prompt: value set via CLI flag" + fi + + if [[ "$CLI_SET_PROFILE_URL" == "false" ]]; then + local PROFILE_URL_INPUT + read -r -p "Profile URL override (leave empty to keep current): " PROFILE_URL_INPUT + if [[ -n "$PROFILE_URL_INPUT" ]]; then + PROFILE_URL="$PROFILE_URL_INPUT" + fi + else + log "Skipping profile URL prompt: value set via CLI flag" + fi + + # Summary + confirmation before any heavy operations + echo "" + echo "Interactive configuration summary:" + echo " UPDATE=${UPDATE}" + echo " MINIMAL=${MINIMAL}" + echo " PACKAGES=${PACKAGES:-}" + echo " STATIC_IP=${STATIC_IP:-}" + echo " LOCAL_PROFILE_PATH=${LOCAL_PROFILE_PATH:-}" + echo " IPFS_ENDPOINT=${IPFS_ENDPOINT}" + echo " PROFILE_URL=${PROFILE_URL}" + + if ! prompt_yes_no "Continue with this configuration?" "y"; then + die "Installation cancelled by user" + fi +} + configure_static_ip() { if [[ -z "${STATIC_IP}" ]]; then return 0 @@ -1038,6 +1280,7 @@ addUserToDockerGroup() { main() { parse_args "$@" + interactive_configure_install bootstrap_filesystem check_prereqs From dcb6b562b68e831b27951ad90ef8ff8805040f93 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 09:43:02 +0100 Subject: [PATCH 32/36] Add conditional output for VPN credentials based on selected packages --- scripts/dappnode_install.sh | 39 ++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 638a4d1..fb5406f 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -232,10 +232,28 @@ wait_for_internal_ip() { # Works on both Linux and macOS as long as the relevant containers are running. print_vpn_access_credentials() { local localhost_flag=() + local has_wireguard=false + local has_vpn=false + local pkg + if $IS_MACOS; then localhost_flag=(--localhost) fi + for pkg in "${PKGS[@]}"; do + if [[ "$pkg" == "WIREGUARD" ]]; then + has_wireguard=true + elif [[ "$pkg" == "VPN" ]]; then + has_vpn=true + fi + done + + if [[ "$has_wireguard" != "true" && "$has_vpn" != "true" ]]; then + echo "" + echo "No VPN package selected (VPN/WIREGUARD). Skipping credentials output." + return 0 + fi + echo "" echo "Waiting for VPN initialization..." wait_for_internal_ip "DAppNodeCore-dappmanager.dnp.dappnode.eth" 120 20 @@ -250,14 +268,21 @@ print_vpn_access_credentials() { echo "credentials below into your VPN app to access your DAppNode." echo "" - echo "--- Wireguard ---" - docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "${localhost_flag[@]}" 2>&1 || \ - echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard${localhost_flag:+ ${localhost_flag[*]}}" + if [[ "$has_wireguard" == "true" ]]; then + echo "--- Wireguard ---" + docker exec -i DAppNodeCore-api.wireguard.dnp.dappnode.eth getWireguardCredentials "${localhost_flag[@]}" 2>&1 || \ + echo "Wireguard credentials not yet available. Try later with: dappnode_wireguard${localhost_flag:+ ${localhost_flag[*]}}" + fi - echo "" - echo "--- OpenVPN ---" - docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin "${localhost_flag[@]}" 2>&1 || \ - echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin${localhost_flag:+ ${localhost_flag[*]}}" + if [[ "$has_wireguard" == "true" && "$has_vpn" == "true" ]]; then + echo "" + fi + + if [[ "$has_vpn" == "true" ]]; then + echo "--- OpenVPN ---" + docker exec -i DAppNodeCore-vpn.dnp.dappnode.eth vpncli get dappnode_admin "${localhost_flag[@]}" 2>&1 || \ + echo "OpenVPN credentials not yet available. Try later with: dappnode_openvpn_get dappnode_admin${localhost_flag:+ ${localhost_flag[*]}}" + fi echo "" echo "Import the configuration above into your VPN client of choice to access your DAppNode at http://my.dappnode" From 6f65644c5f9b11e876d7c481cbcc69455ad46563 Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 09:47:59 +0100 Subject: [PATCH 33/36] Remove interactive mode support from installation script --- scripts/dappnode_install.sh | 244 +----------------------------------- 1 file changed, 1 insertion(+), 243 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index fb5406f..a0e4817 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -21,16 +21,6 @@ set -Eeuo pipefail : "${LOCAL_PROFILE_PATH:=}" : "${MINIMAL:=false}" : "${PACKAGES:=}" -: "${INTERACTIVE:=false}" - -# Track CLI-provided values so interactive mode does not override explicit flags. -CLI_SET_UPDATE=false -CLI_SET_STATIC_IP=false -CLI_SET_LOCAL_PROFILE_PATH=false -CLI_SET_MINIMAL=false -CLI_SET_PACKAGES=false -CLI_SET_IPFS_ENDPOINT=false -CLI_SET_PROFILE_URL=false # Enable alias expansion in non-interactive bash scripts. # Required so commands like `dappnode_wireguard` (defined as aliases in `.dappnode_profile`) work. @@ -73,11 +63,10 @@ Options: --profile-url Override profile download URL (equivalent: PROFILE_URL=...) --minimal Force minimal package set: BIND VPN WIREGUARD DAPPMANAGER (equivalent: MINIMAL=true) --packages Override package selection (comma or space separated), e.g. BIND,IPFS,VPN - --interactive Run an interactive setup wizard (equivalent: INTERACTIVE=true) -h, --help Show this help Environment variables (also supported): - UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES, INTERACTIVE + UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES EOF } @@ -86,51 +75,39 @@ parse_args() { case "$1" in --update) UPDATE=true - CLI_SET_UPDATE=true shift ;; --static-ip) [[ $# -ge 2 ]] || die "--static-ip requires an IPv4 argument" STATIC_IP="$2" - CLI_SET_STATIC_IP=true shift 2 ;; --local-profile-path) [[ $# -ge 2 ]] || die "--local-profile-path requires a path argument" LOCAL_PROFILE_PATH="$2" - CLI_SET_LOCAL_PROFILE_PATH=true shift 2 ;; --ipfs-endpoint) [[ $# -ge 2 ]] || die "--ipfs-endpoint requires a URL argument" IPFS_ENDPOINT="$2" - CLI_SET_IPFS_ENDPOINT=true shift 2 ;; --profile-url) [[ $# -ge 2 ]] || die "--profile-url requires a URL argument" PROFILE_URL="$2" - CLI_SET_PROFILE_URL=true shift 2 ;; --minimal) MINIMAL=true - CLI_SET_MINIMAL=true shift ;; --packages) [[ $# -ge 2 ]] || die "--packages requires a package list argument" PACKAGES="$2" - CLI_SET_PACKAGES=true shift 2 ;; --packages=*) PACKAGES="${1#*=}" - CLI_SET_PACKAGES=true - shift - ;; - --interactive) - INTERACTIVE=true shift ;; -h|--help) @@ -732,224 +709,6 @@ valid_ip() { [[ ${octets[0]} -le 255 && ${octets[1]} -le 255 && ${octets[2]} -le 255 && ${octets[3]} -le 255 ]] } -is_interactive_tty() { - [[ -t 0 && -t 1 ]] -} - -prompt_yes_no() { - local question="$1" - local default_answer="$2" - local answer normalized - - while true; do - if [[ "$default_answer" == "y" ]]; then - read -r -p "$question [Y/n]: " answer - answer="${answer:-y}" - else - read -r -p "$question [y/N]: " answer - answer="${answer:-n}" - fi - - normalized="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')" - - case "$normalized" in - y|yes) - return 0 - ;; - n|no) - return 1 - ;; - *) - echo "Please answer 'y' or 'n'." - ;; - esac - done -} - -print_package_descriptions() { - echo "" - echo "Package quick guide:" - echo " HTTPS Reverse proxy + HTTPS entrypoint for web access" - echo " BIND DNS resolver used by DAppNode services" - echo " IPFS Distributed storage and content fetch layer" - echo " VPN OpenVPN access to your DAppNode" - echo " WIREGUARD WireGuard access to your DAppNode" - echo " DAPPMANAGER Core DAppNode management service (required)" - echo " WIFI Wi-Fi access point support" - echo " NOTIFICATIONS Notification service package" - echo " PREMIUM Premium features package" -} - -interactive_configure_install() { - if [[ "${INTERACTIVE}" != "true" ]]; then - return 0 - fi - - if ! is_interactive_tty; then - warn "--interactive requested but no TTY detected; continuing in non-interactive mode" - INTERACTIVE=false - return 0 - fi - - echo "" - echo "##############################################" - echo "#### DAPPNODE INTERACTIVE SETUP ####" - echo "##############################################" - - # --update - if [[ "$CLI_SET_UPDATE" == "false" ]]; then - echo "" - echo "Update mode:" - echo " - Yes: removes previously downloaded installer artifacts before running" - echo " - No : keeps existing artifacts and reuses what is already present" - if [[ "$UPDATE" == "true" ]]; then - prompt_yes_no "Clean existing artifacts before installing?" "y" && UPDATE=true || UPDATE=false - else - prompt_yes_no "Clean existing artifacts before installing?" "n" && UPDATE=true || UPDATE=false - fi - else - log "Skipping update prompt: value set via CLI flag" - fi - - # Package mode - if [[ "$CLI_SET_PACKAGES" == "true" ]]; then - log "Skipping package selection prompts: --packages was provided" - print_package_descriptions - elif [[ "$CLI_SET_MINIMAL" == "true" ]]; then - log "Skipping package mode prompt: --minimal was provided" - print_package_descriptions - else - local package_mode - while true; do - print_package_descriptions - echo "" - echo "Package selection mode:" - echo " 1) Automatic (recommended)" - echo " Installer chooses packages based on OS/machine and open ports" - echo " 2) Minimal" - echo " Installs: BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM" - echo " 3) Custom package list" - echo " Enter only the packages you want (DAPPMANAGER is auto-added if missing)" - read -r -p "Choose package mode [1/2/3] (default 1): " package_mode - package_mode="${package_mode:-1}" - - case "$package_mode" in - 1) - MINIMAL=false - PACKAGES="" - break - ;; - 2) - MINIMAL=true - PACKAGES="" - break - ;; - 3) - MINIMAL=false - while true; do - echo "Example: BIND,IPFS,VPN,WIREGUARD,DAPPMANAGER" - read -r -p "Enter packages (comma or space separated): " PACKAGES - if [[ -n "${PACKAGES//[[:space:],]/}" ]]; then - break - fi - echo "Package list cannot be empty." - done - break - ;; - *) - echo "Please choose 1, 2, or 3." - ;; - esac - done - fi - - # Static IP - if [[ "$CLI_SET_STATIC_IP" == "false" ]]; then - local STATIC_IP_INPUT - echo "" - echo "Static IP option:" - echo " - Use this only if you need a fixed LAN IP for your DAppNode" - echo " - If unsure, skip and use automatic DHCP" - if [[ -n "$STATIC_IP" ]]; then - if prompt_yes_no "Use static IP ($STATIC_IP)?" "y"; then - while true; do - read -r -p "Static IP [${STATIC_IP}]: " STATIC_IP_INPUT - STATIC_IP_INPUT="${STATIC_IP_INPUT:-$STATIC_IP}" - if valid_ip "$STATIC_IP_INPUT"; then - STATIC_IP="$STATIC_IP_INPUT" - break - fi - echo "Invalid IPv4 address." - done - else - STATIC_IP="" - fi - else - if prompt_yes_no "Configure a static IP?" "n"; then - while true; do - read -r -p "Static IP (IPv4): " STATIC_IP_INPUT - if valid_ip "$STATIC_IP_INPUT"; then - STATIC_IP="$STATIC_IP_INPUT" - break - fi - echo "Invalid IPv4 address." - done - fi - fi - else - log "Skipping static IP prompt: value set via CLI flag" - fi - - # Optional URL/path overrides - if [[ "$CLI_SET_LOCAL_PROFILE_PATH" == "false" ]]; then - local LOCAL_PROFILE_PATH_INPUT - echo "" - echo "Optional overrides:" - echo " - Leave empty to use default values" - read -r -p "Local profile path override (leave empty to keep current): " LOCAL_PROFILE_PATH_INPUT - if [[ -n "$LOCAL_PROFILE_PATH_INPUT" ]]; then - LOCAL_PROFILE_PATH="$LOCAL_PROFILE_PATH_INPUT" - fi - else - log "Skipping local profile path prompt: value set via CLI flag" - fi - - if [[ "$CLI_SET_IPFS_ENDPOINT" == "false" ]]; then - local IPFS_ENDPOINT_INPUT - read -r -p "IPFS endpoint override (leave empty to keep current): " IPFS_ENDPOINT_INPUT - if [[ -n "$IPFS_ENDPOINT_INPUT" ]]; then - IPFS_ENDPOINT="$IPFS_ENDPOINT_INPUT" - fi - else - log "Skipping IPFS endpoint prompt: value set via CLI flag" - fi - - if [[ "$CLI_SET_PROFILE_URL" == "false" ]]; then - local PROFILE_URL_INPUT - read -r -p "Profile URL override (leave empty to keep current): " PROFILE_URL_INPUT - if [[ -n "$PROFILE_URL_INPUT" ]]; then - PROFILE_URL="$PROFILE_URL_INPUT" - fi - else - log "Skipping profile URL prompt: value set via CLI flag" - fi - - # Summary + confirmation before any heavy operations - echo "" - echo "Interactive configuration summary:" - echo " UPDATE=${UPDATE}" - echo " MINIMAL=${MINIMAL}" - echo " PACKAGES=${PACKAGES:-}" - echo " STATIC_IP=${STATIC_IP:-}" - echo " LOCAL_PROFILE_PATH=${LOCAL_PROFILE_PATH:-}" - echo " IPFS_ENDPOINT=${IPFS_ENDPOINT}" - echo " PROFILE_URL=${PROFILE_URL}" - - if ! prompt_yes_no "Continue with this configuration?" "y"; then - die "Installation cancelled by user" - fi -} - configure_static_ip() { if [[ -z "${STATIC_IP}" ]]; then return 0 @@ -1305,7 +1064,6 @@ addUserToDockerGroup() { main() { parse_args "$@" - interactive_configure_install bootstrap_filesystem check_prereqs From 70ec401c97f5986ebb62a489a290149ad354f73a Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 10:15:41 +0100 Subject: [PATCH 34/36] Add support for lite installation mode in the installation script --- scripts/dappnode_install.sh | 44 ++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index a0e4817..3a81064 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -20,6 +20,7 @@ set -Eeuo pipefail : "${STATIC_IP:=}" : "${LOCAL_PROFILE_PATH:=}" : "${MINIMAL:=false}" +: "${LITE:=false}" : "${PACKAGES:=}" # Enable alias expansion in non-interactive bash scripts. @@ -61,12 +62,13 @@ Options: --local-profile-path Use a local .dappnode_profile instead of downloading (equivalent: LOCAL_PROFILE_PATH=...) --ipfs-endpoint Override IPFS gateway endpoint (equivalent: IPFS_ENDPOINT=...) --profile-url Override profile download URL (equivalent: PROFILE_URL=...) - --minimal Force minimal package set: BIND VPN WIREGUARD DAPPMANAGER (equivalent: MINIMAL=true) + --minimal Install only BIND DAPPMANAGER NOTIFICATIONS PREMIUM (equivalent: MINIMAL=true) + --lite Install reduced package set: BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM (equivalent: LITE=true) --packages Override package selection (comma or space separated), e.g. BIND,IPFS,VPN -h, --help Show this help Environment variables (also supported): - UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, PACKAGES + UPDATE, STATIC_IP, LOCAL_PROFILE_PATH, IPFS_ENDPOINT, PROFILE_URL, MINIMAL, LITE, PACKAGES EOF } @@ -101,6 +103,10 @@ parse_args() { MINIMAL=true shift ;; + --lite) + LITE=true + shift + ;; --packages) [[ $# -ge 2 ]] || die "--packages requires a package list argument" PACKAGES="$2" @@ -125,6 +131,12 @@ parse_args() { done } +validate_install_mode() { + if [[ "${MINIMAL}" == "true" && "${LITE}" == "true" ]]; then + die "--minimal and --lite are mutually exclusive" + fi +} + require_cmd() { local cmd="$1" command -v "$cmd" >/dev/null 2>&1 || die "Missing required command: $cmd" @@ -581,7 +593,7 @@ is_port_used() { # Determine packages to be installed determine_packages() { # Explicit package list override from flag/env always has top priority. - # It supersedes MINIMAL and any OS/port-based package determination. + # It supersedes MINIMAL/LITE and any OS/port-based package determination. if [[ -n "${PACKAGES//[[:space:],]/}" ]]; then local raw token normalized local custom_pkgs=() @@ -627,10 +639,11 @@ determine_packages() { log "--packages/PACKAGES did not include DAPPMANAGER; appending it automatically" fi - if [[ "${MINIMAL}" == "true" ]]; then - log "Custom packages provided; overriding --minimal/MINIMAL" + if [[ "${MINIMAL}" == "true" || "${LITE}" == "true" ]]; then + log "Custom packages provided; overriding --minimal/--lite and MINIMAL/LITE" fi MINIMAL=false + LITE=false PKGS=("${custom_pkgs[@]}") log "Packages override enabled via --packages/PACKAGES" @@ -644,9 +657,9 @@ determine_packages() { return 0 fi - # Global override: minimal install, regardless of OS. + # Global override: new minimal install, regardless of OS. if [[ "${MINIMAL}" == "true" ]]; then - PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) + PKGS=(BIND DAPPMANAGER NOTIFICATIONS PREMIUM) log "Minimal mode enabled; overriding packages" log "Packages to be installed: ${PKGS[*]}" log "PKGS: ${PKGS[*]}" @@ -658,6 +671,20 @@ determine_packages() { return 0 fi + # Global override: lite install (former minimal behavior), regardless of OS. + if [[ "${LITE}" == "true" ]]; then + PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) + log "Lite mode enabled; overriding packages" + log "Packages to be installed: ${PKGS[*]}" + log "PKGS: ${PKGS[*]}" + for comp in "${PKGS[@]}"; do + local ver_var + ver_var="${comp}_VERSION" + log "$ver_var = ${!ver_var-}" + done + return 0 + fi + # macOS: package selection depends on whether the Mac is suitable to run always-on. # - non-server mac: BIND VPN WIREGUARD DAPPMANAGER # - server mac: BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS @@ -1064,6 +1091,7 @@ addUserToDockerGroup() { main() { parse_args "$@" + validate_install_mode bootstrap_filesystem check_prereqs @@ -1078,7 +1106,7 @@ main() { # --- Linux-only setup steps --- if $IS_LINUX; then - if [[ "${MINIMAL}" != "true" ]]; then + if [[ "${MINIMAL}" != "true" && "${LITE}" != "true" ]]; then echo "Creating swap memory..." 2>&1 | tee -a "$LOGFILE" addSwap From ebadae4a34f712c754e6be4fdb4428a28d4cc4af Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 11:11:55 +0100 Subject: [PATCH 35/36] Refactor installation script by removing macOS-specific checks and unused functions --- scripts/dappnode_install.sh | 86 +++---------------------------------- 1 file changed, 7 insertions(+), 79 deletions(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index 3a81064..fef7610 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -343,28 +343,6 @@ fi # Cross-platform Helpers # ############################## -# macOS: determine whether we're running on a server-suitable always-on Mac. -# Heuristic: treat Mac mini / Mac Studio / Mac Pro as always-on capable. -# Returns 0 (true) if server-class, 1 otherwise. -is_always_on_mac() { - # Non-macOS hosts are not considered always-on Macs - if ! $IS_MACOS; then - return 1 - fi - - local model - model="$(sysctl -n hw.model 2>/dev/null)" || return 1 - - case "$model" in - Macmini*|MacStudio*|MacPro*) - return 0 - ;; - *) - return 1 - ;; - esac -} - # Download a file: download_file download_file() { local dest="$1" @@ -461,6 +439,7 @@ remove_logging_section() { sed_inplace '/logging/d;/journald/d' "$file" } +# TODO: review difference between this and patch_compose_paths # Replace Linux paths with macOS paths in compose files patch_compose_paths() { local file="$1" @@ -511,16 +490,6 @@ patch_dappmanager_compose_for_macos() { rm -f "$insert_file" || true } -# TODO: remove once profile macos-compatibility published -# Patch .dappnode_profile for macOS compatibility -patch_profile_for_macos() { - local profile="$1" - # Replace GNU find -printf with POSIX-compatible -exec printf - sed_inplace 's/-printf "-f %p "/-exec printf -- "-f %s " {} \\;/' "$profile" - # Replace hardcoded Linux paths with $HOME-based paths - sed_inplace 's|/usr/src/dappnode|\$HOME/dappnode|g' "$profile" -} - bootstrap_filesystem() { # Clean if update if [[ "${UPDATE}" == "true" ]]; then @@ -546,21 +515,6 @@ bootstrap_filesystem() { touch "${LOGFILE}" || true } -# TEMPORARY: think a way to integrate flags instead of use files to detect installation type -is_iso_install() { - # ISO installs are Linux-only - if $IS_MACOS; then - IS_ISO_INSTALL=false - return - fi - # Check old and new location of iso_install.log - if [ -f "${DAPPNODE_DIR}/iso_install.log" ] || [ -f "${DAPPNODE_DIR}/logs/iso_install.log" ]; then - IS_ISO_INSTALL=true - else - IS_ISO_INSTALL=false - fi -} - # Check if port 80 is in use (necessary for HTTPS) # Returns IS_PORT_USED=true only if port 80 or 443 is used by something OTHER than our HTTPS container is_port_used() { @@ -685,31 +639,13 @@ determine_packages() { return 0 fi - # macOS: package selection depends on whether the Mac is suitable to run always-on. - # - non-server mac: BIND VPN WIREGUARD DAPPMANAGER - # - server mac: BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS - # NOTE: HTTPS may be skipped if ports 80/443 are already in use. - if $IS_MACOS; then - is_port_used - - if is_always_on_mac; then - if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) - else - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI HTTPS NOTIFICATIONS PREMIUM) - fi - else - PKGS=(BIND VPN WIREGUARD DAPPMANAGER NOTIFICATIONS PREMIUM) - fi + # Default mode (no --packages/--minimal/--lite): install full package set. + # HTTPS is included only when ports 80/443 are available. + is_port_used + if [ "$IS_PORT_USED" == "true" ]; then + PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) else - # Linux / ISO logic - is_iso_install - is_port_used - if [ "$IS_PORT_USED" == "true" ]; then - PKGS=(BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) - else - PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) - fi + PKGS=(HTTPS BIND IPFS VPN WIREGUARD DAPPMANAGER WIFI NOTIFICATIONS PREMIUM) fi log "Packages to be installed: ${PKGS[*]}" @@ -757,18 +693,10 @@ ensure_profile_loaded() { download_file "${DAPPNODE_PROFILE}" "${PROFILE_URL}" fi - # Patch profile for macOS compatibility (replace GNU-isms and hardcoded Linux paths) - # TODO: remove once profile macos-compatibility published - if $IS_MACOS; then - patch_profile_for_macos "$DAPPNODE_PROFILE" - fi - # shellcheck disable=SC1090 source "${DAPPNODE_PROFILE}" } - - resolve_packages() { # The indirect variable expansion used in ${!ver##*:} allows us to use versions like 'dev:development' # If such variable with 'dev:'' suffix is used, then the component is built from specified branch or commit. From 15324316111a63930426408f8c0d532b3695088f Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Fri, 6 Mar 2026 11:16:05 +0100 Subject: [PATCH 36/36] Improve Docker prerequisite checks in installation script --- scripts/dappnode_install.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/dappnode_install.sh b/scripts/dappnode_install.sh index fef7610..a1d8a81 100755 --- a/scripts/dappnode_install.sh +++ b/scripts/dappnode_install.sh @@ -153,7 +153,15 @@ require_downloader() { } check_prereqs() { - require_cmd docker + if ! command -v docker >/dev/null 2>&1; then + die "Docker is not installed. Install Docker first, then re-run this installer." + fi + + # Docker CLI may exist while the daemon is stopped/unreachable. + if ! docker info >/dev/null 2>&1; then + die "Docker is installed but not running (or not reachable). Start Docker and try again." + fi + require_downloader # Ensure compose is available (Docker Desktop / modern docker engine)