#!/bin/bash # ============================================================================== # Ubuntu Server Management Script # # A menu-driven script to perform common server maintenance tasks. # To add a new action: # 1. Create a new function for your action (e.g., my_new_action()). # 2. Add an option for it in the main_menu() function's 'echo' block. # 3. Add a case for it in the main_menu() function's 'case' statement. # 4. If you want it to run as part of "Run All", add it to run_all_actions(). # ============================================================================== # --- Script Configuration & Safety --- # Exit immediately if a command exits with a non-zero status. set -e # Treat unset variables as an error when substituting. set -u # Exit status of a pipeline is the status of the last command to exit with a # non-zero status, or zero if no command exited with a non-zero status. set -o pipefail # --- Color Definitions --- GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' # No Color # --- Prerequisite Check --- # Check if the script is run as root if [[ $EUID -ne 0 ]]; then echo -e "${RED}This script must be run as root. Please use sudo.${NC}" exit 1 fi # ============================================================================== # --- ACTION FUNCTIONS --- # Each function represents a specific action the user can choose. # ============================================================================== # --- Action: Update the system --- function update_system() { echo -e "\n${GREEN}--- Starting System Update ---${NC}" echo "Updating package lists..." DEBIAN_FRONTEND=noninteractive apt-get update -y echo "Upgrading installed packages..." # Use DEBIAN_FRONTEND to avoid interactive prompts during upgrade DEBIAN_FRONTEND=noninteractive apt-get upgrade -y echo "Cleaning up unused packages..." DEBIAN_FRONTEND=noninteractive apt-get autoremove -y DEBIAN_FRONTEND=noninteractive apt-get clean echo -e "${GREEN}--- System Update Complete ---${NC}" } # --- Action: Set the system to run unattended upgrades --- function setup_unattended_upgrades() { echo -e "\n${GREEN}--- Setting Up Unattended Upgrades ---${NC}" echo "Updating package lists..." DEBIAN_FRONTEND=noninteractive apt-get update -y echo "Installing unattended-upgrades package..." DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades echo "Configuring unattended-upgrades to run automatically..." # The DEBIAN_FRONTEND variable prevents the configuration dialog from appearing DEBIAN_FRONTEND=noninteractive dpkg-reconfigure --priority=low unattended-upgrades echo "Unattended upgrades configuration complete. Checking status:" # Use --no-pager to print status directly to the console without opening a pager systemctl status unattended-upgrades --no-pager echo -e "${GREEN}--- Unattended Upgrades Setup Complete ---${NC}" } # --- Action: Setup a new user --- function setup_new_user() { echo -e "\n${GREEN}--- Setup a New User ---${NC}" read -rp "Enter the new username: " username if id -u "${username}" &>/dev/null; then echo -e "${RED}Error: User '${username}' already exists.${NC}"; return 1; fi local password password_confirm while true; do read -s -p "Enter password for ${username}: " password echo read -s -p "Confirm password: " password_confirm echo if [[ "$password" == "$password_confirm" ]] && [[ -n "$password" ]]; then break elif [[ -z "$password" ]]; then echo -e "${RED}Password cannot be empty. Please try again.${NC}" else echo -e "${RED}Passwords do not match. Please try again.${NC}" fi done echo "Creating user '${username}'..." adduser --disabled-password --gecos "" "${username}" echo "${username}:${password}" | chpasswd echo "Password set for '${username}'." echo "Adding user to 'sudo' group..." usermod -aG sudo "${username}" if getent group docker > /dev/null; then echo "Adding user to 'docker' group..." usermod -aG docker "${username}" else echo -e "${YELLOW}Docker group not found. Skipping.${NC}" fi echo -e "${GREEN}--- User '${username}' created successfully ---${NC}" echo "User can log in via SSH with their password." } # --- Action: Install SSH Public Key --- function add_ssh_key() { echo -e "\n${GREEN}--- Add SSH Public Key ---${NC}" local target_user # Prefer the user who invoked sudo, otherwise prompt. if [[ -n "${SUDO_USER-}" ]]; then target_user="${SUDO_USER}" echo "Detected script was run with sudo by user: ${YELLOW}${target_user}${NC}" else read -rp "Enter the username to add the SSH key for: " target_user fi if ! id "${target_user}" &>/dev/null; then echo -e "${RED}Error: User '${target_user}' does not exist.${NC}"; return 1; fi # Resolve the user's home directory safely (avoid eval) local user_home user_home=$(getent passwd "${target_user}" | cut -d: -f6) if [[ -z "${user_home}" ]]; then echo -e "${RED}Error: Could not determine home directory for '${target_user}'.${NC}"; return 1; fi local ssh_dir="${user_home}/.ssh" local auth_keys_file="${ssh_dir}/authorized_keys" echo "Please paste the public SSH key (e.g., from id_rsa.pub) and press [Enter]:" read -rp "Public Key: " public_key if ! [[ "$public_key" =~ ^(ssh-(rsa|ed25519|dss|ecdsa)|sk-ssh-ed25519@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com) ]]; then echo -e "${RED}Error: Invalid public key format. It should start with 'ssh-rsa', 'ssh-ed25519', 'sk-ssh-ed25519@openssh.com', etc.${NC}"; return 1; fi echo "Creating directory ${ssh_dir} if it doesn't exist..." mkdir -p "${ssh_dir}" echo "Adding key to ${auth_keys_file}..." echo "${public_key}" >> "${auth_keys_file}" echo "Setting secure permissions..." chmod 700 "${ssh_dir}" chmod 600 "${auth_keys_file}" # Ensure correct ownership, especially if run as root chown -R "${target_user}":"${target_user}" "${ssh_dir}" echo -e "${GREEN}--- SSH key added successfully for ${target_user} ---${NC}" } # --- Action: Add the current user to the docker group (no sudo required for docker) --- function add_current_user_to_docker_group() { echo -e "\n${GREEN}--- Add Current User to Docker Group ---${NC}" # This script runs as root; prefer the user who invoked sudo. local target_user if [[ -n "${SUDO_USER-}" && "${SUDO_USER}" != "root" ]]; then target_user="${SUDO_USER}" else # Fallback: try to infer from login; otherwise ask. target_user=$(logname 2>/dev/null || true) if [[ -z "${target_user}" || "${target_user}" == "root" ]]; then read -rp "Enter the username to add to the docker group: " target_user fi fi if ! id "${target_user}" &>/dev/null; then echo -e "${RED}Error: User '${target_user}' does not exist.${NC}"; return 1; fi if ! getent group docker >/dev/null; then echo -e "${RED}Error: 'docker' group does not exist.${NC}" echo -e "${YELLOW}Install Docker first (menu option: Install Docker) and then try again.${NC}" return 1 fi if id -nG "${target_user}" | grep -qw docker; then echo -e "${YELLOW}User '${target_user}' is already in the 'docker' group. Nothing to do.${NC}" return 0 fi echo "Adding '${target_user}' to the 'docker' group..." usermod -aG docker "${target_user}" echo -e "${GREEN}Done.${NC}" echo -e "${YELLOW}Important:${NC} group membership updates require a new login session." echo "- Log out and log back in (recommended), OR" echo "- Run: newgrp docker" echo "Then you should be able to run: docker ps (without sudo)" } # --- Action: Disable SSH Password Login --- function disable_ssh_password_login() { echo -e "\n${YELLOW}--- WARNING: HIGHLY DESTRUCTIVE ACTION ---${NC}" echo "This action will disable all password-based SSH logins." echo "Ensure you have successfully logged in with a working SSH key BEFORE proceeding." echo -e "${RED}If you proceed without a key, YOU WILL BE PERMANENTLY LOCKED OUT of your server.${NC}" read -rp "Are you absolutely sure you want to continue? [y/N]: " confirm if [[ ! "$confirm" =~ ^[Yy]$ ]]; then echo "Operation cancelled." return fi local sshd_config_file="/etc/ssh/sshd_config" local backup_file="${sshd_config_file}.bak.$(date +%F-%T)" echo "Backing up ${sshd_config_file} to ${backup_file}..." cp "${sshd_config_file}" "${backup_file}" echo "Disabling password authentication..." # This sed command finds the line (even if commented out) and replaces it. if grep -qE "^#?\s*PasswordAuthentication" "${sshd_config_file}"; then sed -i -E 's/^#?\s*PasswordAuthentication\s+.*/PasswordAuthentication no/' "${sshd_config_file}" else # If the line doesn't exist at all, append it. echo "" >> "${sshd_config_file}" echo "PasswordAuthentication no" >> "${sshd_config_file}" fi echo "Validating SSH configuration syntax..." if sshd -t; then echo -e "${GREEN}Syntax OK.${NC}" else echo -e "${RED}SSH configuration syntax error detected!${NC}" echo "Restoring backup file to prevent SSH lockout." cp "${backup_file}" "${sshd_config_file}" echo "Operation aborted. SSH service was not restarted." return 1 fi echo "Restarting SSH service to apply changes..." systemctl restart sshd echo -e "${GREEN}--- Password authentication for SSH has been disabled successfully ---${NC}" } # --- Action: Install Docker --- function install_docker() { echo -e "\n${GREEN}--- Starting Docker Installation ---${NC}" # Check if Docker is already installed and running if command -v docker &> /dev/null && docker info &> /dev/null; then echo -e "${YELLOW}Docker is already installed and running.${NC}" echo "Current Docker version: $(docker --version)" read -rp "Do you want to re-run the installation anyway? [y/N]: " reinstall if [[ ! "$reinstall" =~ ^[Yy]$ ]]; then echo "Skipping Docker installation." return 0 fi fi # 1. Remove conflicting packages echo "Removing any conflicting old Docker packages..." local packages_to_remove=() for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do if dpkg -l | grep -q -w "$pkg"; then packages_to_remove+=("$pkg") fi done if [ ${#packages_to_remove[@]} -gt 0 ]; then echo "Removing installed conflicting packages: ${packages_to_remove[*]}" DEBIAN_FRONTEND=noninteractive apt-get remove -y "${packages_to_remove[@]}" else echo "No conflicting packages found." fi DEBIAN_FRONTEND=noninteractive apt-get autoremove -y # 2. Setup Docker's APT Repository echo "Setting up Docker's APT repository..." DEBIAN_FRONTEND=noninteractive apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl install -m 0755 -d /etc/apt/keyrings # Use -f to overwrite if file exists, ensures idempotency curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources, overwriting if it exists echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ tee /etc/apt/sources.list.d/docker.list > /dev/null DEBIAN_FRONTEND=noninteractive apt-get update -y # 3. Install Docker Engine echo "Installing Docker packages..." DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 4. Verify installation and start service echo "Verifying Docker service status..." if ! systemctl is-active --quiet docker; then echo "Docker service is not running. Starting and enabling it now..." systemctl start docker systemctl enable docker else echo "Docker service is already running." fi echo "Running post-installation test..." if docker run --rm hello-world &> /dev/null; then echo -e "${GREEN}Docker post-installation test passed successfully.${NC}" else echo -e "${YELLOW}Warning: Docker post-installation test failed. You may need to troubleshoot.${NC}" fi echo "Current Docker status:" systemctl status docker --no-pager echo -e "${GREEN}--- Docker Installation Complete ---${NC}" } # --- Action: Install Dockge --- function install_dockge() { echo -e "\n${GREEN}--- Starting Dockge Installation ---${NC}" # Prerequisite check: Ensure Docker is installed and running if ! command -v docker &> /dev/null; then echo -e "${RED}Error: Docker is not installed. Please install Docker first (Option 4).${NC}" return 1 fi if ! docker info &> /dev/null; then echo -e "${RED}Error: Docker daemon is not running. Please start Docker.${NC}" return 1 fi echo "Creating directories for stacks and Dockge configuration..." mkdir -p /opt/stacks /opt/dockge echo "Downloading Dockge compose.yaml to /opt/dockge/..." # Using full path for output is safer than using 'cd' curl -fsSL https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output /opt/dockge/compose.yaml echo "Starting Dockge server via Docker Compose..." # Use -f to specify the compose file path explicitly docker compose -f /opt/dockge/compose.yaml up -d echo -e "\nDockge has been started successfully." echo -e "You should be able to access it at: ${YELLOW}http://:5001${NC}" echo -e "${GREEN}--- Dockge Installation Complete ---${NC}" } # --- Action: Update Dockge --- function update_dockge() { echo -e "\n${GREEN}--- Updating Dockge ---${NC}" local DOCKGE_DIR="/opt/dockge" local DOCKGE_COMPOSE_FILE="${DOCKGE_DIR}/compose.yaml" # Prerequisite check if ! command -v docker &> /dev/null || ! docker info &> /dev/null; then echo -e "${RED}Error: Docker is not installed or running.${NC}"; return 1; fi if [ ! -f "${DOCKGE_COMPOSE_FILE}" ]; then echo -e "${RED}Error: Dockge compose file not found at '${DOCKGE_COMPOSE_FILE}'. Is Dockge installed?${NC}"; return 1; fi echo "Pulling the latest Dockge image..." # Use --project-directory for safety; it avoids 'cd' docker compose --project-directory "${DOCKGE_DIR}" pull echo "Recreating the Dockge container with the new image..." docker compose --project-directory "${DOCKGE_DIR}" up -d echo -e "${GREEN}--- Dockge update complete ---${NC}" echo -e "${YELLOW}Note: Old images may still exist. You can clean them up using the 'Docker System Prune' option.${NC}" } # --- Action: Docker System Prune --- function docker_system_prune() { echo -e "\n${GREEN}--- Docker System Prune ---${NC}" if ! command -v docker &> /dev/null || ! docker info &> /dev/null; then echo -e "${RED}Error: Docker is not installed or running.${NC}"; return 1; fi echo -e "${YELLOW}This will remove all unused Docker data, including:" echo " - All stopped containers" echo " - All networks not used by at least one container" echo " - All build cache" echo " - All dangling images (and optionally all unused images)" echo -e "${NC}" read -rp "Do you want to remove ALL unused images (not just dangling ones)? This is more thorough. [y/N]: " prune_all if [[ "$prune_all" =~ ^[Yy]$ ]]; then echo "Pruning all unused images and other data..." docker system prune -a -f else echo "Pruning dangling images and other data..." docker system prune -f fi echo -e "${GREEN}--- Docker prune complete ---${NC}" } # --- Action: Install Tailscale --- function install_tailscale() { echo -e "\n${GREEN}--- Starting Tailscale Installation ---${NC}" # Check if Tailscale is already installed if command -v tailscale &> /dev/null; then echo -e "${YELLOW}Tailscale is already installed. Proceeding to connect...${NC}" else local ubuntu_codename ubuntu_codename=$(lsb_release -cs 2>/dev/null || . /etc/os-release && echo "$VERSION_CODENAME") echo "Tailscale not found. Installing for Ubuntu (${ubuntu_codename})..." # 1. Add Tailscale's GPG key and repository echo "Adding Tailscale's repository..." curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/${ubuntu_codename}.noarmor.gpg" | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/${ubuntu_codename}.tailscale-keyring.list" | tee /etc/apt/sources.list.d/tailscale.list # 2. Install Tailscale echo "Updating package list and installing Tailscale..." DEBIAN_FRONTEND=noninteractive apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y tailscale echo -e "${GREEN}Tailscale package installed successfully.${NC}" fi # 3. Connect the machine to your Tailnet (interactive part) echo -e "\n${YELLOW}---> ACTION REQUIRED <---${NC}" echo "The command below will generate a URL to authenticate this server." echo "Please copy the URL, paste it into a browser, and log in." echo -e "${YELLOW}The script will automatically continue after successful authentication.${NC}\n" # Run the command and let IT handle the waiting. It will exit on its own # once authentication is complete. tailscale up # THE LINE BELOW WAS THE PROBLEM AND HAS BEEN REMOVED. # read -rp "After you have successfully authenticated in your browser, press [Enter] to continue..." # 4. By the time the script reaches here, authentication is complete. echo -e "\n${GREEN}Authentication successful!${NC}" echo "Fetching your Tailscale IP address..." TS_IP=$(tailscale ip -4) echo -e "Your Tailscale IPv4 address is: ${YELLOW}${TS_IP}${NC}" echo -e "${GREEN}--- Tailscale Setup Complete ---${NC}" } # --- Action: Update Maxmind DB --- function update_maxmind_db() { echo -e "\n${GREEN}--- Updating Maxmind GeoLite2 Country Database ---${NC}" local PANGOLIN_DIR="/home/dan/docker/pangolin" local DOWNLOAD_PATH="${PANGOLIN_DIR}/GeoLite2-Country.tar.gz" local DB_URL="https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz" local CONFIG_DIR="${PANGOLIN_DIR}/config" echo "Downloading the GeoLite2 Country database..." # Ensure the target directory exists mkdir -p "${PANGOLIN_DIR}" curl -L -o "${DOWNLOAD_PATH}" "${DB_URL}" echo "Extracting the database..." # Use -C to extract to a specific directory to avoid clutter tar -xzf "${DOWNLOAD_PATH}" -C "${PANGOLIN_DIR}" echo "Moving the .mmdb file to the config directory: ${CONFIG_DIR}" # Ensure the config directory exists mkdir -p "${CONFIG_DIR}" # Use find to safely locate the .mmdb file and move it find "${PANGOLIN_DIR}" -maxdepth 2 -name "*.mmdb" -type f -exec mv {} "${CONFIG_DIR}/" \; echo "Cleaning up downloaded and temporary files..." rm -f "${DOWNLOAD_PATH}" rm -rf "${PANGOLIN_DIR}"/GeoLite2-Country_* echo -e "${GREEN}--- Maxmind Database Update Complete ---${NC}" } # --- Action: Update Newt Service Manager --- function update_newt_service_manager() { echo -e "\n${GREEN}--- Updating Newt Service Manager ---${NC}" echo "Downloading the Newt Service Manager update script..." local temp_script temp_script=$(mktemp) curl -fsSL https://raw.githubusercontent.com/dpurnam/scripts/main/newt/newt-service-manager.sh -o "${temp_script}" echo "Executing the update script..." bash "${temp_script}" rm -f "${temp_script}" echo -e "${GREEN}--- Newt Service Manager update process finished ---${NC}" } # --- Helper function for updating a single Docker Compose application --- function perform_compose_update() { local app_dir="$1" local app_name app_name=$(basename "${app_dir}") echo -e "\n${GREEN}--- Updating application: ${app_name} ---${NC}" echo "Directory: ${app_dir}" echo "Pulling latest images..." docker compose --project-directory "${app_dir}" pull echo "Recreating containers with new images..." # --remove-orphans cleans up containers for services that no longer exist docker compose --project-directory "${app_dir}" up -d --remove-orphans echo -e "${GREEN}--- Update for ${app_name} complete ---${NC}" } # --- Action: Update Docker Applications --- function update_docker_apps() { echo -e "\n${GREEN}--- Update Docker Compose Applications ---${NC}" # Prerequisite check if ! command -v docker &> /dev/null || ! docker info &> /dev/null; then echo -e "${RED}Error: Docker is not installed or running. Please use Option 4 first.${NC}"; return 1; fi echo "Searching for running Docker Compose applications..." # Find all unique directories of running containers managed by docker-compose # This works by inspecting each running container for the 'com.docker.compose.project.working_dir' label local compose_dirs local container_ids container_ids=$(docker ps -q) if [[ -z "$container_ids" ]]; then echo -e "${YELLOW}No running Docker Compose applications found.${NC}" return fi mapfile -t compose_dirs < <(echo "$container_ids" | xargs docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' 2>/dev/null | grep -v '^$' | sort -u) if [ ${#compose_dirs[@]} -eq 0 ]; then echo -e "${YELLOW}No running Docker Compose applications found.${NC}" return fi echo "Found the following applications:" local options=("Update All" "${compose_dirs[@]}" "Cancel") # Present a dynamic menu to the user select opt in "${options[@]}"; do case $opt in "Update All") for dir in "${compose_dirs[@]}"; do perform_compose_update "${dir}" done break ;; "Cancel") echo "Update cancelled." break ;; *) if [[ -n "$opt" ]]; then perform_compose_update "${opt}" else echo -e "${RED}Invalid selection. Please try again.${NC}" fi break ;; esac done } # --- Action: Run all actions --- function run_all_actions() { echo -e "\n${YELLOW}===== RUNNING ALL ACTIONS =====${NC}" update_system setup_unattended_upgrades install_docker # Note: Application installs are not included in "Run All" by default. echo -e "\n${YELLOW}===== ALL ACTIONS COMPLETED =====${NC}" } # ============================================================================== # --- MENU AND SCRIPT LOGIC --- # ============================================================================== function main_menu() { while true; do # Clear the screen for a clean menu display clear echo "========================================" echo " Ubuntu Server Management" echo "========================================" echo -e " --- System & Maintenance ---" echo -e "${YELLOW}1) Run All Actions (Update, Unattended Upgrades, Docker)${NC}" echo "----------------------------------------" echo " 2) Update the System" echo " 3) Setup Unattended Upgrades" echo "" echo " --- User & Security Management ---" echo " 4) Setup a New User" echo " 5) Add SSH Public Key for a User" echo -e "${RED} 6) Disable SSH Password Login${NC}" echo "" echo " --- Docker Management ---" echo " 7) Install Docker" echo " 8) Add Current User to Docker Group (no sudo for docker)" echo " 9) Install Dockge" echo "10) Update Dockge" echo "11) Update Other Docker Apps (Compose)" echo "12) Docker System Prune (Cleanup)" echo "" echo " --- Network Tools ---" echo "13) Install/Connect Tailscale" echo "14) Update Maxmind DB" echo "" echo " --- Service Management ---" echo "15) Update Newt Service Manager" echo "----------------------------------------" echo -e "${RED} q) Quit${NC}" echo "========================================" read -rp "Enter your choice: " choice case $choice in 1) run_all_actions ;; 2) update_system ;; 3) setup_unattended_upgrades ;; 4) setup_new_user ;; 5) add_ssh_key ;; 6) disable_ssh_password_login ;; 7) install_docker ;; 8) add_current_user_to_docker_group ;; 9) install_dockge ;; 10) update_dockge ;; 11) update_docker_apps ;; 12) docker_system_prune ;; 13) install_tailscale ;; 14) update_maxmind_db ;; 15) update_newt_service_manager ;; q|Q) break ;; *) echo -e "\n${RED}Invalid option. Please try again.${NC}" ;; esac # Pause and wait for the user to press Enter before showing the menu again echo "" read -rp "Press [Enter] to return to the menu..." done } # --- Script Entry Point --- main_menu echo -e "\n${GREEN}Exiting script. Goodbye!${NC}\n" exit 0