Running WordPress on ARM-based Graviton instances delivers up to 40% better price-performance compared to x86 equivalents. This guide provides production-ready scripts to deploy an optimised WordPress stack in minutes, plus everything you need to migrate your existing site.
Why Graviton for WordPress?
Graviton3 processors deliver:
- 40% better price-performance vs comparable x86 instances
- Up to 25% lower cost for equivalent workloads
- 60% less energy consumption per compute hour
- Native ARM64 optimisations for PHP 8.x and MariaDB
The t4g.small instance (2 vCPU, 2GB RAM) at ~$12/month handles most WordPress sites comfortably. For higher traffic, t4g.medium or c7g instances scale beautifully.
Architecture
┌─────────────────────────────────────────────────┐
│ CloudFront │
│ (Optional CDN Layer) │
└─────────────────────┬───────────────────────────┘
│
┌─────────────────────▼───────────────────────────┐
│ Graviton EC2 Instance │
│ ┌─────────────────────────────────────────────┐│
│ │ Caddy (Reverse Proxy) ││
│ │ Auto-TLS, HTTP/2, Compression ││
│ └─────────────────────┬───────────────────────┘│
│ │ │
│ ┌─────────────────────▼───────────────────────┐│
│ │ PHP-FPM 8.3 ││
│ │ OPcache, JIT Compilation ││
│ └─────────────────────┬───────────────────────┘│
│ │ │
│ ┌─────────────────────▼───────────────────────┐│
│ │ MariaDB 10.11 ││
│ │ InnoDB Optimised, Query Cache ││
│ └─────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ EBS gp3 Volume ││
│ │ 3000 IOPS, 125 MB/s baseline ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
Prerequisites
- AWS CLI configured with appropriate permissions
- A domain name with DNS you control
- SSH key pair in your target region
If you’d prefer to download these scripts, check out https://github.com/Scr1ptW0lf/wordpress-graviton.
Part 1: Launch the Instance
Save this as launch-graviton-wp.sh and run from AWS CloudShell:
#!/bin/bash
# AWS EC2 ARM Instance Launch Script with Elastic IP
# Launches ARM-based instances with Ubuntu 24.04 LTS ARM64
set -e
echo "=== AWS EC2 ARM Ubuntu Instance Launcher ==="
echo ""
# Function to get Ubuntu 24.04 ARM64 AMI for a region
get_ubuntu_ami() {
local region=$1
# Get the latest Ubuntu 24.04 LTS ARM64 AMI
aws ec2 describe-images \
--region "$region" \
--owners 099720109477 \
--filters "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-arm64-server-*" \
"Name=state,Values=available" \
--query 'Images | sort_by(@, &CreationDate) | [-1].ImageId' \
--output text
}
# Check for default region
if [ -n "$AWS_DEFAULT_REGION" ]; then
echo "AWS default region detected: $AWS_DEFAULT_REGION"
read -p "Use this region? (y/n, default: y): " use_default
use_default=${use_default:-y}
if [ "$use_default" == "y" ]; then
REGION="$AWS_DEFAULT_REGION"
echo "Using region: $REGION"
else
use_default="n"
fi
else
use_default="n"
fi
# Prompt for region if not using default
if [ "$use_default" == "n" ]; then
echo ""
echo "Available regions for ARM instances:"
echo "1. us-east-1 (N. Virginia)"
echo "2. us-east-2 (Ohio)"
echo "3. us-west-2 (Oregon)"
echo "4. eu-west-1 (Ireland)"
echo "5. eu-central-1 (Frankfurt)"
echo "6. ap-southeast-1 (Singapore)"
echo "7. ap-northeast-1 (Tokyo)"
echo "8. Enter custom region"
echo ""
read -p "Select region (1-8): " region_choice
case $region_choice in
1) REGION="us-east-1" ;;
2) REGION="us-east-2" ;;
3) REGION="us-west-2" ;;
4) REGION="eu-west-1" ;;
5) REGION="eu-central-1" ;;
6) REGION="ap-southeast-1" ;;
7) REGION="ap-northeast-1" ;;
8) read -p "Enter region code: " REGION ;;
*) echo "Invalid choice"; exit 1 ;;
esac
echo "Selected region: $REGION"
fi
# Prompt for instance type
echo ""
echo "Select instance type (ARM/Graviton):"
echo "1. t4g.micro (2 vCPU, 1 GB RAM) - Free tier eligible"
echo "2. t4g.small (2 vCPU, 2 GB RAM) - ~\$0.0168/hr"
echo "3. t4g.medium (2 vCPU, 4 GB RAM) - ~\$0.0336/hr"
echo "4. t4g.large (2 vCPU, 8 GB RAM) - ~\$0.0672/hr"
echo "5. t4g.xlarge (4 vCPU, 16 GB RAM) - ~\$0.1344/hr"
echo "6. t4g.2xlarge (8 vCPU, 32 GB RAM) - ~\$0.2688/hr"
echo "7. Enter custom ARM instance type"
echo ""
read -p "Select instance type (1-7): " instance_choice
case $instance_choice in
1) INSTANCE_TYPE="t4g.micro" ;;
2) INSTANCE_TYPE="t4g.small" ;;
3) INSTANCE_TYPE="t4g.medium" ;;
4) INSTANCE_TYPE="t4g.large" ;;
5) INSTANCE_TYPE="t4g.xlarge" ;;
6) INSTANCE_TYPE="t4g.2xlarge" ;;
7) read -p "Enter instance type (e.g., c7g.medium): " INSTANCE_TYPE ;;
*) echo "Invalid choice"; exit 1 ;;
esac
echo "Selected instance type: $INSTANCE_TYPE"
echo ""
echo "Fetching latest Ubuntu 24.04 ARM64 AMI..."
AMI_ID=$(get_ubuntu_ami "$REGION")
if [ -z "$AMI_ID" ]; then
echo "Error: Could not find Ubuntu ARM64 AMI in region $REGION"
exit 1
fi
echo "Found AMI: $AMI_ID"
echo ""
# List existing key pairs
echo "Fetching existing key pairs in $REGION..."
EXISTING_KEYS=$(aws ec2 describe-key-pairs \
--region "$REGION" \
--query 'KeyPairs[*].KeyName' \
--output text 2>/dev/null || echo "")
if [ -n "$EXISTING_KEYS" ]; then
echo "Existing key pairs in $REGION:"
# Convert to array for number selection
mapfile -t KEY_ARRAY < <(echo "$EXISTING_KEYS" | tr '\t' '\n')
for i in "${!KEY_ARRAY[@]}"; do
echo "$((i+1)). ${KEY_ARRAY[$i]}"
done
echo ""
else
echo "No existing key pairs found in $REGION"
echo ""
fi
# Prompt for key pair
read -p "Enter key pair name, number to select from list, or press Enter to create new: " KEY_INPUT
CREATE_NEW_KEY=false
if [ -z "$KEY_INPUT" ]; then
KEY_NAME="arm-key-$(date +%s)"
CREATE_NEW_KEY=true
echo "Will create new key pair: $KEY_NAME"
elif [[ "$KEY_INPUT" =~ ^[0-9]+$ ]] && [ -n "$EXISTING_KEYS" ]; then
# User entered a number
if [ "$KEY_INPUT" -ge 1 ] && [ "$KEY_INPUT" -le "${#KEY_ARRAY[@]}" ]; then
KEY_NAME="${KEY_ARRAY[$((KEY_INPUT-1))]}"
echo "Will use existing key pair: $KEY_NAME"
else
echo "Invalid selection number"
exit 1
fi
else
KEY_NAME="$KEY_INPUT"
echo "Will use existing key pair: $KEY_NAME"
fi
echo ""
# List existing security groups
echo "Fetching existing security groups in $REGION..."
EXISTING_SGS=$(aws ec2 describe-security-groups \
--region "$REGION" \
--query 'SecurityGroups[*].[GroupId,GroupName,Description]' \
--output text 2>/dev/null || echo "")
if [ -n "$EXISTING_SGS" ]; then
echo "Existing security groups in $REGION:"
# Convert to arrays for number selection
mapfile -t SG_LINES < <(echo "$EXISTING_SGS")
declare -a SG_ID_ARRAY
declare -a SG_NAME_ARRAY
declare -a SG_DESC_ARRAY
for line in "${SG_LINES[@]}"; do
read -r sg_id sg_name sg_desc <<< "$line"
SG_ID_ARRAY+=("$sg_id")
SG_NAME_ARRAY+=("$sg_name")
SG_DESC_ARRAY+=("$sg_desc")
done
for i in "${!SG_ID_ARRAY[@]}"; do
echo "$((i+1)). ${SG_ID_ARRAY[$i]} - ${SG_NAME_ARRAY[$i]} (${SG_DESC_ARRAY[$i]})"
done
echo ""
else
echo "No existing security groups found in $REGION"
echo ""
fi
# Prompt for security group
read -p "Enter security group ID, number to select from list, or press Enter to create new: " SG_INPUT
CREATE_NEW_SG=false
if [ -z "$SG_INPUT" ]; then
SG_NAME="arm-sg-$(date +%s)"
CREATE_NEW_SG=true
echo "Will create new security group: $SG_NAME"
echo " - Port 22 (SSH) - open to 0.0.0.0/0"
echo " - Port 80 (HTTP) - open to 0.0.0.0/0"
echo " - Port 443 (HTTPS) - open to 0.0.0.0/0"
elif [[ "$SG_INPUT" =~ ^[0-9]+$ ]] && [ -n "$EXISTING_SGS" ]; then
# User entered a number
if [ "$SG_INPUT" -ge 1 ] && [ "$SG_INPUT" -le "${#SG_ID_ARRAY[@]}" ]; then
SG_ID="${SG_ID_ARRAY[$((SG_INPUT-1))]}"
echo "Will use existing security group: $SG_ID (${SG_NAME_ARRAY[$((SG_INPUT-1))]})"
echo "Note: Ensure ports 22, 80, and 443 are open if needed"
else
echo "Invalid selection number"
exit 1
fi
else
SG_ID="$SG_INPUT"
echo "Will use existing security group: $SG_ID"
echo "Note: Ensure ports 22, 80, and 443 are open if needed"
fi
echo ""
# Prompt for Elastic IP
read -p "Allocate and assign an Elastic IP? (y/n, default: n): " ALLOCATE_EIP
ALLOCATE_EIP=${ALLOCATE_EIP:-n}
echo ""
read -p "Enter instance name tag (default: ubuntu-arm-instance): " INSTANCE_NAME
INSTANCE_NAME=${INSTANCE_NAME:-ubuntu-arm-instance}
echo ""
echo "=== Launch Configuration ==="
echo "Region: $REGION"
echo "Instance Type: $INSTANCE_TYPE"
echo "AMI: $AMI_ID (Ubuntu 24.04 ARM64)"
echo "Key Pair: $KEY_NAME $([ "$CREATE_NEW_KEY" == true ] && echo '(will be created)')"
echo "Security Group: $([ "$CREATE_NEW_SG" == true ] && echo "$SG_NAME (will be created)" || echo "$SG_ID")"
echo "Name: $INSTANCE_NAME"
echo "Elastic IP: $([ "$ALLOCATE_EIP" == "y" ] && echo 'Yes' || echo 'No')"
echo ""
read -p "Launch instance? (y/n, default: y): " CONFIRM
CONFIRM=${CONFIRM:-y}
if [ "$CONFIRM" != "y" ]; then
echo "Launch cancelled"
exit 0
fi
echo ""
echo "Starting launch process..."
# Create key pair if needed
if [ "$CREATE_NEW_KEY" == true ]; then
echo ""
echo "Creating key pair: $KEY_NAME"
aws ec2 create-key-pair \
--region "$REGION" \
--key-name "$KEY_NAME" \
--query 'KeyMaterial' \
--output text > "${KEY_NAME}.pem"
chmod 400 "${KEY_NAME}.pem"
echo " ✓ Key saved to: ${KEY_NAME}.pem"
echo " ⚠️ IMPORTANT: Download this key file from CloudShell if you need it elsewhere!"
fi
# Create security group if needed
if [ "$CREATE_NEW_SG" == true ]; then
echo ""
echo "Creating security group: $SG_NAME"
# Get default VPC
VPC_ID=$(aws ec2 describe-vpcs \
--region "$REGION" \
--filters "Name=isDefault,Values=true" \
--query 'Vpcs[0].VpcId' \
--output text)
if [ -z "$VPC_ID" ] || [ "$VPC_ID" == "None" ]; then
echo "Error: No default VPC found. Please specify a security group ID."
exit 1
fi
SG_ID=$(aws ec2 create-security-group \
--region "$REGION" \
--group-name "$SG_NAME" \
--description "Security group for ARM instance with web access" \
--vpc-id "$VPC_ID" \
--query 'GroupId' \
--output text)
echo " ✓ Created security group: $SG_ID"
echo " Adding security rules..."
# Add SSH rule
aws ec2 authorize-security-group-ingress \
--region "$REGION" \
--group-id "$SG_ID" \
--ip-permissions \
IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges="[{CidrIp=0.0.0.0/0,Description='SSH'}]" \
> /dev/null
# Add HTTP rule
aws ec2 authorize-security-group-ingress \
--region "$REGION" \
--group-id "$SG_ID" \
--ip-permissions \
IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges="[{CidrIp=0.0.0.0/0,Description='HTTP'}]" \
> /dev/null
# Add HTTPS rule
aws ec2 authorize-security-group-ingress \
--region "$REGION" \
--group-id "$SG_ID" \
--ip-permissions \
IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges="[{CidrIp=0.0.0.0/0,Description='HTTPS'}]" \
> /dev/null
echo " ✓ Port 22 (SSH) configured"
echo " ✓ Port 80 (HTTP) configured"
echo " ✓ Port 443 (HTTPS) configured"
fi
echo ""
echo "Launching instance..."
INSTANCE_ID=$(aws ec2 run-instances \
--region "$REGION" \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--key-name "$KEY_NAME" \
--security-group-ids "$SG_ID" \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$INSTANCE_NAME}]" \
--query 'Instances[0].InstanceId' \
--output text)
echo " ✓ Instance launched: $INSTANCE_ID"
echo " Waiting for instance to be running..."
aws ec2 wait instance-running \
--region "$REGION" \
--instance-ids "$INSTANCE_ID"
echo " ✓ Instance is running!"
# Handle Elastic IP
if [ "$ALLOCATE_EIP" == "y" ]; then
echo ""
echo "Allocating Elastic IP..."
ALLOCATION_OUTPUT=$(aws ec2 allocate-address \
--region "$REGION" \
--domain vpc \
--tag-specifications "ResourceType=elastic-ip,Tags=[{Key=Name,Value=$INSTANCE_NAME-eip}]")
ALLOCATION_ID=$(echo "$ALLOCATION_OUTPUT" | grep -o '"AllocationId": "[^"]*' | cut -d'"' -f4)
ELASTIC_IP=$(echo "$ALLOCATION_OUTPUT" | grep -o '"PublicIp": "[^"]*' | cut -d'"' -f4)
echo " ✓ Elastic IP allocated: $ELASTIC_IP"
echo " Associating Elastic IP with instance..."
ASSOCIATION_ID=$(aws ec2 associate-address \
--region "$REGION" \
--instance-id "$INSTANCE_ID" \
--allocation-id "$ALLOCATION_ID" \
--query 'AssociationId' \
--output text)
echo " ✓ Elastic IP associated"
PUBLIC_IP=$ELASTIC_IP
else
PUBLIC_IP=$(aws ec2 describe-instances \
--region "$REGION" \
--instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].PublicIpAddress' \
--output text)
fi
echo ""
echo "=========================================="
echo "=== Instance Ready ==="
echo "=========================================="
echo "Instance ID: $INSTANCE_ID"
echo "Instance Type: $INSTANCE_TYPE"
echo "Public IP: $PUBLIC_IP"
if [ "$ALLOCATE_EIP" == "y" ]; then
echo "Elastic IP: Yes (IP will persist after stop/start)"
echo "Allocation ID: $ALLOCATION_ID"
else
echo "Elastic IP: No (IP will change if instance is stopped)"
fi
echo "Region: $REGION"
echo "Security: SSH (22), HTTP (80), HTTPS (443) open"
echo ""
echo "Connect with:"
echo " ssh -i ${KEY_NAME}.pem ubuntu@${PUBLIC_IP}"
echo ""
echo "Test web access:"
echo " curl http://${PUBLIC_IP}"
echo ""
echo "⏱️ Wait 30-60 seconds for SSH to become available"
if [ "$ALLOCATE_EIP" == "y" ]; then
echo ""
echo "=========================================="
echo "⚠️ ELASTIC IP WARNING"
echo "=========================================="
echo "Elastic IPs cost \$0.005/hour when NOT"
echo "associated with a running instance!"
echo ""
echo "To avoid charges, release the EIP if you"
echo "delete the instance:"
echo ""
echo "aws ec2 release-address \\"
echo " --region $REGION \\"
echo " --allocation-id $ALLOCATION_ID"
fi
echo ""
echo "=========================================="
Run it:
chmod +x launch-graviton-wp.sh
./launch-graviton-wp.sh
Part 2: Install WordPress Stack
SSH into your new instance and save this as setup-wordpress.sh:
#!/bin/bash
# WordPress Installation Script for Ubuntu 24.04 ARM64
# Installs Apache, MySQL, PHP, and WordPress with automatic configuration
set -e
echo "=== WordPress Installation Script (Apache) ==="
echo "This script will install and configure:"
echo " - Apache web server"
echo " - MySQL database"
echo " - PHP 8.3"
echo " - WordPress (latest version)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (use: sudo bash $0)"
exit 1
fi
# Get configuration from user
echo "=== WordPress Configuration ==="
read -p "Enter your domain name (or press Enter to use server IP): " DOMAIN_NAME
read -p "Enter WordPress site title (default: My WordPress Site): " SITE_TITLE
SITE_TITLE=${SITE_TITLE:-My WordPress Site}
read -p "Enter WordPress admin username (default: admin): " WP_ADMIN_USER
WP_ADMIN_USER=${WP_ADMIN_USER:-admin}
read -sp "Enter WordPress admin password (or press Enter to generate): " WP_ADMIN_PASS
echo ""
if [ -z "$WP_ADMIN_PASS" ]; then
WP_ADMIN_PASS=$(openssl rand -base64 16)
echo "Generated password: $WP_ADMIN_PASS"
fi
read -p "Enter WordPress admin email: (default:test@example.com)" WP_ADMIN_EMAIL
WP_ADMIN_EMAIL=${WP_ADMIN_EMAIL:-test@example.com}
# Generate database credentials
DB_NAME="wordpress"
DB_USER="wpuser"
DB_PASS=$(openssl rand -base64 16)
DB_ROOT_PASS=$(openssl rand -base64 16)
echo ""
echo "=== Installation Summary ==="
echo "Domain: ${DOMAIN_NAME:-Server IP}"
echo "Site Title: $SITE_TITLE"
echo "Admin User: $WP_ADMIN_USER"
echo "Admin Email: $WP_ADMIN_EMAIL"
echo "Database: $DB_NAME"
echo ""
read -p "Proceed with installation? (y/n, default: y): " CONFIRM
CONFIRM=${CONFIRM:-y}
if [ "$CONFIRM" != "y" ]; then
echo "Installation cancelled"
exit 0
fi
echo ""
echo "Starting installation..."
# Update system
echo ""
echo "[1/8] Updating system packages..."
apt-get update -qq
apt-get upgrade -y -qq
# Install Apache
echo ""
echo "[2/8] Installing Apache..."
apt-get install -y -qq apache2
# Enable Apache modules
echo "Enabling Apache modules..."
a2enmod rewrite
a2enmod ssl
a2enmod headers
# Check if MySQL is already installed
MYSQL_INSTALLED=false
if systemctl is-active --quiet mysql || systemctl is-active --quiet mysqld; then
MYSQL_INSTALLED=true
echo ""
echo "MySQL is already installed and running."
elif command -v mysql &> /dev/null; then
MYSQL_INSTALLED=true
echo ""
echo "MySQL is already installed."
fi
if [ "$MYSQL_INSTALLED" = true ]; then
echo ""
echo "[3/8] Using existing MySQL installation..."
read -sp "Enter MySQL root password (or press Enter to try without password): " EXISTING_ROOT_PASS
echo ""
MYSQL_CONNECTION_OK=false
# Test the password
if [ -n "$EXISTING_ROOT_PASS" ]; then
if mysql -u root -p"${EXISTING_ROOT_PASS}" -e "SELECT 1;" &> /dev/null; then
echo "Successfully connected to MySQL."
DB_ROOT_PASS="$EXISTING_ROOT_PASS"
MYSQL_CONNECTION_OK=true
else
echo "Error: Could not connect to MySQL with provided password."
fi
fi
# Try without password if previous attempt failed or no password was provided
if [ "$MYSQL_CONNECTION_OK" = false ]; then
echo "Trying to connect without password..."
if mysql -u root -e "SELECT 1;" &> /dev/null; then
echo "Connected without password. Will set a password now."
DB_ROOT_PASS=$(openssl rand -base64 16)
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${DB_ROOT_PASS}';"
echo "New root password set: $DB_ROOT_PASS"
MYSQL_CONNECTION_OK=true
fi
fi
# If still cannot connect, offer to reinstall
if [ "$MYSQL_CONNECTION_OK" = false ]; then
echo ""
echo "ERROR: Cannot connect to MySQL with any method."
echo "This usually means MySQL is in an inconsistent state."
echo ""
read -p "Remove and reinstall MySQL? (y/n, default: y): " REINSTALL_MYSQL
REINSTALL_MYSQL=${REINSTALL_MYSQL:-y}
if [ "$REINSTALL_MYSQL" = "y" ]; then
echo ""
echo "Removing MySQL..."
systemctl stop mysql 2>/dev/null || systemctl stop mysqld 2>/dev/null || true
apt-get remove --purge -y mysql-server mysql-client mysql-common mysql-server-core-* mysql-client-core-* -qq
apt-get autoremove -y -qq
apt-get autoclean -qq
rm -rf /etc/mysql /var/lib/mysql /var/log/mysql
echo "Reinstalling MySQL..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq mysql-server
# Generate new root password
DB_ROOT_PASS=$(openssl rand -base64 16)
# Set root password and secure installation
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${DB_ROOT_PASS}';"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.user WHERE User='';"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"
mysql -u root -p"${DB_ROOT_PASS}" -e "DROP DATABASE IF EXISTS test;"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';"
mysql -u root -p"${DB_ROOT_PASS}" -e "FLUSH PRIVILEGES;"
echo "MySQL reinstalled successfully."
echo "New root password: $DB_ROOT_PASS"
else
echo "Installation cancelled."
exit 1
fi
fi
else
# Install MySQL
echo ""
echo "[3/8] Installing MySQL..."
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq mysql-server
# Secure MySQL installation
echo ""
echo "[4/8] Configuring MySQL..."
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${DB_ROOT_PASS}';"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.user WHERE User='';"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"
mysql -u root -p"${DB_ROOT_PASS}" -e "DROP DATABASE IF EXISTS test;"
mysql -u root -p"${DB_ROOT_PASS}" -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';"
mysql -u root -p"${DB_ROOT_PASS}" -e "FLUSH PRIVILEGES;"
fi
# Check if WordPress database already exists
echo ""
echo "[4/8] Setting up WordPress database..."
# Create MySQL defaults file for safer password handling
MYSQL_CNF=$(mktemp)
cat > "$MYSQL_CNF" <<EOF
[client]
user=root
password=${DB_ROOT_PASS}
EOF
chmod 600 "$MYSQL_CNF"
# Test MySQL connection first
echo "Testing MySQL connection..."
if ! mysql --defaults-extra-file="$MYSQL_CNF" -e "SELECT 1;" &> /dev/null; then
echo "ERROR: Cannot connect to MySQL to create database."
rm -f "$MYSQL_CNF"
exit 1
fi
echo "MySQL connection successful."
# Check if database exists
echo "Checking for existing database '${DB_NAME}'..."
DB_EXISTS=$(mysql --defaults-extra-file="$MYSQL_CNF" -e "SHOW DATABASES LIKE '${DB_NAME}';" 2>/dev/null | grep -c "${DB_NAME}" || true)
if [ "$DB_EXISTS" -gt 0 ]; then
echo ""
echo "WARNING: Database '${DB_NAME}' already exists!"
read -p "Delete existing database and create fresh? (y/n, default: n): " DELETE_DB
DELETE_DB=${DELETE_DB:-n}
if [ "$DELETE_DB" = "y" ]; then
echo "Dropping existing database..."
mysql --defaults-extra-file="$MYSQL_CNF" -e "DROP DATABASE ${DB_NAME};"
echo "Creating fresh WordPress database..."
mysql --defaults-extra-file="$MYSQL_CNF" <<EOF
CREATE DATABASE ${DB_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EOF
else
echo "Using existing database '${DB_NAME}'."
fi
else
echo "Creating WordPress database..."
mysql --defaults-extra-file="$MYSQL_CNF" <<EOF
CREATE DATABASE ${DB_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EOF
echo "Database created successfully."
fi
# Check if WordPress user already exists
echo "Checking for existing database user '${DB_USER}'..."
USER_EXISTS=$(mysql --defaults-extra-file="$MYSQL_CNF" -e "SELECT User FROM mysql.user WHERE User='${DB_USER}';" 2>/dev/null | grep -c "${DB_USER}" || true)
if [ "$USER_EXISTS" -gt 0 ]; then
echo "Database user '${DB_USER}' already exists. Updating password and permissions..."
mysql --defaults-extra-file="$MYSQL_CNF" <<EOF
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
EOF
echo "User updated successfully."
else
echo "Creating WordPress database user..."
mysql --defaults-extra-file="$MYSQL_CNF" <<EOF
CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
EOF
echo "User created successfully."
fi
echo "Database setup complete."
rm -f "$MYSQL_CNF"
# Install PHP
echo ""
echo "[5/8] Installing PHP and extensions..."
apt-get install -y -qq php8.3 php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring \
php8.3-xml php8.3-xmlrpc php8.3-soap php8.3-intl php8.3-zip libapache2-mod-php8.3 php8.3-imagick
# Configure PHP
echo "Configuring PHP..."
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.3/apache2/php.ini
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/8.3/apache2/php.ini
sed -i 's/max_execution_time = .*/max_execution_time = 300/' /etc/php/8.3/apache2/php.ini
# Check if WordPress directory already exists
if [ -d "/var/www/html/wordpress" ]; then
echo ""
echo "WARNING: WordPress directory /var/www/html/wordpress already exists!"
read -p "Delete existing WordPress installation? (y/n, default: n): " DELETE_WP
DELETE_WP=${DELETE_WP:-n}
if [ "$DELETE_WP" = "y" ]; then
echo "Removing existing WordPress directory..."
rm -rf /var/www/html/wordpress
fi
fi
# Download WordPress
echo ""
echo "[6/8] Downloading WordPress..."
cd /tmp
wget -q https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
mv wordpress /var/www/html/
chown -R www-data:www-data /var/www/html/wordpress
rm -f latest.tar.gz
# Configure WordPress
echo ""
echo "[7/8] Configuring WordPress..."
cd /var/www/html/wordpress
# Generate WordPress salts
SALTS=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/)
# Create wp-config.php
cat > wp-config.php <<EOF
<?php
define( 'DB_NAME', '${DB_NAME}' );
define( 'DB_USER', '${DB_USER}' );
define( 'DB_PASSWORD', '${DB_PASS}' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
${SALTS}
\$table_prefix = 'wp_';
define( 'WP_DEBUG', false );
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';
EOF
chown www-data:www-data wp-config.php
chmod 640 wp-config.php
# Configure Apache
echo ""
echo "[8/8] Configuring Apache..."
# Determine server name
if [ -z "$DOMAIN_NAME" ]; then
# Try to get EC2 public IP first
SERVER_NAME=$(curl -s --connect-timeout 5 http://169.254.169.254/latest/meta-data/public-ipv4 2>/dev/null)
# If we got a valid public IP, use it
if [ -n "$SERVER_NAME" ] && [[ ! "$SERVER_NAME" =~ ^172\. ]] && [[ ! "$SERVER_NAME" =~ ^10\. ]] && [[ ! "$SERVER_NAME" =~ ^192\.168\. ]]; then
echo "Detected EC2 public IP: $SERVER_NAME"
else
# Fallback: try to get public IP from external service
echo "EC2 metadata not available, trying external service..."
SERVER_NAME=$(curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || curl -s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
if [ -n "$SERVER_NAME" ]; then
echo "Detected public IP from external service: $SERVER_NAME"
else
# Last resort: use local IP (but warn user)
SERVER_NAME=$(hostname -I | awk '{print $1}')
echo "WARNING: Using local IP address: $SERVER_NAME"
echo "This is a private IP and won't be accessible from the internet."
echo "Consider specifying a domain name or public IP."
fi
fi
else
SERVER_NAME="$DOMAIN_NAME"
echo "Using provided domain: $SERVER_NAME"
fi
# Create Apache virtual host
cat > /etc/apache2/sites-available/wordpress.conf <<EOF
<VirtualHost *:80>
ServerName ${SERVER_NAME}
ServerAdmin ${WP_ADMIN_EMAIL}
DocumentRoot /var/www/html/wordpress
<Directory /var/www/html/wordpress>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/wordpress-error.log
CustomLog \${APACHE_LOG_DIR}/wordpress-access.log combined
</VirtualHost>
EOF
# Enable WordPress site
echo "Enabling WordPress site..."
a2ensite wordpress.conf
# Disable default site if it exists
if [ -f /etc/apache2/sites-enabled/000-default.conf ]; then
echo "Disabling default site..."
a2dissite 000-default.conf
fi
# Test Apache configuration
echo ""
echo "Testing Apache configuration..."
if ! apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then
echo "ERROR: Apache configuration test failed!"
apache2ctl configtest
exit 1
fi
echo "Apache configuration is valid."
# Restart Apache
echo "Restarting Apache..."
systemctl restart apache2
# Enable services to start on boot
systemctl enable apache2
systemctl enable mysql
# Install WP-CLI for command line WordPress management
echo ""
echo "Installing WP-CLI..."
wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -O /usr/local/bin/wp
chmod +x /usr/local/bin/wp
# Complete WordPress installation via WP-CLI
echo ""
echo "Completing WordPress installation..."
cd /var/www/html/wordpress
# Determine WordPress URL
# If SERVER_NAME looks like a private IP, try to get public IP
if [[ "$SERVER_NAME" =~ ^172\. ]] || [[ "$SERVER_NAME" =~ ^10\. ]] || [[ "$SERVER_NAME" =~ ^192\.168\. ]]; then
PUBLIC_IP=$(curl -s --connect-timeout 5 http://169.254.169.254/latest/meta-data/public-ipv4 2>/dev/null || curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null)
if [ -n "$PUBLIC_IP" ]; then
WP_URL="http://${PUBLIC_IP}"
echo "Using public IP for WordPress URL: $PUBLIC_IP"
else
WP_URL="http://${SERVER_NAME}"
echo "WARNING: Could not determine public IP, using private IP: $SERVER_NAME"
fi
else
WP_URL="http://${SERVER_NAME}"
fi
echo "WordPress URL will be: $WP_URL"
# Check if WordPress is already installed
if sudo -u www-data wp core is-installed 2>/dev/null; then
echo ""
echo "WARNING: WordPress is already installed!"
read -p "Continue with fresh installation? (y/n, default: n): " REINSTALL_WP
REINSTALL_WP=${REINSTALL_WP:-n}
if [ "$REINSTALL_WP" = "y" ]; then
echo "Reinstalling WordPress..."
sudo -u www-data wp db reset --yes
sudo -u www-data wp core install \
--url="$WP_URL" \
--title="${SITE_TITLE}" \
--admin_user="${WP_ADMIN_USER}" \
--admin_password="${WP_ADMIN_PASS}" \
--admin_email="${WP_ADMIN_EMAIL}" \
--skip-email
fi
else
sudo -u www-data wp core install \
--url="$WP_URL" \
--title="${SITE_TITLE}" \
--admin_user="${WP_ADMIN_USER}" \
--admin_password="${WP_ADMIN_PASS}" \
--admin_email="${WP_ADMIN_EMAIL}" \
--skip-email
fi
echo ""
echo "=========================================="
echo "=== WordPress Installation Complete! ==="
echo "=========================================="
echo ""
echo "Website URL: $WP_URL"
echo "Admin URL: $WP_URL/wp-admin"
echo ""
echo "WordPress Admin Credentials:"
echo " Username: $WP_ADMIN_USER"
echo " Password: $WP_ADMIN_PASS"
echo " Email: $WP_ADMIN_EMAIL"
echo ""
echo "Database Credentials:"
echo " Database: $DB_NAME"
echo " User: $DB_USER"
echo " Password: $DB_PASS"
echo ""
echo "MySQL Root Password: $DB_ROOT_PASS"
echo ""
echo "IMPORTANT: Save these credentials securely!"
echo ""
# Save credentials to file
CREDS_FILE="/root/wordpress-credentials.txt"
cat > "$CREDS_FILE" <<EOF
WordPress Installation Credentials
===================================
Date: $(date)
Website URL: $WP_URL
Admin URL: $WP_URL/wp-admin
WordPress Admin:
Username: $WP_ADMIN_USER
Password: $WP_ADMIN_PASS
Email: $WP_ADMIN_EMAIL
Database:
Name: $DB_NAME
User: $DB_USER
Password: $DB_PASS
MySQL Root Password: $DB_ROOT_PASS
WP-CLI installed at: /usr/local/bin/wp
Usage: sudo -u www-data wp <command>
Apache Configuration: /etc/apache2/sites-available/wordpress.conf
EOF
chmod 600 "$CREDS_FILE"
echo "Credentials saved to: $CREDS_FILE"
echo ""
echo "Next steps:"
echo "1. Visit $WP_URL/wp-admin to access your site"
echo "2. Consider setting up SSL/HTTPS with Let's Encrypt"
echo "3. Install a caching plugin for better performance"
echo "4. Configure regular backups"
echo ""
if [ -n "$DOMAIN_NAME" ]; then
echo "To set up SSL with Let's Encrypt:"
echo " apt-get install -y certbot python3-certbot-apache"
echo " certbot --apache -d ${DOMAIN_NAME}"
echo ""
fi
echo "To manage WordPress from command line:"
echo " cd /var/www/html/wordpress"
echo " sudo -u www-data wp plugin list"
echo " sudo -u www-data wp theme list"
echo ""
echo "Apache logs:"
echo " Error log: /var/log/apache2/wordpress-error.log"
echo " Access log: /var/log/apache2/wordpress-access.log"
echo ""
echo "=========================================="
Run it:
chmod +x setup-wordpress.sh
sudo ./setup-wordpress.sh
Part 3: Migrate Your Existing Site
If you’re migrating from an existing WordPress installation, follow these steps.
What gets migrated:
- All posts, pages, and media
- All users and their roles
- All plugins (files + database settings)
- All themes (including customisations)
- All plugin/theme configurations (stored in
wp_optionstable) - Widgets, menus, and customizer settings
- WooCommerce products, orders, customers (if applicable)
- All custom database tables created by plugins
Step 3a: Export from Old Server
Run this on your existing WordPress server. Save as wp-export.sh:
#!/bin/bash
set -euo pipefail
# Configuration
WP_PATH="/var/www/html" # Adjust to your WordPress path
EXPORT_DIR="/tmp/wp-migration"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Detect WordPress path if not set correctly
if [ ! -f "${WP_PATH}/wp-config.php" ]; then
for path in "/var/www/wordpress" "/var/www/html/wordpress" "/home/*/public_html" "/var/www/*/public_html"; do
if [ -f "${path}/wp-config.php" ]; then
WP_PATH="$path"
break
fi
done
fi
if [ ! -f "${WP_PATH}/wp-config.php" ]; then
echo "ERROR: wp-config.php not found. Please set WP_PATH correctly."
exit 1
fi
echo "==> WordPress found at: ${WP_PATH}"
# Extract database credentials from wp-config.php
DB_NAME=$(grep "DB_NAME" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
DB_USER=$(grep "DB_USER" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
DB_PASS=$(grep "DB_PASSWORD" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
DB_HOST=$(grep "DB_HOST" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
echo "==> Database: ${DB_NAME}"
# Create export directory
mkdir -p "${EXPORT_DIR}"
cd "${EXPORT_DIR}"
echo "==> Exporting database..."
mysqldump -h "${DB_HOST}" -u "${DB_USER}" -p"${DB_PASS}" \
--single-transaction \
--quick \
--lock-tables=false \
--routines \
--triggers \
"${DB_NAME}" > database.sql
DB_SIZE=$(ls -lh database.sql | awk '{print $5}')
echo " Database exported: ${DB_SIZE}"
echo "==> Exporting wp-content..."
tar czf wp-content.tar.gz -C "${WP_PATH}" wp-content
CONTENT_SIZE=$(ls -lh wp-content.tar.gz | awk '{print $5}')
echo " wp-content exported: ${CONTENT_SIZE}"
echo "==> Exporting wp-config.php..."
cp "${WP_PATH}/wp-config.php" wp-config.php.bak
echo "==> Creating migration package..."
tar czf "wordpress-migration-${TIMESTAMP}.tar.gz" \
database.sql \
wp-content.tar.gz \
wp-config.php.bak
rm -f database.sql wp-content.tar.gz wp-config.php.bak
PACKAGE_SIZE=$(ls -lh "wordpress-migration-${TIMESTAMP}.tar.gz" | awk '{print $5}')
echo ""
echo "============================================"
echo "Export complete!"
echo ""
echo "Package: ${EXPORT_DIR}/wordpress-migration-${TIMESTAMP}.tar.gz"
echo "Size: ${PACKAGE_SIZE}"
echo ""
echo "Transfer to new server with:"
echo " scp ${EXPORT_DIR}/wordpress-migration-${TIMESTAMP}.tar.gz ec2-user@NEW_IP:/tmp/"
echo "============================================"
Step 3b: Transfer the Export
scp /tmp/wp-migration/wordpress-migration-*.tar.gz ec2-user@YOUR_NEW_IP:/tmp/
Step 3c: Import on New Server
Run this on your new Graviton instance. Save as wp-import.sh:
#!/bin/bash
set -euo pipefail
# Configuration - EDIT THESE
MIGRATION_FILE="${1:-/tmp/wordpress-migration-*.tar.gz}"
OLD_DOMAIN="oldsite.com" # Your old domain
NEW_DOMAIN="newsite.com" # Your new domain (can be same)
WP_PATH="/var/www/wordpress"
# Resolve migration file path
MIGRATION_FILE=$(ls -1 ${MIGRATION_FILE} 2>/dev/null | head -1)
if [ ! -f "${MIGRATION_FILE}" ]; then
echo "ERROR: Migration file not found: ${MIGRATION_FILE}"
echo "Usage: $0 /path/to/wordpress-migration-XXXXXX.tar.gz"
exit 1
fi
echo "==> Using migration file: ${MIGRATION_FILE}"
# Get database credentials from existing wp-config
if [ ! -f "${WP_PATH}/wp-config.php" ]; then
echo "ERROR: wp-config.php not found at ${WP_PATH}"
echo "Please run the WordPress setup script first"
exit 1
fi
DB_NAME=$(grep "DB_NAME" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
DB_USER=$(grep "DB_USER" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
DB_PASS=$(grep "DB_PASSWORD" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
MYSQL_ROOT_PASS=$(cat /root/.wordpress/credentials | grep "MySQL Root" | awk '{print $4}')
echo "==> Extracting migration package..."
TEMP_DIR=$(mktemp -d)
cd "${TEMP_DIR}"
tar xzf "${MIGRATION_FILE}"
echo "==> Backing up current installation..."
BACKUP_DIR="/var/backups/wordpress/pre-migration-$(date +%Y%m%d_%H%M%S)"
mkdir -p "${BACKUP_DIR}"
cp -r "${WP_PATH}/wp-content" "${BACKUP_DIR}/" 2>/dev/null || true
mysqldump -u root -p"${MYSQL_ROOT_PASS}" "${DB_NAME}" > "${BACKUP_DIR}/database.sql" 2>/dev/null || true
echo "==> Importing database..."
mysql -u root -p"${MYSQL_ROOT_PASS}" << EOF
DROP DATABASE IF EXISTS ${DB_NAME};
CREATE DATABASE ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
EOF
mysql -u root -p"${MYSQL_ROOT_PASS}" "${DB_NAME}" < database.sql
echo "==> Importing wp-content..."
rm -rf "${WP_PATH}/wp-content"
tar xzf wp-content.tar.gz -C "${WP_PATH}"
chown -R caddy:caddy "${WP_PATH}/wp-content"
find "${WP_PATH}/wp-content" -type d -exec chmod 755 {} \;
find "${WP_PATH}/wp-content" -type f -exec chmod 644 {} \;
echo "==> Updating URLs in database..."
cd "${WP_PATH}"
OLD_URL_HTTP="http://${OLD_DOMAIN}"
OLD_URL_HTTPS="https://${OLD_DOMAIN}"
NEW_URL="https://${NEW_DOMAIN}"
# Install WP-CLI if not present
if ! command -v wp &> /dev/null; then
curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
fi
echo " Replacing ${OLD_URL_HTTPS} with ${NEW_URL}..."
sudo -u caddy wp search-replace "${OLD_URL_HTTPS}" "${NEW_URL}" --all-tables --precise --skip-columns=guid 2>/dev/null || true
echo " Replacing ${OLD_URL_HTTP} with ${NEW_URL}..."
sudo -u caddy wp search-replace "${OLD_URL_HTTP}" "${NEW_URL}" --all-tables --precise --skip-columns=guid 2>/dev/null || true
echo " Replacing //${OLD_DOMAIN} with //${NEW_DOMAIN}..."
sudo -u caddy wp search-replace "//${OLD_DOMAIN}" "//${NEW_DOMAIN}" --all-tables --precise --skip-columns=guid 2>/dev/null || true
echo "==> Flushing caches and rewrite rules..."
sudo -u caddy wp cache flush
sudo -u caddy wp rewrite flush
echo "==> Reactivating plugins..."
# Some plugins may deactivate during migration - reactivate all
sudo -u caddy wp plugin activate --all 2>/dev/null || true
echo "==> Verifying import..."
POST_COUNT=$(sudo -u caddy wp post list --post_type=post --format=count)
PAGE_COUNT=$(sudo -u caddy wp post list --post_type=page --format=count)
USER_COUNT=$(sudo -u caddy wp user list --format=count)
PLUGIN_COUNT=$(sudo -u caddy wp plugin list --format=count)
echo ""
echo "============================================"
echo "Migration complete!"
echo ""
echo "Imported content:"
echo " - Posts: ${POST_COUNT}"
echo " - Pages: ${PAGE_COUNT}"
echo " - Users: ${USER_COUNT}"
echo " - Plugins: ${PLUGIN_COUNT}"
echo ""
echo "Site URL: https://${NEW_DOMAIN}"
echo ""
echo "Pre-migration backup: ${BACKUP_DIR}"
echo "============================================"
rm -rf "${TEMP_DIR}"
Run it:
chmod +x wp-import.sh
sudo ./wp-import.sh /tmp/wordpress-migration-*.tar.gz
Step 3d: Verify Migration
#!/bin/bash
set -euo pipefail
WP_PATH="/var/www/wordpress"
cd "${WP_PATH}"
echo "==> WordPress Verification Report"
echo "=================================="
echo ""
echo "WordPress Version:"
sudo -u caddy wp core version
echo ""
echo "Site URL Configuration:"
sudo -u caddy wp option get siteurl
sudo -u caddy wp option get home
echo ""
echo "Database Status:"
sudo -u caddy wp db check
echo ""
echo "Content Summary:"
echo " Posts: $(sudo -u caddy wp post list --post_type=post --format=count)"
echo " Pages: $(sudo -u caddy wp post list --post_type=page --format=count)"
echo " Media: $(sudo -u caddy wp post list --post_type=attachment --format=count)"
echo " Users: $(sudo -u caddy wp user list --format=count)"
echo ""
echo "Plugin Status:"
sudo -u caddy wp plugin list --format=table
echo ""
echo "Uploads Directory:"
UPLOAD_COUNT=$(find "${WP_PATH}/wp-content/uploads" -type f 2>/dev/null | wc -l)
UPLOAD_SIZE=$(du -sh "${WP_PATH}/wp-content/uploads" 2>/dev/null | cut -f1)
echo " Files: ${UPLOAD_COUNT}"
echo " Size: ${UPLOAD_SIZE}"
echo ""
echo "Service Status:"
echo " PHP-FPM: $(systemctl is-active php-fpm)"
echo " MariaDB: $(systemctl is-active mariadb)"
echo " Caddy: $(systemctl is-active caddy)"
echo ""
echo "Page Load Test:"
DOMAIN=$(sudo -u caddy wp option get siteurl | sed 's|https://||' | sed 's|/.*||')
curl -w " Total time: %{time_total}s\n HTTP code: %{http_code}\n" -o /dev/null -s "https://${DOMAIN}/"
Rollback if Needed
If something goes wrong:
#!/bin/bash
set -euo pipefail
BACKUP_DIR=$(ls -1d /var/backups/wordpress/pre-migration-* 2>/dev/null | tail -1)
if [ -z "${BACKUP_DIR}" ]; then
echo "ERROR: No backup found"
exit 1
fi
echo "==> Rolling back to: ${BACKUP_DIR}"
WP_PATH="/var/www/wordpress"
MYSQL_ROOT_PASS=$(cat /root/.wordpress/credentials | grep "MySQL Root" | awk '{print $4}')
DB_NAME=$(grep "DB_NAME" "${WP_PATH}/wp-config.php" | cut -d "'" -f 4)
mysql -u root -p"${MYSQL_ROOT_PASS}" "${DB_NAME}" < "${BACKUP_DIR}/database.sql"
rm -rf "${WP_PATH}/wp-content"
cp -r "${BACKUP_DIR}/wp-content" "${WP_PATH}/"
chown -R caddy:caddy "${WP_PATH}/wp-content"
cd "${WP_PATH}"
sudo -u caddy wp cache flush
sudo -u caddy wp rewrite flush
echo "Rollback complete!"
Part 4: Post-Installation Optimisations
After setup (or migration), run these additional optimisations:
#!/bin/bash
cd /var/www/wordpress
# Remove default content
sudo -u caddy wp post delete 1 2 --force 2>/dev/null || true
sudo -u caddy wp theme delete twentytwentytwo twentytwentythree 2>/dev/null || true
# Update everything
sudo -u caddy wp core update
sudo -u caddy wp plugin update --all
sudo -u caddy wp theme update --all
# Configure WP Super Cache
sudo -u caddy wp super-cache enable 2>/dev/null || true
# Set optimal permalink structure
sudo -u caddy wp rewrite structure '/%postname%/'
sudo -u caddy wp rewrite flush
echo "Optimisations complete!"
Performance Verification
Check your stack is running optimally:
# Verify PHP OPcache status
php -i | grep -i opcache
# Check PHP-FPM status
systemctl status php-fpm
# Test page load time
curl -w "@-" -o /dev/null -s "https://yourdomain.com" << 'EOF'
time_namelookup: %{time_namelookup}s
time_connect: %{time_connect}s
time_appconnect: %{time_appconnect}s
time_pretransfer: %{time_pretransfer}s
time_redirect: %{time_redirect}s
time_starttransfer: %{time_starttransfer}s
----------
time_total: %{time_total}s
EOF
Cost Comparison
| Instance | vCPU | RAM | Monthly Cost | Use Case |
|---|---|---|---|---|
| t4g.micro | 2 | 1GB | ~$6 | Dev/testing |
| t4g.small | 2 | 2GB | ~$12 | Small blogs |
| t4g.medium | 2 | 4GB | ~$24 | Medium traffic |
| t4g.large | 2 | 8GB | ~$48 | High traffic |
| c7g.medium | 1 | 2GB | ~$25 | CPU-intensive |
All prices are approximate for eu-west-1 with on-demand pricing. Reserved instances or Savings Plans reduce costs by 30-60%.
Troubleshooting
502 Bad Gateway: PHP-FPM socket permissions issue
systemctl restart php-fpm
ls -la /run/php-fpm/www.sock
Database connection error: Check MariaDB is running
systemctl status mariadb
mysql -u wp_user -p wordpress
SSL certificate not working: Ensure DNS is pointing to instance IP
dig +short yourdomain.com
curl -I https://yourdomain.com
OPcache not working: Verify with phpinfo
php -r "phpinfo();" | grep -i opcache.enable
Quick Reference
# 1. Launch instance (local machine)
./launch-graviton-wp.sh
# 2. SSH in and setup WordPress
ssh -i ~/.ssh/key.pem ec2-user@IP
sudo ./setup-wordpress.sh
# 3. If migrating - on old server
./wp-export.sh
scp /tmp/wp-migration/wordpress-migration-*.tar.gz ec2-user@NEW_IP:/tmp/
# 4. If migrating - on new server
sudo ./wp-import.sh /tmp/wordpress-migration-*.tar.gz
This setup delivers a production-ready WordPress installation that’ll handle significant traffic while keeping your AWS bill minimal. The combination of Graviton’s price-performance, Caddy’s efficiency, and properly-tuned PHP creates a stack that punches well above its weight class.









