VPS Init

Bootstrap script generator

-

royz.dev

Configure your server

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