Last active
March 7, 2026 09:46
-
-
Save Stanislas-Poisson/5fd3ac4419253334f9e5f4f436fd8351 to your computer and use it in GitHub Desktop.
Prepare VPS
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 | |
| ################################################################################ | |
| # 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