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:

  1. Daily Synchronisation: Automatically keeps a secondary “Backup” environment up-to-date with production data.
  2. 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 to Backup.
  • Restoration (Restore): Backup (Live) → Swap Primary to default → Delete Domain → Add Domain to Production.

3. Prerequisites & Configuration

3.1 WP Engine Requirements

  • API Access: Enabled in WP Engine User Portal.
  • Two Environments:
    1. Production (Source): The live site.
    2. 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”:

  1. Identify: Locates the custom primary domain (e.g., example.com) and a fallback domain (e.g., example.wpenginepowered.com).
  2. Swap: Sets the fallback domain as Primary on the source environment.
  3. Delete: Removes example.com from the source (now that it is no longer primary).
  4. Move: Adds example.com to the destination environment and sets it as Primary.

5. Operational Guide (Runbook)

How to trigger a Failover (Emergency)

If Production goes down and an immediate recovery is required:

  1. Go to the GitHub Repository → Actions tab.
  2. Select “[Manual] Switch Primary Domain” from the left sidebar.
  3. Click Run workflow.
  4. In the Direction dropdown, select:
    • evacuate (Prod -> Backup)
  5. Click the green Run workflow button.
  6. 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:

  1. Follow the steps above.
  2. In the Direction dropdown, select:
    • restore (Backup -> Prod)
  3. 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

  1. 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.
  2. 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.
  3. Unique Identifiers: This script relies on finding a domain ending in wpenginepowered.com to 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.