Created
February 23, 2026 20:01
-
-
Save vapvarun/649b44c1a00d233ebd7faec32a73169d to your computer and use it in GitHub Desktop.
WordPress on DigitalOcean — Complete Server Setup Guide (Ubuntu 24.04 + Nginx + MySQL 8 + PHP 8.3 + Redis)
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 | |
| # Create a sudo user and disable root SSH login | |
| # Run this as root on your fresh DigitalOcean droplet | |
| set -euo pipefail | |
| USERNAME="deploy" | |
| # Create user with home directory | |
| adduser --disabled-password --gecos "" "$USERNAME" | |
| # Set a strong password (you'll be prompted) | |
| passwd "$USERNAME" | |
| # Add to sudo group | |
| usermod -aG sudo "$USERNAME" | |
| # Copy SSH keys from root to new user | |
| mkdir -p /home/$USERNAME/.ssh | |
| cp /root/.ssh/authorized_keys /home/$USERNAME/.ssh/ | |
| chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh | |
| chmod 700 /home/$USERNAME/.ssh | |
| chmod 600 /home/$USERNAME/.ssh/authorized_keys | |
| # Disable root login via SSH | |
| sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config | |
| sed -i 's/^#PermitRootLogin/PermitRootLogin/' /etc/ssh/sshd_config | |
| # Restart SSH | |
| systemctl restart sshd | |
| echo "User '$USERNAME' created. Test SSH login in a NEW terminal before closing this session:" | |
| echo " ssh $USERNAME@$(curl -s ifconfig.me)" |
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 | |
| # Configure UFW firewall — allow SSH, HTTP, HTTPS only | |
| # Run as your sudo user (not root) | |
| set -euo pipefail | |
| # Enable UFW with default deny incoming | |
| sudo ufw default deny incoming | |
| sudo ufw default allow outgoing | |
| # Allow SSH (do this FIRST or you'll lock yourself out) | |
| sudo ufw allow OpenSSH | |
| # Allow web traffic | |
| sudo ufw allow 'Nginx Full' | |
| # Enable firewall | |
| sudo ufw --force enable | |
| # Verify | |
| sudo ufw status verbose | |
| echo "" | |
| echo "Firewall active. Only SSH (22), HTTP (80), and HTTPS (443) are open." |
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 | |
| # Install Nginx on Ubuntu 24.04 | |
| # Run as sudo user | |
| set -euo pipefail | |
| sudo apt update | |
| sudo apt install -y nginx | |
| # Start and enable on boot | |
| sudo systemctl start nginx | |
| sudo systemctl enable nginx | |
| # Verify | |
| sudo systemctl status nginx --no-pager | |
| echo "" | |
| echo "Nginx installed. Visit http://$(curl -s ifconfig.me) to see the default page." |
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 | |
| # Install MySQL 8 on Ubuntu 24.04 | |
| # Run as sudo user | |
| set -euo pipefail | |
| sudo apt update | |
| sudo apt install -y mysql-server | |
| # Start and enable | |
| sudo systemctl start mysql | |
| sudo systemctl enable mysql | |
| # Secure the installation | |
| # This sets root password, removes test DB, disables remote root login | |
| sudo mysql_secure_installation | |
| # Verify | |
| sudo systemctl status mysql --no-pager | |
| mysql --version |
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 | |
| # Create WordPress database and user | |
| # Run as sudo user | |
| set -euo pipefail | |
| DB_NAME="wordpress" | |
| DB_USER="wpuser" | |
| DB_PASS="$(openssl rand -base64 24)" | |
| sudo mysql <<EOF | |
| CREATE DATABASE ${DB_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; | |
| CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}'; | |
| GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost'; | |
| FLUSH PRIVILEGES; | |
| EOF | |
| echo "Database created successfully." | |
| echo "" | |
| echo "Save these credentials — you'll need them for wp-config.php:" | |
| echo " DB_NAME: ${DB_NAME}" | |
| echo " DB_USER: ${DB_USER}" | |
| echo " DB_PASS: ${DB_PASS}" | |
| echo "" | |
| echo "Store them somewhere safe, then delete this terminal output." |
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 | |
| # Install PHP 8.2-FPM with all WordPress-required extensions | |
| # Run as sudo user | |
| set -euo pipefail | |
| sudo apt update | |
| sudo apt install -y \ | |
| php8.3-fpm \ | |
| php8.3-mysql \ | |
| php8.3-curl \ | |
| php8.3-gd \ | |
| php8.3-intl \ | |
| php8.3-mbstring \ | |
| php8.3-xml \ | |
| php8.3-zip \ | |
| php8.3-bcmath \ | |
| php8.3-imagick \ | |
| php8.3-redis \ | |
| php8.3-opcache \ | |
| php8.3-soap \ | |
| php8.3-cli | |
| # Start and enable | |
| sudo systemctl start php8.3-fpm | |
| sudo systemctl enable php8.3-fpm | |
| # Verify | |
| sudo systemctl status php8.3-fpm --no-pager | |
| php -v | |
| php -m | head -30 | |
| echo "" | |
| echo "PHP 8.3-FPM installed with all WordPress extensions." | |
| echo "" | |
| echo "Note: Ubuntu 24.04 ships PHP 8.3 by default." | |
| echo "If you specifically need 8.2, add the ondrej/php PPA first:" | |
| echo " sudo add-apt-repository ppa:ondrej/php && sudo apt update" | |
| echo " Then replace 8.3 with 8.2 in the commands above." |
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
| ; PHP-FPM pool configuration for WordPress | |
| ; Save to: /etc/php/8.3/fpm/pool.d/www.conf (replace default) | |
| ; | |
| ; Tuned for a 2GB RAM droplet ($12/mo). | |
| ; For 1GB ($6/mo), halve pm.max_children to 5 and set memory_limit = 128M. | |
| ; For 4GB+, double max_children and raise memory_limit to 512M. | |
| [www] | |
| user = www-data | |
| group = www-data | |
| listen = /run/php/php8.3-fpm.sock | |
| listen.owner = www-data | |
| listen.group = www-data | |
| ; Process manager: dynamic scales workers based on traffic | |
| pm = dynamic | |
| pm.max_children = 10 | |
| pm.start_servers = 3 | |
| pm.min_spare_servers = 2 | |
| pm.max_spare_servers = 5 | |
| pm.max_requests = 500 | |
| ; PHP settings for WordPress | |
| php_admin_value[memory_limit] = 256M | |
| php_admin_value[upload_max_filesize] = 64M | |
| php_admin_value[post_max_size] = 64M | |
| php_admin_value[max_execution_time] = 300 | |
| php_admin_value[max_input_vars] = 3000 | |
| php_admin_value[max_input_time] = 300 | |
| ; OPcache — keeps compiled PHP in memory | |
| php_admin_value[opcache.enable] = 1 | |
| php_admin_value[opcache.memory_consumption] = 128 | |
| php_admin_value[opcache.interned_strings_buffer] = 16 | |
| php_admin_value[opcache.max_accelerated_files] = 10000 | |
| php_admin_value[opcache.revalidate_freq] = 60 | |
| php_admin_value[opcache.validate_timestamps] = 1 | |
| ; Security | |
| php_admin_value[expose_php] = Off | |
| php_admin_value[cgi.fix_pathinfo] = 0 |
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
| # Nginx server block for WordPress (HTTP only — SSL added later) | |
| # Save to: /etc/nginx/sites-available/wordpress | |
| # Then: sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/ | |
| # And: sudo rm /etc/nginx/sites-enabled/default | |
| # Then: sudo nginx -t && sudo systemctl reload nginx | |
| server { | |
| listen 80; | |
| listen [::]:80; | |
| server_name yourdomain.com www.yourdomain.com; | |
| root /var/www/wordpress; | |
| index index.php index.html; | |
| # Logs | |
| access_log /var/log/nginx/wordpress-access.log; | |
| error_log /var/log/nginx/wordpress-error.log; | |
| # Max upload size — match PHP settings | |
| client_max_body_size 64M; | |
| # WordPress permalinks | |
| location / { | |
| try_files $uri $uri/ /index.php?$args; | |
| } | |
| # PHP processing | |
| location ~ \.php$ { | |
| include snippets/fastcgi-params.conf; | |
| fastcgi_pass unix:/run/php/php8.3-fpm.sock; | |
| fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; | |
| fastcgi_intercept_errors on; | |
| fastcgi_buffer_size 16k; | |
| fastcgi_buffers 4 16k; | |
| } | |
| # Block access to sensitive files | |
| location ~ /\.ht { | |
| deny all; | |
| } | |
| location ~ /\.git { | |
| deny all; | |
| } | |
| location = /wp-config.php { | |
| deny all; | |
| } | |
| # Block xmlrpc.php (brute force target) | |
| location = /xmlrpc.php { | |
| deny all; | |
| access_log off; | |
| log_not_found off; | |
| } | |
| # Static file caching | |
| location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { | |
| expires 30d; | |
| add_header Cache-Control "public, no-transform"; | |
| access_log off; | |
| } | |
| # Deny access to wp-includes PHP files | |
| location ~* /wp-includes/.*\.php$ { | |
| deny all; | |
| } | |
| # Deny access to uploads PHP files (prevents malicious uploads from executing) | |
| location ~* /wp-content/uploads/.*\.php$ { | |
| deny all; | |
| } | |
| } |
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 | |
| # Download WordPress and set correct ownership + permissions | |
| # Run as sudo user | |
| set -euo pipefail | |
| WP_DIR="/var/www/wordpress" | |
| # Download latest WordPress | |
| cd /tmp | |
| curl -O https://wordpress.org/latest.tar.gz | |
| tar -xzf latest.tar.gz | |
| # Move to web root | |
| sudo mkdir -p "$WP_DIR" | |
| sudo cp -a /tmp/wordpress/. "$WP_DIR/" | |
| rm -rf /tmp/wordpress /tmp/latest.tar.gz | |
| # Set ownership to web server user | |
| sudo chown -R www-data:www-data "$WP_DIR" | |
| # Set directory permissions (755) and file permissions (644) | |
| sudo find "$WP_DIR" -type d -exec chmod 755 {} \; | |
| sudo find "$WP_DIR" -type f -exec chmod 644 {} \; | |
| echo "WordPress downloaded to $WP_DIR" | |
| echo "Ownership: www-data:www-data" | |
| echo "Directories: 755 | Files: 644" |
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 | |
| # Generate wp-config.php with fresh salts and your database credentials | |
| # Run as sudo user | |
| set -euo pipefail | |
| WP_DIR="/var/www/wordpress" | |
| # Your database credentials (from step 05) | |
| DB_NAME="wordpress" | |
| DB_USER="wpuser" | |
| DB_PASS="your-password-from-step-05" | |
| # Create wp-config.php from sample | |
| sudo cp "$WP_DIR/wp-config-sample.php" "$WP_DIR/wp-config.php" | |
| # Set database credentials | |
| sudo sed -i "s/database_name_here/$DB_NAME/" "$WP_DIR/wp-config.php" | |
| sudo sed -i "s/username_here/$DB_USER/" "$WP_DIR/wp-config.php" | |
| sudo sed -i "s/password_here/$DB_PASS/" "$WP_DIR/wp-config.php" | |
| # Replace placeholder salts with fresh ones from WordPress API | |
| SALTS=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/) | |
| if [ -z "$SALTS" ]; then | |
| echo "ERROR: Could not fetch salts from WordPress API" | |
| exit 1 | |
| fi | |
| # Remove existing placeholder salts and insert fresh ones | |
| sudo sed -i '/AUTH_KEY/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/SECURE_AUTH_KEY/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/LOGGED_IN_KEY/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/NONCE_KEY/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/AUTH_SALT/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/SECURE_AUTH_SALT/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/LOGGED_IN_SALT/d' "$WP_DIR/wp-config.php" | |
| sudo sed -i '/NONCE_SALT/d' "$WP_DIR/wp-config.php" | |
| # Insert salts before the "stop editing" line | |
| sudo sed -i "/stop editing/i\\ | |
| $SALTS" "$WP_DIR/wp-config.php" | |
| # Add recommended settings before "stop editing" | |
| sudo sed -i "/stop editing/i\\ | |
| /** Disable file editing from dashboard */" "$WP_DIR/wp-config.php" | |
| sudo sed -i "/stop editing/i\\ | |
| define('DISALLOW_FILE_EDIT', true);" "$WP_DIR/wp-config.php" | |
| sudo sed -i "/stop editing/i\\ | |
| " "$WP_DIR/wp-config.php" | |
| sudo sed -i "/stop editing/i\\ | |
| /** Use native cron instead of wp-cron */" "$WP_DIR/wp-config.php" | |
| sudo sed -i "/stop editing/i\\ | |
| define('DISABLE_WP_CRON', true);" "$WP_DIR/wp-config.php" | |
| # Set correct ownership | |
| sudo chown www-data:www-data "$WP_DIR/wp-config.php" | |
| sudo chmod 640 "$WP_DIR/wp-config.php" | |
| echo "wp-config.php created with:" | |
| echo " - Database credentials set" | |
| echo " - Fresh salts from WordPress API" | |
| echo " - File editing disabled" | |
| echo " - WP-Cron disabled (we'll use system cron instead)" |
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 | |
| # Install WP-CLI globally | |
| # Run as sudo user | |
| set -euo pipefail | |
| # Download WP-CLI | |
| cd /tmp | |
| curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar | |
| # Verify it works | |
| php wp-cli.phar --info | |
| # Install globally | |
| chmod +x wp-cli.phar | |
| sudo mv wp-cli.phar /usr/local/bin/wp | |
| # Verify global install | |
| wp --info | |
| # Add bash tab completion | |
| curl -o /tmp/wp-completion.bash https://raw.githubusercontent.com/wp-cli/wp-cli/v2.11.0/utils/wp-completion.bash | |
| sudo mv /tmp/wp-completion.bash /etc/bash_completion.d/wp-cli | |
| source /etc/bash_completion.d/wp-cli | |
| echo "" | |
| echo "WP-CLI installed globally. Run 'wp --info' to verify." | |
| echo "Tab completion is active — type 'wp ' and press Tab." |
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 | |
| # Run WordPress installation via WP-CLI | |
| # Run as sudo user | |
| set -euo pipefail | |
| WP_DIR="/var/www/wordpress" | |
| SITE_URL="https://yourdomain.com" | |
| SITE_TITLE="My WordPress Site" | |
| ADMIN_USER="admin" | |
| ADMIN_PASS="$(openssl rand -base64 16)" | |
| ADMIN_EMAIL="you@yourdomain.com" | |
| # Run the WordPress install | |
| sudo -u www-data wp core install \ | |
| --path="$WP_DIR" \ | |
| --url="$SITE_URL" \ | |
| --title="$SITE_TITLE" \ | |
| --admin_user="$ADMIN_USER" \ | |
| --admin_password="$ADMIN_PASS" \ | |
| --admin_email="$ADMIN_EMAIL" \ | |
| --skip-email | |
| # Set permalink structure | |
| sudo -u www-data wp rewrite structure '/%postname%/' --path="$WP_DIR" | |
| sudo -u www-data wp rewrite flush --path="$WP_DIR" | |
| # Set timezone | |
| sudo -u www-data wp option update timezone_string 'UTC' --path="$WP_DIR" | |
| # Delete default content | |
| sudo -u www-data wp post delete 1 --force --path="$WP_DIR" # Hello World | |
| sudo -u www-data wp post delete 2 --force --path="$WP_DIR" # Sample Page | |
| # Create first post | |
| sudo -u www-data wp post create \ | |
| --path="$WP_DIR" \ | |
| --post_type=post \ | |
| --post_status=publish \ | |
| --post_title="Welcome — We're Live" \ | |
| --post_content="<p>The site is up and running on our own server. More content coming soon.</p>" | |
| # Delete unused plugins and themes | |
| sudo -u www-data wp plugin delete hello --path="$WP_DIR" 2>/dev/null || true | |
| sudo -u www-data wp plugin delete akismet --path="$WP_DIR" 2>/dev/null || true | |
| sudo -u www-data wp theme delete twentytwentythree --path="$WP_DIR" 2>/dev/null || true | |
| sudo -u www-data wp theme delete twentytwentytwo --path="$WP_DIR" 2>/dev/null || true | |
| echo "" | |
| echo "WordPress installed successfully!" | |
| echo "" | |
| echo "Admin URL: $SITE_URL/wp-admin/" | |
| echo "Username: $ADMIN_USER" | |
| echo "Password: $ADMIN_PASS" | |
| echo "" | |
| echo "SAVE THESE CREDENTIALS and change the password after first login." |
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 | |
| # DNS configuration — point your domain to the droplet | |
| # Run these from your LOCAL machine (not the server) | |
| # ==================================================== | |
| # STEP 1: Set DNS records at your domain registrar | |
| # ==================================================== | |
| # | |
| # Log into your domain registrar (Namecheap, Cloudflare, GoDaddy, etc.) | |
| # and create these records: | |
| # | |
| # Type Host Value TTL | |
| # A @ YOUR_DROPLET_IP 300 | |
| # A www YOUR_DROPLET_IP 300 | |
| # | |
| # Or if you prefer, use a CNAME for www: | |
| # CNAME www yourdomain.com 300 | |
| # | |
| # If using DigitalOcean's nameservers: | |
| # Point your domain's nameservers to: | |
| # ns1.digitalocean.com | |
| # ns2.digitalocean.com | |
| # ns3.digitalocean.com | |
| # ==================================================== | |
| # STEP 2: Verify DNS propagation | |
| # ==================================================== | |
| DOMAIN="yourdomain.com" | |
| EXPECTED_IP="YOUR_DROPLET_IP" | |
| echo "Checking DNS for $DOMAIN..." | |
| echo "" | |
| # Check A record | |
| echo "A record:" | |
| dig +short A "$DOMAIN" | |
| echo "" | |
| echo "www A record:" | |
| dig +short A "www.$DOMAIN" | |
| echo "" | |
| echo "Full dig output:" | |
| dig "$DOMAIN" +noall +answer | |
| # ==================================================== | |
| # STEP 3: Wait for propagation | |
| # ==================================================== | |
| echo "" | |
| echo "If you don't see $EXPECTED_IP above, DNS hasn't propagated yet." | |
| echo "It usually takes 5-30 minutes, but can take up to 48 hours." | |
| echo "" | |
| echo "Check propagation globally at: https://dnschecker.org/#A/$DOMAIN" |
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 | |
| # Install Certbot and obtain SSL certificate via Let's Encrypt | |
| # Run as sudo user AFTER DNS is pointing to your droplet | |
| set -euo pipefail | |
| DOMAIN="yourdomain.com" | |
| EMAIL="you@yourdomain.com" | |
| # Install Certbot via snap (recommended by Let's Encrypt) | |
| sudo snap install --classic certbot | |
| sudo ln -sf /snap/bin/certbot /usr/bin/certbot | |
| # Obtain SSL certificate for both bare and www domain | |
| sudo certbot --nginx \ | |
| -d "$DOMAIN" \ | |
| -d "www.$DOMAIN" \ | |
| --non-interactive \ | |
| --agree-tos \ | |
| --email "$EMAIL" \ | |
| --redirect | |
| # Verify auto-renewal is set up | |
| sudo certbot renew --dry-run | |
| # Check certificate details | |
| echo "" | |
| echo "SSL certificate installed for $DOMAIN" | |
| echo "" | |
| echo "Certificate details:" | |
| sudo certbot certificates | |
| echo "" | |
| echo "Auto-renewal is handled by a systemd timer:" | |
| sudo systemctl list-timers | grep certbot |
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
| # Nginx server block with SSL — replaces the HTTP-only config | |
| # Certbot usually modifies your config automatically, but here's | |
| # the full production config for reference. | |
| # | |
| # Save to: /etc/nginx/sites-available/wordpress | |
| # Then: sudo nginx -t && sudo systemctl reload nginx | |
| # Redirect HTTP to HTTPS | |
| server { | |
| listen 80; | |
| listen [::]:80; | |
| server_name yourdomain.com www.yourdomain.com; | |
| return 301 https://yourdomain.com$request_uri; | |
| } | |
| # Redirect www to non-www (pick one canonical URL) | |
| server { | |
| listen 443 ssl; | |
| listen [::]:443 ssl; | |
| server_name www.yourdomain.com; | |
| ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; | |
| ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; | |
| include /etc/letsencrypt/options-ssl-nginx.conf; | |
| ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | |
| return 301 https://yourdomain.com$request_uri; | |
| } | |
| # Main server block | |
| server { | |
| listen 443 ssl; | |
| listen [::]:443 ssl; | |
| http2 on; | |
| server_name yourdomain.com; | |
| root /var/www/wordpress; | |
| index index.php index.html; | |
| # SSL | |
| ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; | |
| ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; | |
| include /etc/letsencrypt/options-ssl-nginx.conf; | |
| ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; | |
| # Security headers | |
| add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; | |
| add_header X-Frame-Options "SAMEORIGIN" always; | |
| add_header X-Content-Type-Options "nosniff" always; | |
| add_header Referrer-Policy "strict-origin-when-cross-origin" always; | |
| add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; | |
| # Logs | |
| access_log /var/log/nginx/wordpress-access.log; | |
| error_log /var/log/nginx/wordpress-error.log; | |
| client_max_body_size 64M; | |
| # WordPress permalinks | |
| location / { | |
| try_files $uri $uri/ /index.php?$args; | |
| } | |
| # PHP processing | |
| location ~ \.php$ { | |
| include snippets/fastcgi-params.conf; | |
| fastcgi_pass unix:/run/php/php8.3-fpm.sock; | |
| fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; | |
| fastcgi_intercept_errors on; | |
| fastcgi_buffer_size 16k; | |
| fastcgi_buffers 4 16k; | |
| } | |
| # Block sensitive files | |
| location ~ /\.ht { deny all; } | |
| location ~ /\.git { deny all; } | |
| location = /wp-config.php { deny all; } | |
| location = /xmlrpc.php { | |
| deny all; | |
| access_log off; | |
| log_not_found off; | |
| } | |
| # Static file caching | |
| location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { | |
| expires 30d; | |
| add_header Cache-Control "public, no-transform"; | |
| access_log off; | |
| } | |
| # Block PHP execution in uploads and includes | |
| location ~* /wp-content/uploads/.*\.php$ { deny all; } | |
| location ~* /wp-includes/.*\.php$ { deny all; } | |
| } |
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 | |
| # Install and configure Redis for WordPress object caching | |
| # Run as sudo user | |
| set -euo pipefail | |
| # Install Redis server | |
| sudo apt update | |
| sudo apt install -y redis-server | |
| # Configure Redis for WordPress | |
| sudo tee /etc/redis/redis.conf > /dev/null <<'CONF' | |
| # Redis configuration for WordPress object cache | |
| bind 127.0.0.1 ::1 | |
| port 6379 | |
| protected-mode yes | |
| # Memory management | |
| maxmemory 128mb | |
| maxmemory-policy allkeys-lru | |
| # Persistence — disable for pure cache (faster) | |
| # Enable if you want Redis data to survive reboots | |
| save "" | |
| # save 900 1 | |
| # save 300 10 | |
| # Logging | |
| loglevel notice | |
| logfile /var/log/redis/redis-server.log | |
| # Performance | |
| tcp-backlog 511 | |
| timeout 0 | |
| tcp-keepalive 300 | |
| # Snapshotting | |
| dbfilename dump.rdb | |
| dir /var/lib/redis | |
| CONF | |
| # Restart Redis with new config | |
| sudo systemctl restart redis-server | |
| sudo systemctl enable redis-server | |
| # Verify | |
| redis-cli ping | |
| redis-cli info memory | head -5 | |
| echo "" | |
| echo "Redis installed and configured." | |
| echo " Max memory: 128MB" | |
| echo " Eviction policy: allkeys-lru" | |
| echo " Listening on: 127.0.0.1:6379" |
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
| <?php | |
| /** | |
| * Redis Object Cache configuration for wp-config.php | |
| * | |
| * Add these lines to wp-config.php BEFORE "That's all, stop editing!" | |
| * Then install the Redis Object Cache plugin or the drop-in manually. | |
| */ | |
| // Redis connection settings | |
| define('WP_REDIS_HOST', '127.0.0.1'); | |
| define('WP_REDIS_PORT', 6379); | |
| define('WP_REDIS_DATABASE', 0); | |
| // Prefix to avoid collisions if multiple WP sites share one Redis | |
| define('WP_REDIS_PREFIX', 'wp_'); | |
| // Timeout settings | |
| define('WP_REDIS_TIMEOUT', 1); | |
| define('WP_REDIS_READ_TIMEOUT', 1); | |
| /** | |
| * Install the drop-in: | |
| * | |
| * Option A — Use the Redis Object Cache plugin (easiest): | |
| * wp plugin install redis-cache --activate --path=/var/www/wordpress | |
| * wp redis enable --path=/var/www/wordpress | |
| * | |
| * Option B — Manual drop-in (no plugin needed): | |
| * Download from: https://github.com/rhubarbgroup/redis-cache | |
| * Copy includes/object-cache.php to wp-content/object-cache.php | |
| * | |
| * Verify it's working: | |
| * wp redis status --path=/var/www/wordpress | |
| * # or check /wp-admin/ → Settings → Redis | |
| */ |
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
| # MySQL 8 tuning for WordPress — optimized for large sites | |
| # Save to: /etc/mysql/mysql.conf.d/wordpress-tuning.cnf | |
| # Then: sudo systemctl restart mysql | |
| # | |
| # These settings are tuned for a 2GB RAM droplet. | |
| # For 4GB+, double innodb_buffer_pool_size and max_connections. | |
| [mysqld] | |
| # === InnoDB Settings === | |
| # Buffer pool — most impactful setting. Set to 50-70% of available RAM | |
| # 2GB droplet → 768MB for buffer pool (other services need RAM too) | |
| innodb_buffer_pool_size = 768M | |
| innodb_buffer_pool_instances = 1 | |
| # Log file size — larger = better write performance, slower crash recovery | |
| innodb_log_file_size = 256M | |
| innodb_log_buffer_size = 16M | |
| # Flush method — O_DIRECT avoids double buffering with OS cache | |
| innodb_flush_method = O_DIRECT | |
| innodb_flush_log_at_trx_commit = 2 | |
| # File per table — each table gets its own .ibd file (easier management) | |
| innodb_file_per_table = 1 | |
| # IO capacity — SSD droplets can handle more | |
| innodb_io_capacity = 200 | |
| innodb_io_capacity_max = 400 | |
| # === Connection Settings === | |
| max_connections = 100 | |
| wait_timeout = 600 | |
| interactive_timeout = 600 | |
| max_allowed_packet = 64M | |
| # === Query Cache (MySQL 8 removed this — use Redis instead) === | |
| # Note: MySQL 8 does not have query_cache. Use Redis object cache. | |
| # === Temp Tables === | |
| tmp_table_size = 64M | |
| max_heap_table_size = 64M | |
| # === Table Cache === | |
| table_open_cache = 2000 | |
| table_definition_cache = 2000 | |
| # === Slow Query Log — find and fix slow queries === | |
| slow_query_log = 1 | |
| slow_query_log_file = /var/log/mysql/slow.log | |
| long_query_time = 2 | |
| log_queries_not_using_indexes = 1 | |
| # === Thread Cache === | |
| thread_cache_size = 16 | |
| # === Sort and Join Buffers === | |
| sort_buffer_size = 4M | |
| join_buffer_size = 4M | |
| read_buffer_size = 2M | |
| read_rnd_buffer_size = 4M | |
| # === Binary Logging (enable for replication or point-in-time recovery) === | |
| # Uncomment if you want binary logs: | |
| # log_bin = /var/log/mysql/mysql-bin.log | |
| # expire_logs_days = 7 | |
| # max_binlog_size = 100M | |
| # === Performance Schema (disable on small droplets to save RAM) === | |
| # performance_schema = OFF |
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
| # Nginx performance tuning for high-traffic WordPress | |
| # These settings go in /etc/nginx/nginx.conf (top-level) | |
| # and can be included in your server block. | |
| # | |
| # Save to: /etc/nginx/conf.d/performance.conf | |
| # Then: sudo nginx -t && sudo systemctl reload nginx | |
| # === Gzip Compression === | |
| gzip on; | |
| gzip_vary on; | |
| gzip_proxied any; | |
| gzip_comp_level 4; | |
| gzip_min_length 256; | |
| gzip_types | |
| text/plain | |
| text/css | |
| text/javascript | |
| application/javascript | |
| application/json | |
| application/xml | |
| application/rss+xml | |
| application/atom+xml | |
| image/svg+xml | |
| font/woff2; | |
| # === FastCGI Cache (page cache at Nginx level) === | |
| # Add this to /etc/nginx/nginx.conf in the http{} block: | |
| # | |
| # fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 | |
| # keys_zone=WORDPRESS:64m | |
| # inactive=60m | |
| # max_size=256m; | |
| # fastcgi_cache_key "$scheme$request_method$host$request_uri"; | |
| # | |
| # Then in your server block's PHP location: | |
| # | |
| # # Cache settings | |
| # fastcgi_cache WORDPRESS; | |
| # fastcgi_cache_valid 200 60m; | |
| # fastcgi_cache_valid 404 1m; | |
| # fastcgi_cache_bypass $skip_cache; | |
| # fastcgi_no_cache $skip_cache; | |
| # add_header X-FastCGI-Cache $upstream_cache_status; | |
| # | |
| # Skip cache for logged-in users, POST requests, query strings: | |
| # | |
| # set $skip_cache 0; | |
| # if ($request_method = POST) { set $skip_cache 1; } | |
| # if ($query_string != "") { set $skip_cache 1; } | |
| # if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; } | |
| # if ($request_uri ~* "/wp-admin/|/wp-json/|/xmlrpc.php") { set $skip_cache 1; } | |
| # === Buffer Tuning === | |
| client_body_buffer_size 16k; | |
| client_header_buffer_size 1k; | |
| large_client_header_buffers 4 16k; | |
| # === Timeouts === | |
| client_body_timeout 30; | |
| client_header_timeout 30; | |
| send_timeout 30; | |
| keepalive_timeout 65; | |
| keepalive_requests 100; | |
| # === Rate Limiting (protect wp-login.php) === | |
| # Add to /etc/nginx/nginx.conf in http{} block: | |
| # limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; | |
| # | |
| # Then in your server block: | |
| # location = /wp-login.php { | |
| # limit_req zone=login burst=3 nodelay; | |
| # include snippets/fastcgi-params.conf; | |
| # fastcgi_pass unix:/run/php/php8.3-fpm.sock; | |
| # fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; | |
| # } | |
| # === Open File Cache === | |
| open_file_cache max=2000 inactive=30s; | |
| open_file_cache_valid 60s; | |
| open_file_cache_min_uses 2; | |
| open_file_cache_errors on; |
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 | |
| # Security hardening: Fail2ban, auto-updates, SSH lockdown | |
| # Run as sudo user | |
| set -euo pipefail | |
| # ============================================= | |
| # 1. Install Fail2ban (blocks brute force SSH) | |
| # ============================================= | |
| sudo apt install -y fail2ban | |
| sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF' | |
| [DEFAULT] | |
| bantime = 3600 | |
| findtime = 600 | |
| maxretry = 5 | |
| backend = systemd | |
| [sshd] | |
| enabled = true | |
| port = ssh | |
| filter = sshd | |
| maxretry = 3 | |
| bantime = 86400 | |
| [nginx-http-auth] | |
| enabled = true | |
| [nginx-botsearch] | |
| enabled = true | |
| EOF | |
| sudo systemctl enable fail2ban | |
| sudo systemctl restart fail2ban | |
| echo "Fail2ban installed. SSH: 3 attempts then 24h ban." | |
| # ============================================= | |
| # 2. Enable unattended security upgrades | |
| # ============================================= | |
| sudo apt install -y unattended-upgrades apt-listchanges | |
| sudo dpkg-reconfigure -plow unattended-upgrades | |
| echo "Unattended upgrades enabled for security patches." | |
| # ============================================= | |
| # 3. SSH hardening — disable password auth | |
| # ============================================= | |
| # Only do this AFTER confirming SSH key login works! | |
| sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config | |
| sudo sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config | |
| # Disable empty passwords | |
| sudo sed -i 's/^#PermitEmptyPasswords/PermitEmptyPasswords/' /etc/ssh/sshd_config | |
| sudo sed -i 's/^PermitEmptyPasswords yes/PermitEmptyPasswords no/' /etc/ssh/sshd_config | |
| # Restart SSH | |
| sudo systemctl restart sshd | |
| echo "SSH password authentication disabled. Key-only access." | |
| # ============================================= | |
| # 4. Verify | |
| # ============================================= | |
| echo "" | |
| echo "Security summary:" | |
| echo " Fail2ban: $(sudo fail2ban-client status | head -1)" | |
| echo " Auto-updates: enabled" | |
| echo " SSH password auth: disabled" | |
| echo " Root login: disabled (from step 01)" |
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 | |
| # Install essential development and server management tools | |
| # Run as sudo user | |
| set -euo pipefail | |
| # System tools | |
| sudo apt install -y \ | |
| htop \ | |
| ncdu \ | |
| git \ | |
| zip \ | |
| unzip \ | |
| curl \ | |
| wget \ | |
| net-tools \ | |
| dnsutils \ | |
| tree | |
| echo "System tools installed: htop, ncdu, git, zip, curl, wget, tree" | |
| # Composer (PHP dependency manager) | |
| cd /tmp | |
| curl -sS https://getcomposer.org/installer | php | |
| sudo mv composer.phar /usr/local/bin/composer | |
| composer --version | |
| echo "Composer installed globally." | |
| # Node.js via nvm (for build tools, if needed) | |
| curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash | |
| # Load nvm in current session | |
| export NVM_DIR="$HOME/.nvm" | |
| [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" | |
| # Install latest LTS | |
| nvm install --lts | |
| node --version | |
| npm --version | |
| echo "Node.js LTS installed via nvm." | |
| echo "" | |
| echo "All tools installed:" | |
| echo " htop — interactive process viewer" | |
| echo " ncdu — disk usage analyzer" | |
| echo " git — version control" | |
| echo " composer — PHP packages" | |
| echo " node/npm — JS build tools (via nvm)" |
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 | |
| # Automated daily backup — database export + files tarball | |
| # Save to: /opt/backups/backup-wordpress.sh | |
| # Make executable: chmod +x /opt/backups/backup-wordpress.sh | |
| set -euo pipefail | |
| # Configuration | |
| WP_DIR="/var/www/wordpress" | |
| BACKUP_DIR="/opt/backups/wordpress" | |
| DB_NAME="wordpress" | |
| DB_USER="wpuser" | |
| DB_PASS="your-db-password" | |
| RETENTION_DAYS=14 | |
| DATE=$(date +%Y%m%d-%H%M) | |
| # Create backup directory | |
| mkdir -p "$BACKUP_DIR" | |
| # ============================================= | |
| # 1. Database backup | |
| # ============================================= | |
| DB_FILE="$BACKUP_DIR/db-$DATE.sql.gz" | |
| mysqldump -u"$DB_USER" -p"$DB_PASS" \ | |
| --single-transaction \ | |
| --routines \ | |
| --triggers \ | |
| "$DB_NAME" | gzip > "$DB_FILE" | |
| echo "Database backup: $DB_FILE ($(du -h "$DB_FILE" | cut -f1))" | |
| # ============================================= | |
| # 2. Files backup (wp-content only — core can be reinstalled) | |
| # ============================================= | |
| FILES_FILE="$BACKUP_DIR/files-$DATE.tar.gz" | |
| tar -czf "$FILES_FILE" \ | |
| -C "$WP_DIR" \ | |
| wp-content \ | |
| wp-config.php | |
| echo "Files backup: $FILES_FILE ($(du -h "$FILES_FILE" | cut -f1))" | |
| # ============================================= | |
| # 3. Clean up old backups | |
| # ============================================= | |
| find "$BACKUP_DIR" -name "db-*.sql.gz" -mtime +$RETENTION_DAYS -delete | |
| find "$BACKUP_DIR" -name "files-*.tar.gz" -mtime +$RETENTION_DAYS -delete | |
| echo "Cleaned backups older than $RETENTION_DAYS days." | |
| # ============================================= | |
| # 4. Summary | |
| # ============================================= | |
| echo "" | |
| echo "Backup complete: $DATE" | |
| echo "Backups on disk:" | |
| du -sh "$BACKUP_DIR" | |
| ls -lh "$BACKUP_DIR" | tail -6 |
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 | |
| # Set up system cron: daily backups + WP-Cron replacement | |
| # Run as sudo user | |
| set -euo pipefail | |
| BACKUP_SCRIPT="/opt/backups/backup-wordpress.sh" | |
| WP_DIR="/var/www/wordpress" | |
| # Create backup directory and script location | |
| sudo mkdir -p /opt/backups | |
| # Copy the backup script into place (if not already there) | |
| # sudo cp /path/to/22-backup-script.sh $BACKUP_SCRIPT | |
| sudo chmod +x "$BACKUP_SCRIPT" | |
| # ============================================= | |
| # 1. Daily backup at 3:00 AM server time | |
| # ============================================= | |
| (sudo crontab -l 2>/dev/null || true; echo "0 3 * * * $BACKUP_SCRIPT >> /var/log/wp-backup.log 2>&1") | sudo crontab - | |
| # ============================================= | |
| # 2. WP-Cron replacement — run every 5 minutes | |
| # ============================================= | |
| # WordPress's built-in wp-cron relies on site visits to trigger. | |
| # A system cron is more reliable — runs even with zero traffic. | |
| # (We already set DISABLE_WP_CRON in wp-config.php) | |
| (sudo crontab -u www-data -l 2>/dev/null || true; echo "*/5 * * * * cd $WP_DIR && /usr/local/bin/wp cron event run --due-now --quiet") | sudo crontab -u www-data - | |
| # ============================================= | |
| # 3. Certbot renewal check (twice daily) | |
| # ============================================= | |
| # Certbot's snap already adds a systemd timer, but just in case: | |
| (sudo crontab -l 2>/dev/null || true; echo "0 */12 * * * certbot renew --quiet --deploy-hook 'systemctl reload nginx'") | sudo crontab - | |
| # ============================================= | |
| # 4. Verify | |
| # ============================================= | |
| echo "Root crontab:" | |
| sudo crontab -l | |
| echo "" | |
| echo "www-data crontab:" | |
| sudo crontab -u www-data -l | |
| echo "" | |
| echo "Cron jobs set up:" | |
| echo " 3:00 AM daily — WordPress backup" | |
| echo " Every 5 minutes — WP-Cron events" | |
| echo " Every 12 hours — SSL certificate renewal check" |
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 | |
| # Create swap file — essential for $6 droplets with 1GB RAM | |
| # Prevents MySQL/PHP from getting OOM-killed during traffic spikes | |
| # Run as sudo user | |
| set -euo pipefail | |
| SWAP_SIZE="2G" # 2x RAM for 1GB droplet; 1x RAM for 2GB+ | |
| # Check if swap already exists | |
| if swapon --show | grep -q '/swapfile'; then | |
| echo "Swap already configured:" | |
| swapon --show | |
| exit 0 | |
| fi | |
| # Create swap file | |
| sudo fallocate -l $SWAP_SIZE /swapfile | |
| sudo chmod 600 /swapfile | |
| sudo mkswap /swapfile | |
| sudo swapon /swapfile | |
| # Make permanent (survives reboot) | |
| echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab | |
| # Tune swappiness — lower value means Linux prefers RAM over swap | |
| # 10 is good for servers: only swap when RAM is nearly full | |
| sudo sysctl vm.swappiness=10 | |
| echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf | |
| # Tune cache pressure | |
| sudo sysctl vm.vfs_cache_pressure=50 | |
| echo 'vm.vfs_cache_pressure=50' | sudo tee -a /etc/sysctl.conf | |
| # Verify | |
| echo "" | |
| echo "Swap configured:" | |
| swapon --show | |
| echo "" | |
| free -h |
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 | |
| # Final verification checklist — run after completing all setup steps | |
| # This checks every component and reports pass/fail | |
| # Run as sudo user | |
| set -euo pipefail | |
| DOMAIN="yourdomain.com" | |
| WP_DIR="/var/www/wordpress" | |
| PASS=0 | |
| FAIL=0 | |
| check() { | |
| local name="$1" | |
| local result="$2" | |
| if [ "$result" = "0" ]; then | |
| echo " [PASS] $name" | |
| PASS=$((PASS + 1)) | |
| else | |
| echo " [FAIL] $name" | |
| FAIL=$((FAIL + 1)) | |
| fi | |
| } | |
| echo "================================================" | |
| echo " WordPress Server Verification Checklist" | |
| echo "================================================" | |
| echo "" | |
| # 1. Nginx running | |
| echo "--- Web Server ---" | |
| systemctl is-active nginx > /dev/null 2>&1 | |
| check "Nginx is running" $? | |
| # 2. PHP-FPM running | |
| systemctl is-active php8.3-fpm > /dev/null 2>&1 | |
| check "PHP-FPM is running" $? | |
| # 3. MySQL running | |
| systemctl is-active mysql > /dev/null 2>&1 | |
| check "MySQL is running" $? | |
| # 4. Redis running | |
| echo "" | |
| echo "--- Services ---" | |
| systemctl is-active redis-server > /dev/null 2>&1 | |
| check "Redis is running" $? | |
| redis-cli ping 2>/dev/null | grep -q PONG | |
| check "Redis responds to PING" $? | |
| # 5. Firewall active | |
| echo "" | |
| echo "--- Security ---" | |
| sudo ufw status | grep -q "Status: active" | |
| check "UFW firewall is active" $? | |
| systemctl is-active fail2ban > /dev/null 2>&1 | |
| check "Fail2ban is running" $? | |
| grep -q "PermitRootLogin no" /etc/ssh/sshd_config | |
| check "Root SSH login disabled" $? | |
| grep -q "PasswordAuthentication no" /etc/ssh/sshd_config | |
| check "SSH password auth disabled" $? | |
| # 6. WordPress | |
| echo "" | |
| echo "--- WordPress ---" | |
| sudo -u www-data wp core is-installed --path="$WP_DIR" 2>/dev/null | |
| check "WordPress is installed" $? | |
| sudo -u www-data wp core version --path="$WP_DIR" > /dev/null 2>&1 | |
| check "WP-CLI can connect" $? | |
| # Check Redis object cache | |
| sudo -u www-data wp redis status --path="$WP_DIR" 2>/dev/null | grep -q "Status: Connected" | |
| check "Redis object cache connected" $? || true | |
| # 7. File permissions | |
| echo "" | |
| echo "--- Permissions ---" | |
| OWNER=$(stat -c '%U' "$WP_DIR/wp-config.php") | |
| [ "$OWNER" = "www-data" ] | |
| check "wp-config.php owned by www-data" $? | |
| PERMS=$(stat -c '%a' "$WP_DIR/wp-config.php") | |
| [ "$PERMS" = "640" ] | |
| check "wp-config.php permissions 640" $? | |
| # 8. SSL | |
| echo "" | |
| echo "--- SSL ---" | |
| if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then | |
| check "SSL certificate exists" 0 | |
| EXPIRY=$(sudo openssl x509 -enddate -noout -in "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" | cut -d= -f2) | |
| echo " Certificate expires: $EXPIRY" | |
| else | |
| check "SSL certificate exists" 1 | |
| fi | |
| sudo certbot renew --dry-run > /dev/null 2>&1 | |
| check "Certbot auto-renewal works" $? | |
| # 9. Swap | |
| echo "" | |
| echo "--- System ---" | |
| swapon --show | grep -q "swapfile" | |
| check "Swap file configured" $? | |
| # 10. Cron jobs | |
| sudo crontab -l 2>/dev/null | grep -q "backup-wordpress" | |
| check "Backup cron job exists" $? | |
| sudo crontab -u www-data -l 2>/dev/null | grep -q "wp cron" | |
| check "WP-Cron replacement active" $? | |
| # Summary | |
| echo "" | |
| echo "================================================" | |
| echo " Results: $PASS passed, $FAIL failed" | |
| echo "================================================" | |
| if [ "$FAIL" -gt 0 ]; then | |
| echo "" | |
| echo "Fix the failed checks above before going live." | |
| exit 1 | |
| else | |
| echo "" | |
| echo "All checks passed. Your WordPress server is ready." | |
| echo "" | |
| echo "Next steps:" | |
| echo " 1. Log in at https://$DOMAIN/wp-admin/" | |
| echo " 2. Install your theme" | |
| echo " 3. Configure plugins" | |
| echo " 4. Test SSL at https://www.ssllabs.com/ssltest/analyze.html?d=$DOMAIN" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment