Last active
June 7, 2026 20:54
-
-
Save mustafakibar/617911ac52f39e18d54f7c70b1beb27f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ========================================================== | |
| # kibar.pro - Advanced Modular VDS Provisioning Script | |
| # Version: 1.2 | |
| # Contact: vdsscript@kibar.pro | |
| # Compatible with: Debian & Ubuntu | |
| # Tuning profile: Redis (cache-heavy, high-RAM) + S3-style | |
| # object storage (MinIO / RustFS) | |
| # ========================================================== | |
| set -o pipefail | |
| # ========================================================== | |
| # 1. CENTRAL CONFIGURATION | |
| # All static / editable settings live here. | |
| # ========================================================== | |
| SCRIPT_VERSION="1.2" | |
| # --- Locale & PATH (kept here so everything is in one place) --- | |
| LOCALE="C.UTF-8" | |
| export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" | |
| export LC_ALL="$LOCALE" | |
| export LANG="$LOCALE" | |
| export LANGUAGE="$LOCALE" | |
| # --- Apt non-interactive behaviour --- | |
| export DEBIAN_FRONTEND=noninteractive | |
| export NEEDRESTART_MODE=a | |
| export NEEDRESTART_SUSPEND=1 | |
| # --- Identity / system --- | |
| NEW_USER="kibar" | |
| TIMEZONE="Europe/Istanbul" | |
| SERVER_IP="" # auto-detected at runtime | |
| HOSTNAME="" # asked at runtime | |
| SSH_PORT="22" # default; can be overridden at runtime | |
| # --- Resource / tuning knobs (Redis + object-storage profile) --- | |
| SWAP_SIZE="2G" # swap file size (kept small; only an OOM safety net) | |
| SWAPPINESS="1" # cache server: avoid swapping RAM out, but keep swap as emergency | |
| VFS_CACHE_PRESSURE="50" # keep inode/dentry cache longer (good for many small objects) | |
| DIRTY_BG_BYTES="268435456" # 256 MB - start background flush | |
| DIRTY_BYTES="1073741824" # 1 GB - hard cap on dirty pages (prevents multi-GB flush stalls on high RAM) | |
| NOFILE_LIMIT="1048576" # max open files (many Redis clients + MinIO objects) | |
| NPROC_LIMIT="65535" | |
| TCP_BUFFER_MAX="16777216" # 16 MB max TCP socket buffer (large S3 object transfers) | |
| # --- Base packages --- | |
| # procps -> sysctl, free, top | |
| # gnupg -> dearmor eza apt key (missing on minimal images) | |
| # ca-certificates -> https repos | |
| BASE_PACKAGES=( | |
| "procps" "gnupg" "ca-certificates" "curl" "wget" "git" "ufw" "fail2ban" | |
| "unattended-upgrades" "apt-listchanges" "build-essential" "libssl-dev" | |
| "pkg-config" "cmake" "net-tools" "bat" "fd-find" | |
| ) | |
| # ========================================================== | |
| # 2. HELPER FUNCTIONS | |
| # ========================================================== | |
| log_info() { echo -e "\n\e[1;34m[INFO]\e[0m \e[1;37m$1\e[0m"; } | |
| log_success() { echo -e "\e[1;32m[SUCCESS] $1\e[0m"; } | |
| log_warning() { echo -e "\e[1;33m[SKIPPED] $1\e[0m"; } | |
| log_error() { echo -e "\e[1;31m[ERROR] $1\e[0m"; exit 1; } | |
| # STEP_FORCED is set by ask() on each call. | |
| # y → run step, honour "already done" guards (STEP_FORCED=0) | |
| # n → skip step entirely (STEP_FORCED=0, returns 1) | |
| # f → force step, bypass "already done" guards (STEP_FORCED=1) | |
| STEP_FORCED=0 | |
| ask() { | |
| local prompt="$1" | |
| local default="${2:-Y}" | |
| local reply | |
| read -p "$(echo -e "\e[1;36m[?] $prompt [y/n/f] (Default: $default):\e[0m ")" reply | |
| reply="${reply:-$default}" | |
| case "$reply" in | |
| [Yy]* ) STEP_FORCED=0; return 0 ;; | |
| [Ff]* ) STEP_FORCED=1; return 0 ;; | |
| [Nn]* ) STEP_FORCED=0; return 1 ;; | |
| * ) STEP_FORCED=0; return 0 ;; | |
| esac | |
| } | |
| ask_value() { | |
| local prompt="$1" | |
| local default="$2" | |
| local reply | |
| read -p "$(echo -e "\e[1;36m[?] $prompt\e[0m ")" reply | |
| echo "${reply:-$default}" | |
| } | |
| is_installed() { | |
| dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q "ok installed" | |
| } | |
| detect_ip() { | |
| local ip | |
| ip=$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -n1) | |
| [ -z "$ip" ] && ip=$(hostname -I 2>/dev/null | awk '{print $1}') | |
| echo "$ip" | |
| } | |
| echo "==========================================================" | |
| echo " kibar.pro - Advanced Modular VDS Provisioning Script v${SCRIPT_VERSION}" | |
| echo "==========================================================" | |
| echo " Prompt options: y = yes (skip if done) n = no/skip" | |
| echo " f = force (re-run even if already done)" | |
| echo "==========================================================" | |
| if [ "$EUID" -ne 0 ]; then | |
| log_error "Please run this script as root." | |
| fi | |
| # ========================================================== | |
| # 3. OS DETECTION (Debian / Ubuntu) | |
| # ========================================================== | |
| if [ -r /etc/os-release ]; then | |
| . /etc/os-release | |
| else | |
| log_error "/etc/os-release not found. Unsupported system." | |
| fi | |
| OS_ID="${ID:-unknown}" | |
| OS_CODENAME="${VERSION_CODENAME:-}" | |
| case "$OS_ID" in | |
| ubuntu|debian) | |
| log_info "Detected OS: $PRETTY_NAME (id=$OS_ID, codename=${OS_CODENAME:-unknown})" | |
| ;; | |
| *) | |
| log_warning "Detected OS '$OS_ID' is not Ubuntu/Debian. Distro-specific repos (Docker etc.) may fail." | |
| ;; | |
| esac | |
| # ========================================================== | |
| # 4. INTERACTIVE PRE-CONFIG | |
| # ========================================================== | |
| SERVER_IP="$(detect_ip)" | |
| if [ -n "$SERVER_IP" ]; then | |
| log_success "Auto-detected server IP: $SERVER_IP" | |
| else | |
| log_warning "Could not auto-detect an IP address." | |
| fi | |
| CURRENT_HOSTNAME="$(hostname)" | |
| HOSTNAME="$(ask_value "Enter desired hostname (leave EMPTY to keep current '$CURRENT_HOSTNAME'):" "$CURRENT_HOSTNAME")" | |
| if [ "$HOSTNAME" = "$CURRENT_HOSTNAME" ]; then | |
| log_warning "Current hostname '$CURRENT_HOSTNAME' will be kept." | |
| else | |
| log_info "Hostname will be set to: $HOSTNAME" | |
| fi | |
| SSH_PORT="$(ask_value "Enter desired SSH Port (Default: 22):" "22")" | |
| log_info "SSH Port will be configured as: $SSH_PORT" | |
| # ========================================================== | |
| # 5. ROOT PASSWORD MANAGEMENT | |
| # ========================================================== | |
| if ask "Do you want to change the 'root' user password?"; then | |
| log_info "Changing password for 'root'..." | |
| passwd root | |
| log_success "Root password updated." | |
| fi | |
| # ========================================================== | |
| # 6. BASE SYSTEM & PACKAGES | |
| # ========================================================== | |
| if ask "Do you want to update/upgrade the system and install base packages?"; then | |
| log_info "Updating package lists..." | |
| apt-get update -y | |
| log_info "Upgrading installed packages (non-interactive)..." | |
| apt-get upgrade -y | |
| log_info "Checking and installing base packages individually..." | |
| for pkg in "${BASE_PACKAGES[@]}"; do | |
| if [ "$STEP_FORCED" != "1" ] && is_installed "$pkg"; then | |
| log_warning "[$pkg] is already installed. Skipping." | |
| else | |
| log_info "Installing [$pkg]..." | |
| apt-get install -y "$pkg" | |
| if is_installed "$pkg"; then | |
| log_success "[$pkg] successfully installed." | |
| else | |
| log_error "Failed to install [$pkg]! Please check apt logs." | |
| fi | |
| fi | |
| done | |
| fi | |
| # ========================================================== | |
| # 7. SWAP CONFIGURATION | |
| # ========================================================== | |
| if ask "Do you want to setup a $SWAP_SIZE Swap file for stability?"; then | |
| if [ "$STEP_FORCED" != "1" ] && swapon --show | grep -q "/swapfile"; then | |
| log_warning "Swap file already exists." | |
| else | |
| if swapon --show | grep -q "/swapfile"; then | |
| log_info "Force mode: deactivating existing swap to recreate..." | |
| swapoff /swapfile && rm -f /swapfile \ | |
| || { log_warning "swapoff failed (in use?). Keeping existing swap."; } | |
| fi | |
| if ! swapon --show | grep -q "/swapfile"; then | |
| log_info "Creating $SWAP_SIZE swap file..." | |
| SWAP_MB=$(( ${SWAP_SIZE%G} * 1024 )) | |
| fallocate -l "$SWAP_SIZE" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_MB" | |
| chmod 600 /swapfile | |
| mkswap /swapfile | |
| swapon /swapfile | |
| if ! grep -q "/swapfile" /etc/fstab; then | |
| echo "/swapfile none swap sw 0 0" >> /etc/fstab | |
| fi | |
| log_success "$SWAP_SIZE Swap file created and activated." | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 8. HOSTNAME & TIMEZONE | |
| # ========================================================== | |
| if ask "Do you want to configure Hostname ('$HOSTNAME') and Timezone ($TIMEZONE)?"; then | |
| if [ "$STEP_FORCED" = "1" ] || [ "$(hostname)" != "$HOSTNAME" ]; then | |
| hostnamectl set-hostname "$HOSTNAME" | |
| log_success "Hostname updated to $HOSTNAME." | |
| else | |
| log_warning "Hostname is already '$HOSTNAME'." | |
| fi | |
| if ! grep -qE "^127\.0\.1\.1[[:space:]].*\b$HOSTNAME\b" /etc/hosts; then | |
| echo "127.0.1.1 $HOSTNAME" >> /etc/hosts | |
| log_success "Added '$HOSTNAME' to /etc/hosts." | |
| fi | |
| CURRENT_TZ="$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null)" | |
| if [ "$STEP_FORCED" = "1" ] || [ "$CURRENT_TZ" != "$TIMEZONE" ]; then | |
| timedatectl set-timezone "$TIMEZONE" | |
| log_success "Timezone updated to $TIMEZONE." | |
| else | |
| log_warning "Timezone is already $TIMEZONE." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 9. EZA (Modern ls) | |
| # ========================================================== | |
| if ask "Do you want to check/install 'eza' (modern ls)?"; then | |
| if [ "$STEP_FORCED" != "1" ] && { is_installed "eza" || command -v eza >/dev/null; }; then | |
| log_warning "eza is already installed." | |
| else | |
| log_info "Installing eza..." | |
| command -v gpg >/dev/null || apt-get install -y gnupg | |
| mkdir -p /etc/apt/keyrings | |
| wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc | gpg --dearmor -o /etc/apt/keyrings/gierens.gpg | |
| chmod a+r /etc/apt/keyrings/gierens.gpg | |
| echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" | tee /etc/apt/sources.list.d/gierens.list >/dev/null | |
| apt-get update && apt-get install -y --reinstall eza | |
| if command -v eza >/dev/null; then | |
| log_success "eza installed." | |
| else | |
| log_warning "eza installation could not be verified." | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 10. USER MANAGEMENT & SSH KEYS | |
| # ========================================================== | |
| if ask "Do you want to create/manage the '$NEW_USER' user?"; then | |
| if [ "$STEP_FORCED" != "1" ] && id "$NEW_USER" &>/dev/null; then | |
| log_warning "User '$NEW_USER' already exists." | |
| else | |
| log_info "Creating user '$NEW_USER'..." | |
| useradd -m -s /bin/bash "$NEW_USER" 2>/dev/null || true | |
| usermod -aG sudo "$NEW_USER" | |
| echo "$NEW_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/"$NEW_USER" | |
| chmod 0440 /etc/sudoers.d/"$NEW_USER" | |
| log_success "User '$NEW_USER' created and granted sudo privileges." | |
| fi | |
| if ask "Do you want to set/change the password for '$NEW_USER'?"; then | |
| log_info "Changing password for '$NEW_USER'..." | |
| passwd "$NEW_USER" | |
| log_success "Password for '$NEW_USER' updated." | |
| fi | |
| if ask "Do you want to migrate root's SSH keys to '$NEW_USER'?"; then | |
| if [ "$STEP_FORCED" != "1" ] && [ -f /home/"$NEW_USER"/.ssh/authorized_keys ]; then | |
| log_warning "Keys already migrated (use f to overwrite)." | |
| elif [ -f /root/.ssh/authorized_keys ]; then | |
| log_info "Migrating SSH keys..." | |
| mkdir -p /home/"$NEW_USER"/.ssh | |
| cp /root/.ssh/authorized_keys /home/"$NEW_USER"/.ssh/authorized_keys | |
| chown -R "$NEW_USER":"$NEW_USER" /home/"$NEW_USER"/.ssh | |
| chmod 700 /home/"$NEW_USER"/.ssh | |
| chmod 600 /home/"$NEW_USER"/.ssh/authorized_keys | |
| log_success "SSH keys migrated successfully." | |
| else | |
| log_warning "/root/.ssh/authorized_keys not found." | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 11. SSH CONFIGURATION HARDENING | |
| # ========================================================== | |
| if ask "Do you want to apply SSH hardening (disable root login, disable passwords, set custom port)?"; then | |
| SSHD_MARKER="# === kibar.pro hardening (must stay at top; first match wins) ===" | |
| if [ "$STEP_FORCED" != "1" ] && grep -qF "$SSHD_MARKER" /etc/ssh/sshd_config; then | |
| log_warning "SSH is already hardened (marker found). Use f to re-apply." | |
| else | |
| # Force mode: strip the previous hardening block before re-prepending. | |
| if grep -qF "$SSHD_MARKER" /etc/ssh/sshd_config; then | |
| log_info "Force mode: removing previous hardening block from sshd_config..." | |
| sed -i '/^# === kibar\.pro hardening/,/^# === end hardening ===/d' /etc/ssh/sshd_config | |
| fi | |
| log_info "Applying SSH hardening rules (Port: $SSH_PORT)..." | |
| BACKUP="/etc/ssh/sshd_config.bak.$(date +%s)" | |
| cp /etc/ssh/sshd_config "$BACKUP" | |
| TMP_SSHD="$(mktemp)" | |
| cat > "$TMP_SSHD" <<SSHD_HARDEN | |
| $SSHD_MARKER | |
| Port $SSH_PORT | |
| PermitRootLogin no | |
| PasswordAuthentication no | |
| PubkeyAuthentication yes | |
| PermitEmptyPasswords no | |
| KbdInteractiveAuthentication no | |
| # === end hardening === | |
| SSHD_HARDEN | |
| cat /etc/ssh/sshd_config >> "$TMP_SSHD" | |
| mv "$TMP_SSHD" /etc/ssh/sshd_config | |
| chmod 644 /etc/ssh/sshd_config | |
| SSHD_BIN="$(command -v sshd || echo /usr/sbin/sshd)" | |
| if "$SSHD_BIN" -t 2>/tmp/sshd_test_err; then | |
| log_success "SSH rules applied & config validated." | |
| else | |
| log_warning "sshd config test FAILED. Restoring backup." | |
| cp "$BACKUP" /etc/ssh/sshd_config | |
| cat /tmp/sshd_test_err | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 12. TAILSCALE | |
| # ========================================================== | |
| if ask "Do you want to install Tailscale?"; then | |
| if [ "$STEP_FORCED" != "1" ] && command -v tailscale >/dev/null; then | |
| log_warning "Tailscale is already installed." | |
| else | |
| log_info "Installing Tailscale..." | |
| curl -fsSL https://tailscale.com/install.sh | sh | |
| log_success "Tailscale installed. Remember to run 'sudo tailscale up' later." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 13. UFW FIREWALL | |
| # ========================================================== | |
| if ask "Do you want to configure the UFW Firewall?"; then | |
| if is_installed "ufw"; then | |
| if [ "$STEP_FORCED" != "1" ] && ufw status | grep -q "Status: active"; then | |
| log_warning "UFW is already active and configured. Use f to reset and reconfigure." | |
| else | |
| if ufw status | grep -q "Status: active"; then | |
| log_info "Force mode: resetting all UFW rules..." | |
| ufw --force reset | |
| fi | |
| log_info "Configuring UFW..." | |
| ufw default deny incoming | |
| ufw default allow outgoing | |
| ufw allow $SSH_PORT/tcp comment 'Custom SSH Port' | |
| ufw allow 80/tcp comment 'Docker HTTP' | |
| ufw allow 443/tcp comment 'Docker HTTPS' | |
| ufw allow in on tailscale0 comment 'Full Access via Tailscale' | |
| if ufw --force enable; then | |
| log_success "UFW firewall is active." | |
| else | |
| log_error "UFW configuration failed!" | |
| fi | |
| fi | |
| else | |
| log_error "UFW is not installed! Check the base packages installation step." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 14. FAIL2BAN CONFIGURATION | |
| # ========================================================== | |
| if ask "Do you want to configure Fail2Ban?"; then | |
| if is_installed "fail2ban"; then | |
| if [ "$STEP_FORCED" != "1" ] && [ -f /etc/fail2ban/jail.local ] && grep -q "\[sshd\]" /etc/fail2ban/jail.local; then | |
| log_warning "Fail2Ban is already configured. Use f to overwrite." | |
| else | |
| log_info "Setting up Fail2Ban (Port: $SSH_PORT)..." | |
| if [ -f /var/log/auth.log ]; then | |
| SSH_BACKEND="auto" | |
| SSH_LOGPATH="logpath = /var/log/auth.log" | |
| else | |
| SSH_BACKEND="systemd" | |
| SSH_LOGPATH="" | |
| is_installed "python3-systemd" || apt-get install -y python3-systemd || true | |
| fi | |
| cat << FAIL2BAN > /etc/fail2ban/jail.local | |
| [DEFAULT] | |
| bantime = 1h | |
| findtime = 10m | |
| maxretry = 5 | |
| [sshd] | |
| enabled = true | |
| port = $SSH_PORT | |
| filter = sshd | |
| backend = $SSH_BACKEND | |
| $SSH_LOGPATH | |
| FAIL2BAN | |
| systemctl enable fail2ban | |
| if systemctl restart fail2ban; then | |
| log_success "Fail2Ban configured for SSH (backend: $SSH_BACKEND)." | |
| else | |
| log_warning "Fail2Ban restart failed. Check 'journalctl -u fail2ban'." | |
| fi | |
| fi | |
| else | |
| log_error "Fail2Ban is not installed! Check the base packages installation step." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 15. KERNEL & SYSTEM TUNING | |
| # Profile: Redis (cache-heavy, high RAM) + S3-style | |
| # object storage (MinIO / RustFS). | |
| # Notes: | |
| # * vm.overcommit_memory=1 and THP=never are REQUIRED by | |
| # Redis (background save fork + latency); both are global | |
| # kernel settings, so containers inherit them. | |
| # * net.core.somaxconn / tcp-backlog are network-namespaced; | |
| # for a Redis CONTAINER set them via the compose/service | |
| # 'sysctls:' block (host value below covers host-net services). | |
| # ========================================================== | |
| if ask "Do you want to apply Kernel Tuning (Redis + Object Storage profile + TCP BBR)?"; then | |
| if [ "$STEP_FORCED" != "1" ] && [ -f /etc/sysctl.d/99-custom-performance.conf ]; then | |
| log_warning "Kernel tuning already applied. Use f to re-apply." | |
| else | |
| if [ -f /etc/sysctl.d/99-custom-performance.conf ]; then | |
| log_info "Force mode: removing existing tuning config for re-apply..." | |
| rm -f /etc/sysctl.d/99-custom-performance.conf | |
| fi | |
| log_info "Loading tcp_bbr module..." | |
| echo "tcp_bbr" > /etc/modules-load.d/bbr.conf | |
| modprobe tcp_bbr 2>/dev/null || true | |
| log_info "Writing sysctl tuning file..." | |
| # Unquoted heredoc so the central tuning variables expand. | |
| cat << SYSCTL > /etc/sysctl.d/99-custom-performance.conf | |
| # ---------- Memory / Redis ---------- | |
| vm.overcommit_memory = 1 | |
| vm.swappiness = $SWAPPINESS | |
| vm.vfs_cache_pressure = $VFS_CACHE_PRESSURE | |
| # Cap dirty pages by BYTES (not %): on high-RAM boxes the default | |
| # percentage buffers many GB and causes long flush stalls. | |
| vm.dirty_background_bytes = $DIRTY_BG_BYTES | |
| vm.dirty_bytes = $DIRTY_BYTES | |
| # ---------- File handles (many objects / clients) ---------- | |
| fs.file-max = 2097152 | |
| fs.nr_open = 2097152 | |
| # ---------- Network core ---------- | |
| net.core.somaxconn = 65535 | |
| net.core.netdev_max_backlog = 16384 | |
| net.core.rmem_max = $TCP_BUFFER_MAX | |
| net.core.wmem_max = $TCP_BUFFER_MAX | |
| net.core.default_qdisc = fq | |
| # ---------- TCP ---------- | |
| net.ipv4.tcp_max_syn_backlog = 65535 | |
| net.ipv4.tcp_congestion_control = bbr | |
| net.ipv4.tcp_tw_reuse = 1 | |
| net.ipv4.tcp_fin_timeout = 15 | |
| net.ipv4.tcp_keepalive_time = 600 | |
| net.ipv4.tcp_slow_start_after_idle = 0 | |
| net.ipv4.tcp_mtu_probing = 1 | |
| net.ipv4.tcp_rmem = 4096 87380 $TCP_BUFFER_MAX | |
| net.ipv4.tcp_wmem = 4096 65536 $TCP_BUFFER_MAX | |
| net.ipv4.ip_local_port_range = 10240 65535 | |
| SYSCTL | |
| sysctl -p /etc/sysctl.d/99-custom-performance.conf >/dev/null \ | |
| || log_warning "Some sysctl keys could not be applied (check kernel support)." | |
| log_info "Writing PAM resource limits (affects SSH login shells)..." | |
| cat << LIMITS > /etc/security/limits.d/custom-limits.conf | |
| * soft nofile $NOFILE_LIMIT | |
| * hard nofile $NOFILE_LIMIT | |
| root soft nofile $NOFILE_LIMIT | |
| root hard nofile $NOFILE_LIMIT | |
| * soft nproc $NPROC_LIMIT | |
| * hard nproc $NPROC_LIMIT | |
| LIMITS | |
| # PAM limits DO NOT apply to systemd services; set the systemd | |
| # manager default too (Redis/MinIO usually run as services/containers). | |
| log_info "Raising systemd DefaultLimitNOFILE..." | |
| mkdir -p /etc/systemd/system.conf.d /etc/systemd/user.conf.d | |
| printf '[Manager]\nDefaultLimitNOFILE=%s\n' "$NOFILE_LIMIT" > /etc/systemd/system.conf.d/limits.conf | |
| printf '[Manager]\nDefaultLimitNOFILE=%s\n' "$NOFILE_LIMIT" > /etc/systemd/user.conf.d/limits.conf | |
| systemctl daemon-reexec || true | |
| # Disable Transparent Huge Pages (Redis requirement) now + at boot. | |
| log_info "Disabling Transparent Huge Pages (THP) for Redis..." | |
| echo never > /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true | |
| echo never > /sys/kernel/mm/transparent_hugepage/defrag 2>/dev/null || true | |
| cat << 'THPUNIT' > /etc/systemd/system/disable-thp.service | |
| [Unit] | |
| Description=Disable Transparent Huge Pages (THP) for Redis | |
| DefaultDependencies=no | |
| After=sysinit.target local-fs.target | |
| [Service] | |
| Type=oneshot | |
| ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled; echo never > /sys/kernel/mm/transparent_hugepage/defrag' | |
| [Install] | |
| WantedBy=basic.target | |
| THPUNIT | |
| systemctl daemon-reload | |
| systemctl enable disable-thp.service >/dev/null 2>&1 || true | |
| log_success "Kernel & system tuning applied." | |
| # ---------- VERIFICATION ---------- | |
| log_info "Verifying applied tunings (live runtime values):" | |
| printf " %-28s %s\n" "vm.overcommit_memory" "$(sysctl -n vm.overcommit_memory 2>/dev/null)" | |
| printf " %-28s %s\n" "vm.swappiness" "$(sysctl -n vm.swappiness 2>/dev/null)" | |
| printf " %-28s %s\n" "vm.vfs_cache_pressure" "$(sysctl -n vm.vfs_cache_pressure 2>/dev/null)" | |
| printf " %-28s %s\n" "vm.dirty_bytes" "$(sysctl -n vm.dirty_bytes 2>/dev/null)" | |
| printf " %-28s %s\n" "fs.file-max" "$(sysctl -n fs.file-max 2>/dev/null)" | |
| printf " %-28s %s\n" "net.core.somaxconn" "$(sysctl -n net.core.somaxconn 2>/dev/null)" | |
| printf " %-28s %s\n" "net.core.rmem_max" "$(sysctl -n net.core.rmem_max 2>/dev/null)" | |
| printf " %-28s %s\n" "default_qdisc" "$(sysctl -n net.core.default_qdisc 2>/dev/null)" | |
| printf " %-28s %s\n" "tcp_congestion_control" "$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null)" | |
| printf " %-28s %s\n" "available_congestion_control" "$(sysctl -n net.ipv4.tcp_available_congestion_control 2>/dev/null)" | |
| printf " %-28s %s\n" "THP enabled" "$(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || echo n/a)" | |
| # Sanity check: warn loudly if BBR did not actually take effect. | |
| if [ "$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null)" != "bbr" ]; then | |
| log_warning "TCP BBR is NOT active. Kernel may be too old or tcp_bbr module unavailable." | |
| else | |
| log_success "TCP BBR is active." | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 16. AUTOMATIC SECURITY UPDATES | |
| # ========================================================== | |
| if ask "Do you want to configure automatic security updates (unattended-upgrades)?"; then | |
| if [ "$STEP_FORCED" != "1" ] && [ -f /etc/apt/apt.conf.d/20auto-upgrades ] && grep -q "Unattended-Upgrade" /etc/apt/apt.conf.d/20auto-upgrades; then | |
| log_warning "Automatic updates already configured. Use f to overwrite." | |
| else | |
| log_info "Configuring unattended-upgrades..." | |
| cat << EOF > /etc/apt/apt.conf.d/20auto-upgrades | |
| APT::Periodic::Update-Package-Lists "1"; | |
| APT::Periodic::Download-Upgradeable-Packages "1"; | |
| APT::Periodic::AutocleanInterval "7"; | |
| APT::Periodic::Unattended-Upgrade "1"; | |
| EOF | |
| systemctl enable unattended-upgrades >/dev/null 2>&1 | |
| systemctl restart unattended-upgrades >/dev/null 2>&1 | |
| log_success "Automatic security updates enabled." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 17. DOCKER & DOCKER SWARM | |
| # ========================================================== | |
| if ask "Do you want to install Docker, configure Security/Performance & Init Swarm?"; then | |
| if command -v docker >/dev/null; then | |
| log_warning "Docker is already installed." | |
| else | |
| case "$OS_ID" in | |
| ubuntu|debian) : ;; | |
| *) log_error "Docker repo setup only supports Debian/Ubuntu. Detected: $OS_ID" ;; | |
| esac | |
| if [ -z "$OS_CODENAME" ]; then | |
| log_error "Could not determine OS codename. Cannot set up Docker repo." | |
| fi | |
| log_info "Installing Docker Engine for $OS_ID ($OS_CODENAME)..." | |
| install -m 0755 -d /etc/apt/keyrings | |
| curl -fsSL "https://download.docker.com/linux/$OS_ID/gpg" -o /etc/apt/keyrings/docker.asc | |
| chmod a+r /etc/apt/keyrings/docker.asc | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$OS_ID $OS_CODENAME stable" \ | |
| | tee /etc/apt/sources.list.d/docker.list > /dev/null | |
| apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| systemctl enable docker | |
| log_success "Docker installed." | |
| fi | |
| # Docker daemon security & performance configuration. | |
| # default-ulimits => Redis/MinIO containers get high open-file limits | |
| # without needing per-service ulimit overrides. | |
| if [ -d /etc/docker ] && { [ "$STEP_FORCED" = "1" ] || [ ! -f /etc/docker/daemon.json ]; }; then | |
| log_info "Applying Docker daemon performance & security settings..." | |
| cat << EOF > /etc/docker/daemon.json | |
| { | |
| "log-driver": "json-file", | |
| "log-opts": { | |
| "max-size": "50m", | |
| "max-file": "3" | |
| }, | |
| "live-restore": false, | |
| "icc": false, | |
| "max-concurrent-downloads": 10, | |
| "max-concurrent-uploads": 5, | |
| "default-ulimits": { | |
| "nofile": { | |
| "Name": "nofile", | |
| "Hard": $NOFILE_LIMIT, | |
| "Soft": $NOFILE_LIMIT | |
| } | |
| } | |
| } | |
| EOF | |
| systemctl restart docker | |
| log_success "Docker daemon.json configured (incl. nofile=$NOFILE_LIMIT for containers)." | |
| else | |
| systemctl start docker | |
| fi | |
| if id "$NEW_USER" &>/dev/null; then | |
| usermod -aG docker "$NEW_USER" | |
| log_success "User '$NEW_USER' added to 'docker' group." | |
| fi | |
| if [ "$STEP_FORCED" != "1" ] && docker info 2>/dev/null | grep -q "Swarm: active"; then | |
| log_warning "Docker Swarm is already active. Use f to leave and reinitialize." | |
| else | |
| if docker info 2>/dev/null | grep -q "Swarm: active"; then | |
| log_info "Force mode: leaving existing Swarm to reinitialize..." | |
| docker swarm leave --force || true | |
| fi | |
| SWARM_IP="$(ask_value "Docker Swarm advertise IP (press ENTER for detected '$SERVER_IP'):" "$SERVER_IP")" | |
| if [ -n "$SWARM_IP" ]; then | |
| log_info "Initializing Docker Swarm with advertise-addr $SWARM_IP..." | |
| docker swarm init --advertise-addr "$SWARM_IP" \ | |
| && log_success "Docker Swarm initialized." \ | |
| || log_warning "Docker Swarm init failed." | |
| fi | |
| fi | |
| fi | |
| # ========================================================== | |
| # 18. NEOVIM & RUST COMPONENTS | |
| # ========================================================== | |
| if ask "Do you want to install Neovim and Rust tools?"; then | |
| # --- Neovim install (FUSE-free, arch-aware; falls back to apt) --- | |
| if [ "$STEP_FORCED" != "1" ] && command -v nvim >/dev/null; then | |
| log_warning "Neovim is already installed. Use f to reinstall." | |
| else | |
| if command -v nvim >/dev/null; then | |
| log_info "Force mode: removing existing Neovim installation..." | |
| rm -f /usr/local/bin/nvim | |
| rm -rf /opt/nvim | |
| fi | |
| log_info "Installing Neovim..." | |
| NVIM_ARCH="$(dpkg --print-architecture)" | |
| case "$NVIM_ARCH" in | |
| amd64) NVIM_ASSET="nvim-linux-x86_64.tar.gz" ;; | |
| arm64) NVIM_ASSET="nvim-linux-arm64.tar.gz" ;; | |
| *) NVIM_ASSET="" ;; | |
| esac | |
| NVIM_OK=0 | |
| if [ -n "$NVIM_ASSET" ]; then | |
| NVIM_TMP="$(mktemp -d)" | |
| # curl -f fails on non-200, so a "Not Found" HTML page is never saved. | |
| if curl -fsSL -o "$NVIM_TMP/nvim.tar.gz" "https://github.com/neovim/neovim/releases/latest/download/$NVIM_ASSET" \ | |
| && tar -xzf "$NVIM_TMP/nvim.tar.gz" -C "$NVIM_TMP" 2>/dev/null; then | |
| rm -rf /opt/nvim | |
| mv "$NVIM_TMP"/nvim-linux-* /opt/nvim 2>/dev/null || true | |
| if [ -d /opt/nvim/bin ]; then | |
| ln -sf /opt/nvim/bin/nvim /usr/local/bin/nvim | |
| NVIM_OK=1 | |
| log_success "Neovim (pre-built) installed to /opt/nvim." | |
| fi | |
| fi | |
| rm -rf "$NVIM_TMP" | |
| fi | |
| if [ "$NVIM_OK" -ne 1 ]; then | |
| log_warning "Pre-built Neovim unavailable/failed; falling back to apt." | |
| rm -f /usr/local/bin/nvim | |
| apt-get install -y neovim && log_success "Neovim installed via apt." || log_warning "Neovim install failed." | |
| fi | |
| fi | |
| # --- Rust toolchain --- | |
| if [ "$STEP_FORCED" != "1" ] && [ -d /usr/local/rustup ]; then | |
| log_warning "Rust toolchain is already installed. Use f to reinstall/update." | |
| else | |
| log_info "Installing Rust toolchain..." | |
| export RUSTUP_HOME=/usr/local/rustup | |
| export CARGO_HOME=/usr/local/cargo | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path | |
| echo 'export RUSTUP_HOME=/usr/local/rustup' > /etc/profile.d/rust.sh | |
| echo 'export CARGO_HOME=/usr/local/cargo' >> /etc/profile.d/rust.sh | |
| echo 'export PATH=$PATH:/usr/local/cargo/bin' >> /etc/profile.d/rust.sh | |
| chmod 644 /etc/profile.d/rust.sh | |
| chmod -R 755 /usr/local/rustup /usr/local/cargo | |
| /usr/local/cargo/bin/rustup component add clippy rustfmt | |
| log_success "Rust installed." | |
| fi | |
| fi | |
| # ========================================================== | |
| # 19. BASH ALIASES | |
| # ========================================================== | |
| if ask "Do you want to inject custom terminal aliases?"; then | |
| ALIAS_MARKER="# --- CUSTOM KIBAR ALIASES ---" | |
| inject_aliases() { | |
| local target_file="$1" | |
| if [ "$STEP_FORCED" != "1" ] && grep -q "$ALIAS_MARKER" "$target_file" 2>/dev/null; then | |
| log_warning "Aliases already exist in $target_file. Use f to overwrite." | |
| else | |
| # Force mode: remove the existing alias block (from marker to EOF, | |
| # since aliases are always appended at the bottom of the file). | |
| if grep -q "$ALIAS_MARKER" "$target_file" 2>/dev/null; then | |
| log_info "Force mode: removing existing alias block from $target_file..." | |
| sed -i "/^# --- CUSTOM KIBAR ALIASES ---/,\$d" "$target_file" | |
| fi | |
| log_info "Injecting aliases into $target_file..." | |
| cat << 'ALIAS_BLOCK' >> "$target_file" | |
| # --- CUSTOM KIBAR ALIASES --- | |
| # --- SYSTEM --- | |
| alias upt='sudo apt update' | |
| alias upg='sudo -- sh -c "apt update && apt upgrade"' | |
| alias aptclean='sudo apt autoremove -y' | |
| alias c='clear' | |
| alias e='exit' | |
| alias q='exit' | |
| # --- POWER --- | |
| alias rb='sudo /sbin/reboot' | |
| alias po='sudo /sbin/poweroff' | |
| alias sd='sudo /sbin/shutdown' | |
| # --- NAVIGATION --- | |
| alias ..='cd ..' | |
| alias ...='cd ../../../' | |
| # --- MODERN REPLACEMENTS (RUST TOOLS) --- | |
| if command -v batcat &> /dev/null; then | |
| alias cat='batcat' | |
| elif command -v bat &> /dev/null; then | |
| alias cat='bat' | |
| fi | |
| if command -v fdfind &> /dev/null; then | |
| alias find='fdfind' | |
| elif command -v fd &> /dev/null; then | |
| alias find='fd' | |
| fi | |
| if command -v eza &> /dev/null; then | |
| alias ls='eza --icons=always' | |
| alias ll='eza --icons=always -l' | |
| alias la='eza --icons=always -la' | |
| alias ltree='eza --icons=always -l --tree --no-user --no-permissions --no-filesize --level=2' | |
| fi | |
| # --- STANDARD UTILITIES --- | |
| alias grep='grep --color=auto' | |
| alias mv='mv -i' | |
| alias cp='cp -i' | |
| alias rm='rm -I --preserve-root' | |
| # --- NETWORK & INFO --- | |
| alias ports='sudo netstat -tulanp' | |
| alias myip='curl ifconfig.me' | |
| alias openports='ss -tulwn' | |
| alias mem='free -h' | |
| alias freemem='watch -n 1 free -h' | |
| alias diskuse='df -Th | grep -Ev "tmpfs|loop"' | |
| # --- GIT SHORTCUTS --- | |
| alias g='git' | |
| alias gs='git status' | |
| alias ga='git add' | |
| alias gaa='git add .' | |
| alias gc='git commit -m' | |
| alias gp='git push' | |
| alias gl='git pull' | |
| alias gco='git checkout' | |
| ALIAS_BLOCK | |
| log_success "Aliases injected to $target_file." | |
| fi | |
| } | |
| inject_aliases "/root/.bashrc" | |
| if id "$NEW_USER" &>/dev/null; then | |
| inject_aliases "/home/$NEW_USER/.bashrc" | |
| chown "$NEW_USER":"$NEW_USER" "/home/$NEW_USER/.bashrc" | |
| fi | |
| fi | |
| echo -e "\n==========================================================" | |
| echo -e "\e[1;32m SETUP COMPLETE! \e[0m" | |
| echo "==========================================================" | |
| echo "Summary of manual steps you might need to take:" | |
| echo " 1. Run 'sudo tailscale up' to authenticate to Tailscale." | |
| echo " 2. IMPORTANT: Verify your SSH KEY login works BEFORE restarting SSH," | |
| echo " because password login is now disabled. Then run:" | |
| echo " sudo systemctl restart ssh" | |
| echo " 3. Disconnect and reconnect to activate the new shell aliases." | |
| echo " 4. Log out/in (or 'newgrp docker') for docker group membership to apply." | |
| echo " 5. Redis CONTAINER tip: set per-service sysctls in your compose/stack:" | |
| echo " sysctls: { net.core.somaxconn: 65535 }" | |
| echo " (somaxconn is network-namespaced; THP & overcommit are already global)." | |
| if [ "$SSH_PORT" != "22" ]; then | |
| echo " 6. Remember to connect via your custom SSH port: -p $SSH_PORT" | |
| fi | |
| echo "==========================================================" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment