Managing multiple web applications often requires a few repetitive, time-sensitive tasks. One of these tasks is updating the underlying PHP version.

If you want to update the PHP version for a single web application on RunCloud, then you can easily do so using the RunCloud dashboard. 

However, if a PHP version reaches End-of-Life (EOL) or a critical security vulnerability is found in an older version of PHP, you might need to update the PHP version of hundreds of web applications overnight

Although you can manually open each application’s control panel in the RunCloud dashboard to perform a backup, you can also trigger a PHP version update. But this can be slow and tedious if you manage more than a handful of applications. 

This post will show you how to use the RunCloud API to create a Python script that streamlines this process. By leveraging the RunCloud API, this script automates updating PHP versions across multiple applications based on predefined criteria.

There are several advantages to using the RunCloud API for performing batch updates:

  1. Efficiency: Updates numerous applications with a single script execution, saving significant time and effort.
  2. Consistency: Ensures the same logic is applied to all applications, reducing the chance of manual errors.
  3. Targeted Updates: Allows different PHP versions to be applied based on the application’s deployment environment (e.g., a stable version for production and a newer one for development/staging).
  4. Rapid Response: Enables quick reaction to EOL announcements or vulnerability disclosures.

Necessary Information Before You Begin

Before executing the provided Python script, please be aware of the following:

  1. BACKUPS ARE ESSENTIAL: This cannot be stressed enough. This script performs updates, not backups. Before running any automated update process, ensure you have complete, verified backups of all target web applications and their databases. Automation mistakes can have widespread consequences, and backups are your safety net. Consider using RunCloud’s backup features or other reliable backup solutions beforehand.
  2. RunCloud API Credentials: The script requires authentication to interact with the RunCloud API.
    • You will need your RunCloud API Secret. You can generate API Keys from your RunCloud account settings.
    • Treat your API Key and Secret like passwords. Do not hardcode them directly into the script if you plan to share it or commit it to version control. Consider using environment variables or a secure configuration management system. 
  3. Server ID: You will need the specific numeric ID of the RunCloud server hosting the web applications you wish to update.
    • You can typically find this ID in the URL when viewing the server details in the RunCloud dashboard (e.g., manage.runcloud.io/servers/12345/summary -> Server ID is 12345).
    • Alternatively, RunCloud provides API endpoints to list your servers and retrieve their IDs. Read our documentation to learn how to use the RunCloud API to retrieve server information
  4. Target PHP Version Strings: The script uses specific strings like “php82rc” or “php84rc”. These must match the exact identifiers RunCloud uses for the desired PHP versions. 
  5. Python Environment: You’ll need Python 3 installed on the machine where you intend to run the script. The script uses standard libraries, so no external package installation is required for this specific script.

Working with Python and the RunCloud API

We’ll now walk you through the steps to use the RunCloud API with a Python script. However, it’s important to remember that the RunCloud API is flexible. You are not limited to Python; you can interact with the RunCloud API using virtually any programming language you are comfortable with.Please refer to the official RunCloud API v3 documentation for more detailed information on using the API with other languages or exploring its full capabilities.

import http.client
import json


# --- Configuration ---
RUNCLOUD_API_HOST = "manage.runcloud.io"
# IMPORTANT: Replace with your actual Server ID
SERVER_ID = None
# IMPORTANT: Replace with your actual API Key and Secret or Bearer Token
auth_string = 'See RunCloud API documentation for generating API credentials. (https://runcloud.io/docs/generating-api-keys-enabling-api-access)'
AUTH_HEADER = {"Authorization": f"Bearer {auth_string}",
               "user-agent": "My Python App"}


PHP_VERSION_PRODUCTION = "php82rc"
PHP_VERSION_DEVELOPMENT = "php84rc" 


# --- Helper Function for API Requests ---
def make_api_request(method, path, payload=None, extra_headers=None):
    """
    Makes an API request to the RunCloud API.


    Args:
        method (str): HTTP method (e.g., "GET", "PATCH").
        path (str): API endpoint path (e.g., "/api/v3/servers/...").
        payload (dict, optional): Data payload for POST/PATCH requests. Defaults to None.
        extra_headers (dict, optional): Extra headers to add/override. Defaults to None.


    Returns:
        tuple: (status_code, response_body_dict) or (None, error_message) on failure.
    """
    conn = None # Initialize conn to None
    try:
        conn = http.client.HTTPSConnection(RUNCLOUD_API_HOST)
        
        # Prepare headers
        headers = AUTH_HEADER.copy() # Start with base auth headers
        if extra_headers:
            headers.update(extra_headers)
        if 'Content-Type' not in headers:
            headers['Content-Type'] = 'application/json' # Ensure header is set for JSON
            
        # Prepare payload
        body = None
        if payload:
            body = json.dumps(payload).encode('utf-8')
        else:
            body = '' # Empty body for GET/DELETE etc. if no payload specified


        conn.request(method, path, body=body, headers=headers)
        
        res = conn.getresponse()
        status = res.status
        data = res.read()
        response_body_str = data.decode("utf-8")
        
        # Try to parse JSON, but return raw string if not JSON or if error
        try:
            response_json = json.loads(response_body_str)
            return status, response_json
        except json.JSONDecodeError:
             # Handle cases where response isn't valid JSON (e.g., unexpected HTML error page)
            print(f"     Warning: Response was not valid JSON.")
            print(f"     Raw Response Body: {response_body_str[:200]}...") # Print start of body
            # Return status and raw body for non-JSON success, or just body for errors
            if 200 <= status < 300:
                 return status, {"raw_response": response_body_str} 
            else:
                return status, {"error": "Invalid JSON response", "details": response_body_str}


    except http.client.HTTPException as e:
        print(f"[Error] HTTP Exception during {method} {path}: {e}")
        return None, {"error": "HTTP Exception", "details": str(e)}
    except ConnectionError as e:
        print(f"[Error] Connection Error during {method} {path}: {e}")
        return None, {"error": "Connection Error", "details": str(e)}
    except Exception as e:
        print(f"[Error] Unexpected error during {method} {path}: {e}")
        return None, {"error": "Unexpected Error", "details": str(e)}
    finally:
        if conn:
            conn.close()




# --- Main Script Logic ---
def update_php_versions():
    """
    Fetches web applications and updates their PHP versions based on stackMode.
    """
    print("Starting PHP version update process...")




    if SERVER_ID == "{{serverId}}" or not AUTH_HEADER.get("Authorization"):
         print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
         print("!!! ERROR: Please configure SERVER_ID and AUTH_HEADER   !!!")
         print("!!!        in the script before running.                !!!")
         print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
         return




    # 1. Fetch the list of web applications
    # Note: This currently only fetches the first page. Implement pagination if needed.
    get_apps_path = f"/api/v3/servers/{SERVER_ID}/webapps"
    print(f"\n[Step 1] Fetching web applications for Server ID: {SERVER_ID}...")
    
    status, response_data = make_api_request("GET", get_apps_path)




    if status is None or status >= 300:
        print(f"[Error] Failed to fetch web applications.")
        print(f"        Response: {response_data}")
        return




    if not isinstance(response_data, dict) or 'data' not in response_data:
        print(f"[Error] Unexpected response format when fetching apps.")
        print(f"        Received: {response_data}")
        return
        
    applications = response_data.get('data', [])
    
    if not applications:
        print("No web applications found on this server.")
        return


    print(f"Found {len(applications)} web applications on this page.")
    
    # 2. Iterate and update PHP version for each application
    print("\n[Step 2] Processing applications for PHP updates...")
    updated_count = 0
    failed_count = 0


    for app in applications:
        try:
            app_id = app.get("id")
            app_name = app.get("name")
            current_php = app.get("phpVersion")
            stack_mode = app.get("stackMode")


            print(f"\nProcessing App: '{app_name}' (ID: {app_id})")
            print(f"  Current PHP: {current_php}, Stack Mode: {stack_mode}")




            target_php_version = None
            if stack_mode == "production":
                target_php_version = PHP_VERSION_PRODUCTION
            else: 
                target_php_version = PHP_VERSION_DEVELOPMENT
                
            if current_php == target_php_version:
                 print(f"  Skipping: App already has the target PHP version ({target_php_version}).")
                 continue


            print(f"  Updating PHP version to: {target_php_version}")


            # Construct payload and path for PATCH request
            update_path = f"/api/v3/servers/{SERVER_ID}/webapps/{app_id}/settings/php"
            update_payload = {"phpVersion": target_php_version}
            
            # Make the PATCH request to update PHP
            patch_status, patch_response = make_api_request("PATCH", update_path, payload=update_payload)


            if patch_status is not None and 200 <= patch_status < 300:
                print(f"  SUCCESS: PHP version updated for '{app_name}'.") 
                updated_count += 1
            else:
                print(f"  FAILURE: Failed to update PHP version for '{app_name}'.")
                print(f"           Status Code: {patch_status}")
                print(f"           Response: {patch_response}")
                failed_count += 1
        except KeyError as e:
            print(f"[Error] Missing expected key in application data: {e}")
            print(f"        Problematic application data snippet: {str(app)[:200]}...") # Print part of the data
            failed_count += 1
        except Exception as e:
            # Catch unexpected errors during the processing of a single app
            app_name_err = app.get("name", f"ID {app.get('id', 'N/A')}")
            print(f"[Error] Unexpected error processing application '{app_name_err}': {e}")
            failed_count += 1
            
    print("\n--------------------------------------------------")
    print("PHP Update Process Summary:")
    print(f"  Successfully updated: {updated_count} application(s)")
    print(f"  Failed or skipped:    {failed_count} application(s)")
    print("--------------------------------------------------")




# --- Run the script ---
if __name__ == "__main__":
    update_php_versions()

When you run python main.py, the script will perform these actions:

  1. Connect to RunCloud: The script uses the API details you configured to connect to your RunCloud account.
  2. Get Application List: It then asks RunCloud for a list of all web applications running on your specified server ID.
  3. Process Each Application:
    • The script looks at each application one by one, checking the application’s “stack mode” to determine whether it’s marked as production (a live site) or something else (development).
    • Based on this, it decides which new PHP version to apply (either your PHP_VERSION_PRODUCTION or PHP_VERSION_DEVELOPMENT).
    • If the application already has the target PHP version, it will be skipped.
  4. Update PHP Version: If an update is needed, the script sends a command back to RunCloud to change the PHP version for that specific application.
  5. Show Progress: As it works, the script will print messages to your screen, telling you which application it’s processing, and whether the update was successful or if there was an error.
  6. Provide a Summary: After trying to update all applications, it will show a summary of how many were updated successfully and how many failed or were skipped.

How to Use This Python Script

  1. Save the File: Copy the entire Python script code provided above and save it in a new file on your computer, named main.py.
  2. Configure Settings: Before you run the script, you’ll need to edit main.py with a text editor. At the top of the file, you must change these placeholder values:
    • SERVER_ID: Add the actual numerical ID of your RunCloud server. You can find this in your RunCloud dashboard.
    • auth_string: Add the API token for your RunCloud account.
    • PHP_VERSION_PRODUCTION: Set this to the PHP version string (e.g., “php82rc”) you want for your live production applications.
    • PHP_VERSION_DEVELOPMENT: Set this to the PHP version string (e.g., “php84rc”) you want for your development or non-production applications.
  3. Run the Script:
    • Open your computer’s command line tool (e.g., Terminal on macOS/Linux or Command Prompt/PowerShell on Windows).
    • Navigate to the directory where you saved main.py. 
    • Once you are in the correct directory, type the command: python main.py
    • Press ‘Enter’. This will execute the script.

Wrapping Up and Next Steps

In this guide, we’ve explored a practical method for automating PHP version updates across your web applications managed by RunCloud. Using the provided Python script can significantly streamline maintenance tasks and save valuable time.

This is just one approach, and the RunCloud API opens up many possibilities.

The Python example we’ve provided is a starting point. You can adapt this script further: perhaps to include pre-update backup commands, handle pagination for servers with a huge number of applications, or integrate it into broader deployment workflows.

If you’re keen to explore this solution further, customize it, or explore other automation opportunities for managing your RunCloud servers and applications, we highly recommend consulting the official RunCloud API v3 documentation.

We hope this example script allows you to manage your RunCloud applications more effectively.