Skip to content

Instantly share code, notes, and snippets.

@mustafakibar
Last active June 7, 2026 20:54
Show Gist options
  • Select an option

  • Save mustafakibar/617911ac52f39e18d54f7c70b1beb27f to your computer and use it in GitHub Desktop.

Select an option

Save mustafakibar/617911ac52f39e18d54f7c70b1beb27f to your computer and use it in GitHub Desktop.
#!/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