1. Executive Summary
Managed WordPress hosting rarely includes failover between environments: if the live environment goes down, the site goes with it. In this proof of concept I built an automated sync and domain-switching strategy with GitHub Actions and the WP Engine API, which automates two critical operations:
- Daily Synchronisation: Automatically keeps a secondary “Backup” environment up-to-date with production data.
- Emergency Failover: A manual, one-click workflow to instantly move the primary domain from a downed environment to the backup environment.
2. Architecture Overview
The solution utilises two GitHub Actions workflows interacting with the WP Engine API.
Workflow Logic
- Daily Sync:
Production→ Clones content/DB →Backup Environment(Runs daily at 04:00 UTC). - Failover (Evacuate):
Production(Down) → Swap Primary to default → Delete Domain → Add Domain toBackup. - Restoration (Restore):
Backup(Live) → Swap Primary to default → Delete Domain → Add Domain toProduction.
3. Prerequisites & Configuration
3.1 WP Engine Requirements
- API Access: Enabled in WP Engine User Portal.
- Two Environments:
- Production (Source): The live site.
- Backup (Destination): A separate environment to serve as the failover target.
- UUIDs: You must obtain the Long UUIDs (e.g.,
5c3615a7-...) for both installs, not the short names. WP Engine API Playground It is a useful resource to find information about the IDs of environments on your account
3.2 GitHub Secrets
Navigate to Settings > Secrets and variables > Actions and add the following repository secrets:
| Secret Name | Description | Example Value |
|---|---|---|
WPE_API_USER_ID |
WP Engine API User UUID | a1b2c3d4-... |
WPE_API_PASSWORD |
WP Engine API Password | secure-password-string |
WPE_INSTALL_ID_PROD |
UUID of the Production Install | 5c3615a7-... |
WPE_INSTALL_ID_BACKUP |
UUID of the Backup Install | e47b1028-... |
4. Workflows Implemented
Workflow A: Daily Site Copy
File: .github/workflows/daily-site-copy.yml
Trigger: Cron Schedule (0 4 * * * – 4 AM UTC)
Function: This workflow utilises the /install_copy endpoint. It forces a copy of the database and file system from Production to Backup. This ensures that if Production fails, the Backup is at most 24 hours behind (or less, depending on content velocity).
Workflow B: Manual Domain Switcher (The “Red Button”)
File: .github/workflows/switch-primary-domain.yml
Trigger: Manual (workflow_dispatch)
Function: This workflow handles the complex logic of moving a domain between environments. Since WP Engine does not allow deleting a “Primary” domain, the script performs a “Safe Swap”:
- Identify: Locates the custom primary domain (e.g.,
example.com) and a fallback domain (e.g.,example.wpenginepowered.com). - Swap: Sets the fallback domain as
Primaryon the source environment. - Delete: Removes
example.comfrom the source (now that it is no longer primary). - Move: Adds
example.comto the destination environment and sets it asPrimary.
5. Operational Guide (Runbook)
How to trigger a Failover (Emergency)
If Production goes down and an immediate recovery is required:
- Go to the GitHub Repository → Actions tab.
- Select “[Manual] Switch Primary Domain” from the left sidebar.
- Click Run workflow.
- In the Direction dropdown, select:
evacuate (Prod -> Backup)
- Click the green Run workflow button.
- Result: The domain will move to the Backup environment within approx. 30-60 seconds.
How to Restore to Production
Once the Production issue is resolved:
- Follow the steps above.
- In the Direction dropdown, select:
restore (Backup -> Prod)
- Result: Traffic is routed back to the main Production environment.
6. Code Reference (For Maintenance)
daily-site-copy.yml
name: Daily Site Copy to Backup
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
jobs:
copy-environment:
runs-on: ubuntu-latest
steps:
- name: Trigger Copy from Prod to Backup
env:
WPE_USER: ${{ secrets.WPE_API_USER_ID }}
WPE_PASS: ${{ secrets.WPE_API_PASSWORD }}
SOURCE_UUID: ${{ secrets.WPE_INSTALL_ID_PROD }}
DEST_UUID: ${{ secrets.WPE_INSTALL_ID_BACKUP }}
run: |
curl -X POST "https://api.wpengineapi.com/v1/install_copy" \
-u "$WPE_USER:$WPE_PASS" \
-H "Content-Type: application/json" \
-d "{
\"source_environment_id\": \"$SOURCE_UUID\",
\"destination_environment_id\": \"$DEST_UUID\",
\"notification_emails\": []
}" \
--fail-with-body
switch-primary-domain.yml
name: "[Manual] Switch Primary Domain"
on:
workflow_dispatch:
inputs:
direction:
description: "Which way are we moving the domain?"
required: true
default: "evacuate (Prod -> Backup)"
type: choice
options:
- "evacuate (Prod -> Backup)"
- "restore (Backup -> Prod)"
jobs:
switch-domain:
runs-on: ubuntu-latest
steps:
- name: Install Utilities
run: sudo apt-get install -y jq
- name: Execute Domain Switch
env:
WPE_USER: ${{ secrets.WPE_API_USER_ID }}
WPE_PASS: ${{ secrets.WPE_API_PASSWORD }}
ID_PROD: ${{ secrets.WPE_INSTALL_ID_PROD }}
ID_BACKUP: ${{ secrets.WPE_INSTALL_ID_BACKUP }}
DIRECTION: ${{ inputs.direction }}
run: |
# 1. Determine Direction
if [[ "$DIRECTION" == "evacuate (Prod -> Backup)" ]]; then
OLD_ID="$ID_PROD"
NEW_ID="$ID_BACKUP"
else
OLD_ID="$ID_BACKUP"
NEW_ID="$ID_PROD"
fi
# 2. Fetch Domains & Identify Targets
LIST_RESPONSE=$(curl -s -X GET "https://api.wpengineapi.com/v1/installs/$OLD_ID/domains" -u "$WPE_USER:$WPE_PASS")
# Find Primary (to move) and Fallback (wpenginepowered.com)
PRIMARY_DATA=$(echo "$LIST_RESPONSE" | jq -r '.results[] | select(.primary == true) | "\(.id)|\(.name)"')
FALLBACK_DATA=$(echo "$LIST_RESPONSE" | jq -r '.results[] | select(.name | contains("wpenginepowered.com")) | "\(.id)|\(.name)"' | head -n 1)
if [ -z "$PRIMARY_DATA" ] || [ -z "$FALLBACK_DATA" ]; then
echo "Error: Could not find Primary or Fallback domain."
exit 1
fi
TARGET_ID=$(echo "$PRIMARY_DATA" | cut -d'|' -f1)
TARGET_NAME=$(echo "$PRIMARY_DATA" | cut -d'|' -f2)
FALLBACK_ID=$(echo "$FALLBACK_DATA" | cut -d'|' -f1)
# 3. Swap Primary to Fallback
curl -X PATCH "https://api.wpengineapi.com/v1/installs/$OLD_ID/domains/$FALLBACK_ID" \
-u "$WPE_USER:$WPE_PASS" -H "Content-Type: application/json" -d '{ "primary": true }' --fail-with-body
sleep 5
# 4. Delete Target Domain from Old
curl -X DELETE "https://api.wpengineapi.com/v1/installs/$OLD_ID/domains/$TARGET_ID" \
-u "$WPE_USER:$WPE_PASS" --fail-with-body
sleep 5
# 5. Add Target Domain to New
curl -X POST "https://api.wpengineapi.com/v1/installs/$NEW_ID/domains" \
-u "$WPE_USER:$WPE_PASS" -H "Content-Type: application/json" \
-d "{ \"name\": \"$TARGET_NAME\", \"primary\": true }" --fail-with-body
7. Limitations & Known Risks
- Data Currency: The Backup environment is only as fresh as the last 4 AM copy. Any content created after 4 AM on the day of the crash will not be present in the Backup environment unless a manual copy is triggered.
- DNS Propagation: While the WP Engine setting changes instantly, DNS caching (TTL) may cause a slight delay for some users.
- AN (Advanced Network): No DNS changes are needed in case of Advanced Network, as CNAME is the same between environments
- Legacy (Legacy Network): Ensure your DNS provider (e.g., Cloudflare) has a low TTL or uses CNAME flattening so IP changes are not required; DNS needs to be changed manually to the new environment.
- Unique Identifiers: This script relies on finding a domain ending in
wpenginepowered.comto use as a temporary primary.
Skills used
GitHub Actions (scheduled and manual workflows), the WP Engine REST API, Bash and curl, disaster recovery and failover planning, DNS and domain management, GitHub secrets, documenting risks and limitations.