Skip to content

Instantly share code, notes, and snippets.

@Stanislas-Poisson
Last active March 7, 2026 09:46
Show Gist options
  • Select an option

  • Save Stanislas-Poisson/5fd3ac4419253334f9e5f4f436fd8351 to your computer and use it in GitHub Desktop.

Select an option

Save Stanislas-Poisson/5fd3ac4419253334f9e5f4f436fd8351 to your computer and use it in GitHub Desktop.
Prepare VPS
#!/bin/bash
################################################################################
# VPS Setup Script - Production Ready + Docker Optimization
################################################################################
# Author: StanislasP (https://github.com/Stanislas-Poisson)
# Version: 1.1.0
# Date: 2025-12-18
# Description: Complete VPS setup for Debian 12/13 with Docker,
# GitLab Runner, security hardening, monitoring,
# and AUTOMATIC DOCKER CLEANUP
# Repository: https://gist.github.com/Stanislas-Poisson/5fd3ac4419253334f9e5f4f436fd8351
#
# Usage: bash prepare-vps.sh
#
# Requirements: - Debian 11+ (tested on 12 & 13)
# - Root access (use sudo)
# - Internet connection
# - Minimum 1GB RAM (2GB recommended)
#
# What it does: 1. System updates & locale/timezone config
# 2. Swap configuration (optional)
# 3. Docker + Docker Compose installation
# 4. GitLab Runner setup with optimized cache
# 5. AUTOMATIC DOCKER CLEANUP (daily + weekly)
# 6. Firewall (UFW) configuration
# 7. Fail2ban intrusion prevention
# 8. Automatic security updates
# 9. Prometheus Node Exporter monitoring
# 10. Custom bashrc with Docker stats alias
#
# Changelog: 1.1.0 - Added automatic Docker cleanup (daily/weekly)
# - Optimized pull_policy (if-not-present)
# - Added dockerstats command
# 1.0.0 - Initial release
################################################################################
set -euo pipefail
IFS=$'\n\t'
################################################################################
# CONFIGURATION - MODIFY THESE VALUES ACCORDING TO YOUR NEEDS
################################################################################
# System configuration
LOCALE="fr_FR.UTF-8"
TIMEZONE="Europe/Paris"
USER_NAME="debian"
# Swap configuration
ENABLE_SWAP=true
SWAP_SIZE="2G"
# GitLab Runner configuration (leave empty to skip)
GL_URL="https://gitlab.com/"
GL_PAT="" # GitLab Personal Access Token
GL_PROJECT_PATH="" # "groups/groupid" OR "username/project"
GL_RUNNER_TAGS="vps,docker,debian"
GL_RUNNER_DESC="runner-$(hostname -s)"
# Additional ports to open (besides SSH port 22)
EXTRA_PORTS="80,443"
# Node Exporter port (monitoring)
NODE_EXPORTER_PORT="9100"
################################################################################
# COLORS & LOGGING
################################################################################
readonly SCRIPT_VERSION="1.1.0"
readonly MIN_DEBIAN_VERSION=12
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly BOLD='\033[1m'
readonly NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_success() { echo -e "${GREEN}[✓]${NC} $*"; }
log_warning() { echo -e "${YELLOW}[⚠]${NC} $*"; }
log_error() { echo -e "${RED}[✗]${NC} $*"; exit 1; }
log_step() { echo -e "\n${CYAN}${BOLD}━━━ Step $1/11: $2 ━━━${NC}\n"; }
################################################################################
# PRE-FLIGHT CHECKS
################################################################################
[[ $EUID -ne 0 ]] && log_error "This script must be run as root (use sudo)"
if [[ ! -f /etc/debian_version ]]; then
log_error "This script is designed for Debian systems only"
fi
DEBIAN_VERSION=$(cat /etc/debian_version | cut -d. -f1)
if [[ $DEBIAN_VERSION -lt $MIN_DEBIAN_VERSION ]]; then
log_error "Debian ${MIN_DEBIAN_VERSION}+ required. Current version: $DEBIAN_VERSION"
fi
log_info "Checking internet connectivity…"
INTERNET_OK=false
for host in 8.8.8.8 1.1.1.1 208.67.222.222; do
if ping -c 1 -W 5 "$host" &>/dev/null; then
INTERNET_OK=true
break
fi
done
if [[ "$INTERNET_OK" == false ]]; then
log_error "No internet connectivity detected. Please check network configuration."
fi
log_success "Internet connectivity OK"
################################################################################
# BANNER
################################################################################
clear
cat <<EOF
${CYAN}${BOLD}
╔═══════════════════════════════════════════════════════════╗
║ ║
║ VPS Setup Script v${SCRIPT_VERSION} ║
║ Production-Ready Debian Server ║
║ + Automatic Docker Cleanup ║
║ ║
║ Author: StanislasP ║
║ GitHub: github.com/zairakai/ ║
║ ║
╚═══════════════════════════════════════════════════════════╝
${NC}
${BOLD}Detected System:${NC} Debian $DEBIAN_VERSION
${BOLD}Target User:${NC} $USER_NAME
${BOLD}Configuration:${NC}
• Locale: $LOCALE
• Timezone: $TIMEZONE
• Swap: $([ "$ENABLE_SWAP" == true ] && echo "$SWAP_SIZE" || echo "Disabled")
• GitLab Runner: $([ -n "$GL_PAT" ] && echo "Enabled" || echo "Disabled")
• Docker Cleanup: Automated (daily + weekly)
EOF
read -r -p "Press Enter to continue or Ctrl+C to abort…"
################################################################################
# STEP 1: SYSTEM UPDATES
################################################################################
log_step 1 "System Updates"
log_info "Updating package lists…"
apt update
log_info "Upgrading installed packages…"
DEBIAN_FRONTEND=noninteractive apt upgrade -y
log_info "Installing essential packages…"
apt install -y \
ca-certificates curl gnupg apt-transport-https \
git vim htop tree wget unzip zip rsync jq \
bash-completion net-tools ncdu iotop
log_success "System updated and essential packages installed"
################################################################################
# STEP 2: LOCALE & TIMEZONE
################################################################################
log_step 2 "Locale & Timezone Configuration"
log_info "Configuring locale: $LOCALE"
apt install -y locales
if ! grep -q "^${LOCALE} UTF-8" /etc/locale.gen; then
echo "${LOCALE} UTF-8" >> /etc/locale.gen
log_info "Locale added to /etc/locale.gen"
else
log_info "Locale already present in /etc/locale.gen"
fi
locale-gen "$LOCALE"
update-locale LANG="$LOCALE" LANGUAGE="$LOCALE" LC_ALL="$LOCALE"
log_info "Setting timezone: $TIMEZONE"
timedatectl set-timezone "$TIMEZONE"
log_success "Locale and timezone configured"
################################################################################
# STEP 3: SWAP CONFIGURATION
################################################################################
log_step 3 "Swap Configuration"
if [[ "$ENABLE_SWAP" == true ]]; then
if swapon --show | grep -q "/swapfile"; then
log_warning "Swap already exists and is active"
else
log_info "Creating swap file: $SWAP_SIZE"
fallocate -l "$SWAP_SIZE" /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
if ! grep -q "/swapfile" /etc/fstab; then
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
sysctl vm.swappiness=10
if ! grep -q "vm.swappiness" /etc/sysctl.conf; then
echo 'vm.swappiness=10' >> /etc/sysctl.conf
fi
log_success "Swap $SWAP_SIZE created and configured"
fi
else
log_info "Swap disabled (ENABLE_SWAP=false)"
fi
################################################################################
# STEP 4: DOCKER INSTALLATION
################################################################################
log_step 4 "Docker Installation"
log_info "Removing old Docker versions…"
for pkg in docker docker-engine docker.io containerd runc; do
apt remove -y "$pkg" 2>/dev/null || true
done
log_info "Adding Docker repository…"
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
DOCKER_CODENAME="bookworm"
if [[ $DEBIAN_VERSION -eq 13 ]]; then
log_warning "Debian 13 detected - using Bookworm Docker repository"
fi
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $DOCKER_CODENAME stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
log_info "Installing Docker Engine…"
apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
if id "$USER_NAME" &>/dev/null; then
usermod -aG docker "$USER_NAME"
log_success "User $USER_NAME added to docker group"
else
log_warning "User $USER_NAME not found - skipping docker group addition"
fi
log_info "Configuring Docker daemon…"
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"features": {
"buildkit": true
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB"
}
}
}
EOF
log_info "Configuring Docker log rotation…"
cat > /etc/logrotate.d/docker <<EOF
/var/lib/docker/containers/*/*.log {
rotate 7
daily
compress
size=10M
missingok
delaycompress
copytruncate
}
EOF
systemctl enable docker
systemctl start docker
mkdir -p /var/cache/docker-build
chown "$USER_NAME":"$USER_NAME" /var/cache/docker-build
if systemctl is-active --quiet docker; then
DOCKER_VERSION=$(docker --version | cut -d' ' -f3 | tr -d ',')
log_success "Docker $DOCKER_VERSION installed and running"
else
log_error "Docker installation failed"
fi
################################################################################
# STEP 5: GITLAB RUNNER
################################################################################
log_step 5 "GitLab Runner Configuration"
if [[ -n "$GL_PAT" ]] && [[ -n "$GL_PROJECT_PATH" ]]; then
log_info "Setting up GitLab Runner with Docker socket binding…"
mkdir -p /srv/gitlab-runner/config
mkdir -p /srv/gitlab-runner/cache
chown -R "$USER_NAME":"$USER_NAME" /srv/gitlab-runner
log_info "Pulling GitLab Runner image…"
docker pull gitlab/gitlab-runner:latest
if docker ps -a --format '{{.Names}}' | grep -q "^gitlab-runner$"; then
log_warning "Removing existing GitLab Runner container…"
docker rm -f gitlab-runner
fi
log_info "Creating GitLab Runner container with Docker socket access…"
docker run -d \
--name gitlab-runner \
--restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /srv/gitlab-runner/cache:/cache \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
log_info "Creating GitLab Runner via modern API (GitLab 16.0+)…"
if [[ "$GL_PROJECT_PATH" =~ ^groups/ ]]; then
RUNNER_TYPE="group_type"
GROUP_ID="${GL_PROJECT_PATH#groups/}"
log_info "Creating group runner for group ID: $GROUP_ID"
CREATE_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" --request POST \
--header "PRIVATE-TOKEN: $GL_PAT" \
--header "Content-Type: application/json" \
"${GL_URL}api/v4/user/runners" \
--data "{
\"runner_type\": \"$RUNNER_TYPE\",
\"group_id\": $GROUP_ID,
\"description\": \"$GL_RUNNER_DESC\",
\"tag_list\": [$(echo "$GL_RUNNER_TAGS" | sed 's/,/","/g' | sed 's/^/"/;s/$/"/')],
\"run_untagged\": true,
\"locked\": false
}")
else
RUNNER_TYPE="project_type"
ENCODED_PATH=$(echo "$GL_PROJECT_PATH" | sed 's/\//%2F/g')
log_info "Getting project ID for: $GL_PROJECT_PATH"
PROJECT_RESPONSE=$(curl -s --header "PRIVATE-TOKEN: $GL_PAT" \
"${GL_URL}api/v4/projects/${ENCODED_PATH}")
PROJECT_ID=$(echo "$PROJECT_RESPONSE" | jq -r '.id' 2>/dev/null)
if [[ -z "$PROJECT_ID" ]] || [[ "$PROJECT_ID" == "null" ]]; then
log_error "Cannot find project: $GL_PROJECT_PATH. Response: $PROJECT_RESPONSE"
fi
log_info "Creating project runner for project ID: $PROJECT_ID"
CREATE_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" --request POST \
--header "PRIVATE-TOKEN: $GL_PAT" \
--header "Content-Type: application/json" \
"${GL_URL}api/v4/user/runners" \
--data "{
\"runner_type\": \"$RUNNER_TYPE\",
\"project_id\": $PROJECT_ID,
\"description\": \"$GL_RUNNER_DESC\",
\"tag_list\": [$(echo "$GL_RUNNER_TAGS" | sed 's/,/","/g' | sed 's/^/"/;s/$/"/')],
\"run_untagged\": true,
\"locked\": false
}")
fi
HTTP_BODY=$(echo "$CREATE_RESPONSE" | grep -v "^HTTP_STATUS:")
HTTP_STATUS=$(echo "$CREATE_RESPONSE" | grep "^HTTP_STATUS:" | cut -d: -f2)
if [[ -z "$HTTP_STATUS" ]] || [[ "$HTTP_STATUS" -lt 200 ]] || [[ "$HTTP_STATUS" -ge 300 ]]; then
log_error "GitLab Runner creation failed with HTTP $HTTP_STATUS. Response: $HTTP_BODY"
fi
RUNNER_TOKEN=$(echo "$HTTP_BODY" | jq -r '.token' 2>/dev/null)
RUNNER_ID=$(echo "$HTTP_BODY" | jq -r '.id' 2>/dev/null)
if [[ -z "$RUNNER_TOKEN" ]] || [[ "$RUNNER_TOKEN" == "null" ]]; then
log_error "Failed to extract runner token. API Response: $HTTP_BODY"
fi
log_success "Runner created successfully (ID: $RUNNER_ID)"
log_info "Registering runner with Docker executor (socket binding)…"
if ! docker exec gitlab-runner gitlab-runner register \
--non-interactive \
--url "$GL_URL" \
--token "$RUNNER_TOKEN" \
--executor "docker" \
--docker-image "docker:24-cli" \
--docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
--docker-volumes "/cache" \
--docker-volumes "/var/cache/docker-build:/cache/docker:rw" 2>&1 | tee -a /tmp/gitlab-runner-register.log; then
log_error "Runner registration failed. Check logs: /tmp/gitlab-runner-register.log"
fi
log_info "Optimizing runner configuration…"
docker exec gitlab-runner sh -c "
# Set concurrent builds
sed -i 's/^concurrent = .*/concurrent = 4/' /etc/gitlab-runner/config.toml
# CRITICAL: Use local cache first (no always pull)
sed -i 's/pull_policy = .*/pull_policy = [\"if-not-present\"]/' /etc/gitlab-runner/config.toml
# Ensure Docker socket is properly mounted
if ! grep -q '/var/run/docker.sock:/var/run/docker.sock' /etc/gitlab-runner/config.toml; then
sed -i '/volumes = /s/\]/,\"\/var\/run\/docker.sock:\/var\/run\/docker.sock\"]/' /etc/gitlab-runner/config.toml
fi
"
docker restart gitlab-runner
log_success "GitLab Runner configured with optimized settings"
log_info "Runner ID: $RUNNER_ID"
log_info "Configuration:"
log_info " • Docker socket: /var/run/docker.sock (bind-mounted)"
log_info " • Build cache: /var/cache/docker-build"
log_info " • Runner cache: /srv/gitlab-runner/cache"
log_info " • Concurrent builds: 4"
log_info " • Pull policy: if-not-present (uses local cache)"
else
log_warning "GitLab Runner not configured (GL_PAT or GL_PROJECT_PATH empty)"
log_info "To configure later:"
log_info " 1. Set GL_PAT and GL_PROJECT_PATH in this script"
log_info " 2. Re-run the GitLab Runner section (Step 5)"
fi
################################################################################
# STEP 6: DOCKER CLEANUP AUTOMATION (CRITICAL)
################################################################################
log_step 6 "Docker Cleanup Automation"
log_info "Creating smart daily cleanup script…"
cat > /usr/local/bin/docker-smart-cleanup.sh <<'EOF'
#!/bin/bash
LOG="/var/log/docker-cleanup.log"
echo "=== Smart Cleanup $(date) ===" >> $LOG
# Keep images from last 24h, remove dangling and old build cache
docker container prune -f >> $LOG 2>&1
docker image prune -f >> $LOG 2>&1
docker builder prune --keep-storage 10GB -f >> $LOG 2>&1
echo "Disk after cleanup:" >> $LOG
df -h / >> $LOG
docker system df >> $LOG
echo "" >> $LOG
EOF
chmod +x /usr/local/bin/docker-smart-cleanup.sh
log_info "Creating aggressive weekly cleanup script…"
cat > /usr/local/bin/docker-weekly-cleanup.sh <<'EOF'
#!/bin/bash
LOG="/var/log/docker-cleanup.log"
echo "=== Weekly Full Cleanup $(date) ===" >> $LOG
# Remove EVERYTHING unused (images will be re-pulled on next build)
docker system prune -a --volumes -f >> $LOG 2>&1
echo "Disk after cleanup:" >> $LOG
df -h / >> $LOG
docker system df >> $LOG
echo "" >> $LOG
EOF
chmod +x /usr/local/bin/docker-weekly-cleanup.sh
log_info "Configuring cron jobs…"
(crontab -l 2>/dev/null | grep -v "docker.*cleanup"; cat <<CRON
0 3 * * 1-6 /usr/local/bin/docker-smart-cleanup.sh
0 2 * * 0 /usr/local/bin/docker-weekly-cleanup.sh
CRON
) | crontab -
log_success "Docker cleanup automation configured"
log_info " • Smart cleanup: Monday-Saturday at 3:00 AM"
log_info " → Keeps recent images, removes dangling & old cache"
log_info " • Full cleanup: Sunday at 2:00 AM"
log_info " → Removes ALL unused images/volumes"
log_info " • Cleanup logs: /var/log/docker-cleanup.log"
################################################################################
# STEP 7: FIREWALL (UFW)
################################################################################
log_step 7 "Firewall Configuration"
log_info "Installing and configuring UFW…"
apt install -y ufw
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw limit 22/tcp comment 'SSH with rate limiting'
log_info "SSH port 22 allowed with rate limiting"
if [[ -n "$EXTRA_PORTS" ]]; then
IFS=',' read -ra PORTS <<< "$EXTRA_PORTS"
for port in "${PORTS[@]}"; do
port=$(echo "$port" | xargs)
if [[ -n "$port" ]]; then
ufw allow "${port}/tcp" comment "Custom port ${port}"
log_info "Port $port allowed"
fi
done
fi
# CRITICAL FIX: Enable firewall properly
log_info "Enabling firewall…"
echo "y" | ufw enable
# Verify firewall is active
if ufw status | grep -q "Status: active"; then
log_success "Firewall configured and ACTIVE"
else
log_error "Firewall failed to activate"
fi
################################################################################
# STEP 8: FAIL2BAN
################################################################################
log_step 8 "Fail2ban Configuration"
log_info "Installing and configuring fail2ban…"
apt install -y fail2ban
cat > /etc/fail2ban/jail.local <<EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
destemail = root@localhost
sendername = Fail2Ban
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 5
EOF
systemctl enable fail2ban
systemctl restart fail2ban
log_success "Fail2ban configured (SSH protection active)"
################################################################################
# STEP 9: AUTOMATIC SECURITY UPDATES
################################################################################
log_step 9 "Automatic Security Updates"
log_info "Configuring unattended-upgrades…"
apt install -y unattended-upgrades
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
Unattended-Upgrade::Allowed-Origins {
"\${distro_id}:\${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
EOF
cat > /etc/apt/apt.conf.d/20auto-upgrades <<EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Download-Upgradeable-Packages "1";
EOF
log_success "Automatic security updates enabled"
################################################################################
# STEP 10: MONITORING (NODE EXPORTER)
################################################################################
log_step 10 "Monitoring Installation"
log_info "Installing Prometheus Node Exporter…"
apt install -y prometheus-node-exporter
systemctl enable prometheus-node-exporter
systemctl start prometheus-node-exporter
if systemctl is-active --quiet prometheus-node-exporter; then
log_success "Node Exporter running on port $NODE_EXPORTER_PORT (localhost only)"
log_info "To scrape metrics: curl http://localhost:$NODE_EXPORTER_PORT/metrics"
else
log_warning "Node Exporter installation failed"
fi
################################################################################
# STEP 11: CUSTOM BASHRC
################################################################################
log_step 11 "Custom Bashrc Installation"
USER_HOME="/home/$USER_NAME"
if [[ ! -d "$USER_HOME" ]]; then
log_error "User home directory not found: $USER_HOME"
fi
if [[ -f "$USER_HOME/.bashrc" ]]; then
BACKUP_FILE="$USER_HOME/.bashrc.backup-$(date +%Y%m%d-%H%M%S)"
cp "$USER_HOME/.bashrc" "$BACKUP_FILE"
log_info "Existing .bashrc backed up to: $BACKUP_FILE"
fi
log_info "Installing custom .bashrc with dockerstats command…"
cat > "$USER_HOME/.bashrc" <<'BASHRC'
case $- in
*i*) ;;
*) return;;
esac
HISTCONTROL=ignoreboth:ignoredups:erasedups
shopt -s histappend
HISTSIZE=10000
HISTFILESIZE=20000
HISTTIMEFORMAT="%Y-%m-%d %T "
shopt -s checkwinsize
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto --group-directories-first'
alias grep='grep --color=auto'
fi
smart_path() {
local path=$(pwd | sed "s|$HOME|~|")
if [[ ${#path} -gt 60 ]]; then
local start=$(echo "$path" | cut -d'/' -f1-2)
local end=$(echo "$path" | rev | cut -d'/' -f1-2 | rev)
echo "${start}/…/${end}"
else
echo "$path"
fi
}
git_status() {
if ! git rev-parse --git-dir &>/dev/null; then return; fi
local branch=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)
local status=$(git status --porcelain 2>/dev/null)
local staged=$(echo "$status" | grep '^[MADRC]' | wc -l)
local unstaged=$(echo "$status" | grep '^.[MD]' | wc -l)
local untracked=$(echo "$status" | grep '^??' | wc -l)
if [[ $staged -eq 0 && $unstaged -eq 0 && $untracked -eq 0 ]]; then
echo -e " \033[1;33mgit:\033[0m\033[1;36m$branch\033[0m \033[1;32m✓\033[0m"
else
local status_str=""
[[ $staged -gt 0 ]] && status_str+=" \033[1;32m+$staged\033[0m"
[[ $unstaged -gt 0 ]] && status_str+=" \033[1;31m~$unstaged\033[0m"
[[ $untracked -gt 0 ]] && status_str+=" \033[1;37m?$untracked\033[0m"
echo -e " \033[1;33mgit:\033[0m\033[1;36m$branch\033[0m$status_str"
fi
}
maj() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 System Update"
sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo apt autoclean -y
echo "✅ Update completed!"
}
status() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 VPS Status"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💾 Disk: $(df -h / | tail -1 | awk '{print $3 "/" $2 " (" $5 ")"}')"
echo "🧠 RAM: $(free -h | grep Mem | awk '{print $3 "/" $2}')"
echo "⏱️ Uptime: $(uptime -p)"
echo "🐳 Docker: $(docker ps --format '{{.Names}}' | wc -l) containers"
docker ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null || echo "Docker not available"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
check() {
echo "🔍 Service Health Check"
echo ""
systemctl is-active docker &>/dev/null && echo "✓ Docker" || echo "✗ Docker"
systemctl is-active fail2ban &>/dev/null && echo "✓ Fail2ban" || echo "✗ Fail2ban"
ufw status | grep -q "Status: active" && echo "✓ UFW (active)" || echo "✗ UFW (inactive)"
docker ps -q --filter name=gitlab-runner &>/dev/null && echo "✓ GitLab Runner" || echo "✗ GitLab Runner"
systemctl is-active prometheus-node-exporter &>/dev/null && echo "✓ Node Exporter" || echo "✗ Node Exporter"
}
dockerstats() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🐳 Docker Usage"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
docker system df
echo ""
echo "📦 Top 10 Images by Size:"
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | head -n 11
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
PS1="\[\033[1;34m\](\t)\[\033[0m\] \[\033[0;31m\]\u@\h\[\033[0m\] \[\033[1;32m\]\$(smart_path)\[\033[0m\]\$(git_status)\n\[\033[0;35m\]>\[\033[0m\] "
alias ll='ls -AlFh'
alias la='ls -A'
alias l='ls -lAh'
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
alias ..='cd ..'
alias …='cd ../..'
alias dc='docker compose'
alias dcup='docker compose up -d'
alias dcdown='docker compose down'
alias dclogs='docker compose logs -f'
alias dcps='docker compose ps'
alias dcrestart='docker compose restart'
alias gs='git status'
alias ga='git add'
alias gc='git commit -m'
alias gp='git push'
alias gl='git pull'
alias www='cd /var/www'
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
[ -f ~/.bash_aliases ] && . ~/.bash_aliases
if ! shopt -oq posix; then
[ -f /usr/share/bash-completion/bash_completion ] && . /usr/share/bash-completion/bash_completion
[ -f /etc/bash_completion ] && . /etc/bash_completion
fi
BASHRC
chown "$USER_NAME":"$USER_NAME" "$USER_HOME/.bashrc"
chmod 644 "$USER_HOME/.bashrc"
log_success "Custom .bashrc installed for $USER_NAME"
################################################################################
# CLEANUP
################################################################################
log_info "Cleaning up…"
apt autoremove -y
apt autoclean -y
################################################################################
# FINAL SUMMARY
################################################################################
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ VPS Setup Completed Successfully!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf "\n"
printf "%b📋 Configuration Summary:%b\n" "${BOLD}" "${NC}"
printf " • System: Debian %s\n" "$DEBIAN_VERSION"
printf " • Locale: %s\n" "$LOCALE"
printf " • Timezone: %s\n" "$TIMEZONE"
printf " • Swap: %s\n" "$([ "$ENABLE_SWAP" == true ] && echo "$SWAP_SIZE" || echo "Disabled")"
printf " • Docker: %s\n" "$DOCKER_VERSION"
printf " • GitLab Runner: %s\n" "$(docker ps --filter name=gitlab-runner -q &>/dev/null && echo "✓ Running" || echo "✗ Not configured")"
printf " • Firewall (UFW): %s\n" "$(ufw status | grep -q 'Status: active' && echo "✓ Active" || echo "✗ Inactive")"
printf " • Fail2ban: %s\n" "$(systemctl is-active fail2ban 2>/dev/null | grep -q active && echo "✓ Active" || echo "✗ Inactive")"
printf " • Docker Cleanup: %s\n" "✓ Automated (daily + weekly)"
printf "\n"
printf "%b%b⚠️ CRITICAL - Action Required:%b\n" "${BOLD}" "${YELLOW}" "${NC}"
printf "\n"
printf " 1️⃣ %bLog out and back in%b to apply docker group changes:\n" "${BOLD}" "${NC}"
printf " %bexit%b\n" "${CYAN}" "${NC}"
printf " %bssh %s@\$(hostname -I | awk '{print \$1}')%b\n" "${CYAN}" "$USER_NAME" "${NC}"
printf "\n"
printf " 2️⃣ %bTest Docker%b (as %s, not root):\n" "${BOLD}" "${NC}" "$USER_NAME"
printf " %bdocker run hello-world%b\n" "${CYAN}" "${NC}"
printf "\n"
printf " 3️⃣ %bSetup SSH key authentication%b (CRITICAL for security):\n" "${BOLD}" "${NC}"
printf " %b# On your local machine:%b\n" "${CYAN}" "${NC}"
printf " %bssh-copy-id %s@\$(hostname -I | awk '{print \$1}')%b\n" "${CYAN}" "$USER_NAME" "${NC}"
printf "\n"
printf " %b# Then on the server:%b\n" "${CYAN}" "${NC}"
printf " %bsudo nano /etc/ssh/sshd_config%b\n" "${CYAN}" "${NC}"
printf " %b# Set: PasswordAuthentication no%b\n" "${CYAN}" "${NC}"
printf " %b# Set: PubkeyAuthentication yes%b\n" "${CYAN}" "${NC}"
printf " %bsudo systemctl restart sshd%b\n" "${CYAN}" "${NC}"
printf "\n"
printf " 4️⃣ %bVerify all services:%b\n" "${BOLD}" "${NC}"
printf " %bcheck%b\n" "${CYAN}" "${NC}"
printf " %bstatus%b\n" "${CYAN}" "${NC}"
printf "\n"
printf "%b📚 Available Commands:%b\n" "${BOLD}" "${NC}"
printf " • %bmaj%b : Update system packages\n" "${CYAN}" "${NC}"
printf " • %bstatus%b : Display system status (disk, RAM, uptime, containers)\n" "${CYAN}" "${NC}"
printf " • %bcheck%b : Health check for all services\n" "${CYAN}" "${NC}"
printf " • %bdockerstats%b : Display Docker disk usage and top images\n" "${CYAN}" "${NC}"
printf " • %bdc / dcup%b : Docker Compose shortcuts\n" "${CYAN}" "${NC}"
printf "\n"
printf "%b🧹 Docker Maintenance:%b\n" "${BOLD}" "${NC}"
printf " • Smart cleanup: Monday-Saturday at 3:00 AM\n"
printf " → Keeps latest images, removes dangling & old cache\n"
printf " • Full cleanup: Sunday at 2:00 AM\n"
printf " → Removes ALL unused images/volumes\n"
printf " • Manual cleanup: docker system prune -a --volumes -f\n"
printf " • Cleanup logs: /var/log/docker-cleanup.log\n"
printf "\n"
printf "%b📁 Important Paths:%b\n" "${BOLD}" "${NC}"
printf " • Docker cache: /var/cache/docker-build\n"
printf " • Runner cache: /srv/gitlab-runner/cache\n"
printf " • Runner config: /srv/gitlab-runner/config\n"
printf " • Docker logs: /var/lib/docker/containers/*/*.log\n"
printf " • Cleanup logs: /var/log/docker-cleanup.log\n"
printf " • System logs: /var/log/\n"
printf "\n"
printf "%b🔥 Firewall Rules:%b\n" "${BOLD}" "${NC}"
ufw status numbered | grep -E "^\[|ALLOW" | sed 's/^/ /' || echo " No rules displayed"
printf "\n"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf "\n"
printf "%b%b🎉 Your VPS is now ready for production use!%b\n" "${GREEN}" "${BOLD}" "${NC}"
printf "%b Docker cleanup automation will prevent disk saturation.%b\n" "${GREEN}" "${NC}"
printf "\n"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment