Toggle the features you need. A ready-to-review Bash script updates live on the right.
System
Initial package update, hostname, and timezone
Create Sudo User
A non-root user with sudo privileges — required
SSH Hardening
Restrict SSH access to reduce attack surface
Default is 22
Security
Firewall, intrusion prevention, and automatic updates
Add ports to open (e.g. 8080/tcp, 5432/tcp)
System Tuning
Swap memory and kernel tweaks
Swap size
Baseline Utilities
curl, git, htop, tmux, jq, ripgrep, and more
Installs: curl wget git vim nano htop btop tmux screen build-essential jq ripgrep unzip zip net-tools dnsutils ncdu tree mtr
Node.js
Install via fnm (Fast Node Manager)
Version
Python
Install uv — the fast Python package and project manager
Docker
Install Docker Engine from the official repository
Reverse Proxy
Caddy auto-manages TLS; Nginx gives manual control
Choose reverse proxy
Tailscale
Zero-config VPN for secure private networking
Shell
Zsh, Oh My Zsh, autosuggestions, and zoxide
ZSH_THEME in .zshrc
Doppler
Secrets manager CLI for environment variable management
#!/usr/bin/env bash
# ════════════════════════════════════════════════════════════
# VPS Init — Bootstrap Script
# Generated at: 2026-04-01 16:49 (UTC)
# ════════════════════════════════════════════════════════════
# Review this script carefully before running.
# Run as root or with: sudo bash <script>
# Tested against: Ubuntu 22.04 LTS / Ubuntu 24.04 LTS
# ════════════════════════════════════════════════════════════
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
TARGET_USER=""
[ -z "$TARGET_USER" ] && { echo "❌ TARGET_USER is not set — set a username before running this script."; exit 1; }
echo ""
echo "┌──────────────────────────────────────────────────────────┐"
echo "│ VPS Init — Bootstrap Starting │"
echo "└──────────────────────────────────────────────────────────┘"
echo ""
# ──────────────────────────────────────────────────────────
# System Update & Upgrade
# ──────────────────────────────────────────────────────────
echo "→ Updating system packages..."
apt-get update -y
apt-get upgrade -y
apt-get autoremove -y
echo "✓ System updated."
# ──────────────────────────────────────────────────────────
# Timezone
# ──────────────────────────────────────────────────────────
TIMEZONE="UTC"
echo "→ Setting timezone to: $TIMEZONE"
timedatectl set-timezone "$TIMEZONE"
echo "✓ Timezone set."
# ──────────────────────────────────────────────────────────
# SSH Hardening
# ──────────────────────────────────────────────────────────
# ⚠ CAUTION: Ensure you have a working login method before applying.
echo "→ Hardening SSH configuration..."
SSHD_CONFIG="/etc/ssh/sshd_config"
sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' "$SSHD_CONFIG"
grep -q '^PermitRootLogin ' "$SSHD_CONFIG" || echo 'PermitRootLogin no' >> "$SSHD_CONFIG"
# Requires SSH keys to be already installed before enabling
sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' "$SSHD_CONFIG"
grep -q '^PasswordAuthentication ' "$SSHD_CONFIG" || echo 'PasswordAuthentication no' >> "$SSHD_CONFIG"
# Ubuntu 22.04+ uses 'ssh', older distros use 'sshd'
systemctl restart ssh 2>/dev/null || systemctl restart sshd
echo "✓ SSH hardened."
# ──────────────────────────────────────────────────────────
# Firewall (UFW)
# ──────────────────────────────────────────────────────────
echo "→ Configuring UFW firewall..."
apt-get install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable
ufw status verbose
echo "✓ Firewall enabled."
# ──────────────────────────────────────────────────────────
# Fail2ban
# ──────────────────────────────────────────────────────────
echo "→ Installing fail2ban..."
apt-get install -y fail2ban
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
backend = systemd
[sshd]
enabled = true
EOF
systemctl enable --now fail2ban
echo "✓ Fail2ban installed and active."
# ──────────────────────────────────────────────────────────
# Unattended Security Upgrades
# ──────────────────────────────────────────────────────────
echo "→ Enabling automatic security updates..."
apt-get install -y unattended-upgrades apt-listchanges
dpkg-reconfigure -plow unattended-upgrades
echo "✓ Unattended upgrades enabled."
# ──────────────────────────────────────────────────────────
# Swap (2G)
# ──────────────────────────────────────────────────────────
echo "→ Configuring swap..."
SWAP_FILE="/swapfile"
SWAP_SIZE="2G"
if [ -f "$SWAP_FILE" ]; then
echo " Swap file already exists — skipping."
else
fallocate -l "$SWAP_SIZE" "$SWAP_FILE"
chmod 600 "$SWAP_FILE"
mkswap "$SWAP_FILE"
swapon "$SWAP_FILE"
echo "$SWAP_FILE none swap sw 0 0" >> /etc/fstab
echo "✓ Swap ($SWAP_SIZE) configured."
fi
# Optimize for server workloads
sysctl -w vm.swappiness=10
grep -q 'vm.swappiness' /etc/sysctl.conf || echo 'vm.swappiness=10' >> /etc/sysctl.conf
# ──────────────────────────────────────────────────────────
# Baseline Utilities
# ──────────────────────────────────────────────────────────
echo "→ Installing baseline utilities..."
apt-get install -y \curl wget git vim nano \htop btop tmux screen uild-essential ca-certificates gnupg lsb-release \jq ripgrep unzip zip
et-tools dnsutils ncdu tree mtr
echo "✓ Baseline utilities installed."
# ──────────────────────────────────────────────────────────
# Node.js lts (via fnm)
# ──────────────────────────────────────────────────────────
echo "→ Installing Node.js lts for $TARGET_USER (via fnm)..."
su - "$TARGET_USER" -s /bin/bash << 'NODESETUP'
set -euo pipefail
curl -fsSL https://fnm.vercel.app/install | bash
export FNM_PATH="$HOME/.local/share/fnm"
export PATH="$FNM_PATH:$PATH"
eval "$(fnm env --shell bash)"
fnm install --lts
fnm use lts-latest
fnm default lts-latest
node --version
npm --version
NODESETUP
echo "✓ Node.js installed."
# ──────────────────────────────────────────────────────────
# Python (uv)
# ──────────────────────────────────────────────────────────
echo "→ Installing uv (Python package manager) for $TARGET_USER..."
su - "$TARGET_USER" -s /bin/bash << 'UVSETUP'
set -euo pipefail
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv --version
echo "→ Installing latest Python via uv..."
uv python install
uv python list
echo "✓ Python installed."
UVSETUP
echo "✓ uv installed."
# ──────────────────────────────────────────────────────────
# Caddy (Reverse Proxy)
# ──────────────────────────────────────────────────────────
echo "→ Installing Caddy..."
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \| tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update -y
apt-get install -y caddy
systemctl enable --now caddy
caddy version
echo "✓ Caddy installed."
# ──────────────────────────────────────────────────────────
# Zsh & Oh My Zsh
# ──────────────────────────────────────────────────────────
echo "→ Installing Zsh..."
apt-get install -y zsh
chsh -s "$(which zsh)" "$TARGET_USER"
echo "→ Installing zoxide..."
apt-get install -y zoxide 2>/dev/null || {
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
install -m 755 "$HOME/.local/bin/zoxide" /usr/local/bin/zoxide 2>/dev/null || true
}
echo "→ Configuring zsh for $TARGET_USER..."
su - "$TARGET_USER" -s /bin/bash << 'ZSHSETUP'
set -euo pipefail
echo "→ Installing Oh My Zsh..."
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
sed -i 's/ZSH_THEME="robbyrussell"/ZSH_THEME="avit"/' ~/.zshrc
echo " Theme set to: avit"
echo "→ Installing zsh-autosuggestions & zsh-completions..."
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
git clone --depth=1 https://github.com/zsh-users/zsh-completions ~/.oh-my-zsh/custom/plugins/zsh-completions
sed -i 's/^plugins=(\\(.*\\))/plugins=(\\1 zsh-autosuggestions zsh-completions)/' ~/.zshrc
echo "✓ Autosuggestions & completions enabled."
grep -qF 'zoxide init zsh' ~/.zshrc || printf '
eval "$(zoxide init zsh)"
' >> ~/.zshrc
echo "✓ zoxide init added to ~/.zshrc"
# Add fnm init to .zshrc if not already present
if [ -f "$HOME/.local/share/fnm/fnm" ]; then
grep -qF 'fnm env' ~/.zshrc 2>/dev/null || printf '
# fnm
export FNM_PATH="$HOME/.local/share/fnm"
export PATH="$FNM_PATH:$PATH"
eval "$(fnm env --shell zsh)"
' >> ~/.zshrc
fi
# Add uv PATH to .zshrc if not already present
if [ -f "$HOME/.local/bin/uv" ]; then
grep -qF '.local/bin' ~/.zshrc 2>/dev/null || printf '
# uv
export PATH="$HOME/.local/bin:$PATH"
' >> ~/.zshrc
fi
ZSHSETUP
echo "✓ Zsh & Oh My Zsh configured for $TARGET_USER."
# ════════════════════════════════════════════════════════════
echo ""
echo "┌──────────────────────────────────────────────────────────┐"
echo "│ ✓ Bootstrap complete! Reboot recommended. │"
echo "└──────────────────────────────────────────────────────────┘"
echo ""How to run
Download, review, upload to your server, then:
sudo bash vps-init.sh