Skip to content

Instantly share code, notes, and snippets.

@vapvarun
Created February 23, 2026 20:01
Show Gist options
  • Select an option

  • Save vapvarun/649b44c1a00d233ebd7faec32a73169d to your computer and use it in GitHub Desktop.

Select an option

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)
#!/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)"
#!/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."
#!/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."
#!/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
#!/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."
#!/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."
; 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
# 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;
}
}
#!/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"
#!/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)"
#!/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."
#!/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."
#!/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"
#!/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
# 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; }
}
#!/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"
<?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
*/
# 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
# 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;
#!/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)"
#!/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)"
#!/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
#!/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"
#!/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
#!/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