Author: @khoipro, @copilot
- Install ioncube for all PHP versions
- Migrate WordPress and Laravel apps between RunCloud servers
- Fix web applications permission (runcloud chown, file 644 folder 755)
- Chown a webapp tree to www-data while preserving wp-content/uploads
- Disk space cleanup (LiteSpeed cache, swap, journal logs)
- Change SSH port
- Debug WP-CLI issues
- Update Node.js
- Automatic Tweak my.cnf
- Git untracked file cleanup (classify + commit or gitignore)
- Batch update WP Site (using wp-cli)
- WP Security audit installer and WP Security Audit
- Server metrics collector with webhook reporting
- Self-update (auto-pull latest from GitHub)
- WordPress code freeze (lock filesystem + disable user management)
- WP vulnerability check (CVE scanning via WPVulnerability.net API)
- WP health check (FPM PHP binary probe — DB, plugin/theme fatals)
- OpenLitespeed/Nginx
- Ubuntu 20, 22 or 24 version
Login as root and clone the repo:
cd /root
git clone https://github.com/codetot-web/runcloud-bash-scripts.git
cd runcloud-bash-scripts
chmod +x *.shFull WordPress migration between RunCloud servers. Handles database, config files, uploads, git submodules, and staging URL in one command.
What it does:
- Exports database (wp-cli or mysqldump with corrupted table handling)
- Transfers and imports database on destination
- Syncs wp-config.php, .htaccess, .htninja
- Syncs wp-content/uploads and normalizes file permissions for web serving
- Initializes git submodules on destination
- Optionally updates site URL for staging (search-replace)
Prerequisites:
- SSH key auth from source to destination server
- Database and user must already exist on destination (create via RunCloud panel)
- Run as the
runclouduser on the source server
Setup SSH keys (first time only):
./wp-migration.sh runcloud@destination-server.com --setup-sshMigrate a site (same app name on both servers):
./wp-migration.sh runcloud@destination-server.com myappMigrate with a different app name on destination:
./wp-migration.sh runcloud@destination-server.com myapp newappMigrate with staging URL update:
./wp-migration.sh runcloud@destination-server.com myapp --staging-url=http://myapp.example.temp-site.linkCustom SSH port:
./wp-migration.sh runcloud@destination-server.com:2222 myappFull example (typical workflow):
# 1. Setup SSH keys to destination (one-time)
./wp-migration.sh runcloud@sg3.codetot.org --setup-ssh
# 2. Run migration with staging URL
./wp-migration.sh runcloud@sg3.codetot.org myapp --staging-url=http://myapp.staging.temp-site.linkSync a local WordPress site on this Mac to a RunCloud production webapp.
What it does:
- Exports the local database from the source site on your Mac
- Transfers and imports the database into the destination RunCloud webapp
- Syncs
wp-content/uploadsand normalizes file permissions for web serving - Syncs
.htaccessand.htninjawhen present - Runs a production URL search-replace on the destination
- Normalizes upload file permissions to
644and directories to755on the destination
Prerequisites:
- SSH access to the destination RunCloud server
- Destination webapp and database already created in RunCloud
- Local site path points at the WordPress
publicdirectory
Examples:
./wp-local-to-production.sh runcloud@sg4.codetot.org "/Users/khoipro/Local Sites/cdev/app/public" \
--production-url=http://cdev.example.temp-site.link
./wp-local-to-production.sh runcloud@sg4.codetot.org "/Users/khoipro/Local Sites/cdev/app/public" cdev \
--production-url=http://cdev.example.temp-site.link
./wp-local-to-production.sh runcloud@sg4.codetot.org "/Users/khoipro/Local Sites/cdev/app/public" cdev \
--dry-run --production-url=http://cdev.example.temp-site.linkFlags:
--production-url=URL— required, destination URL used for search-replace--setup-ssh— add your Mac SSH public key to the destination server--dry-run— print the planned steps without exporting, importing, or syncing anything--skip-uploads— skip syncingwp-content/uploads--skip-search-replace— skip URL replacement on the destination--skip-htaccess— skip syncing.htaccess/.htninja--skip-submodules— skipgit submodule update --init --recursive
Full Laravel migration between RunCloud servers. Mirrors wp-migration.sh but for Laravel apps — reads .env, syncs storage + build assets, runs composer install and artisan optimize on the destination.
What it does:
- Exports database (mysqldump, reads creds from
.env) - Transfers and imports database on destination
- Syncs
.env(with optionalAPP_URLoverride for staging) - Syncs
storage/app/andpublic/build/ - Initializes git submodules
- Auto-detects the webapp's PHP version from
/etc/lsws-rc/conf.d/<app>.d/handler.confand runs:composer install --no-dev --optimize-autoloaderphp artisan storage:link,config:cache,route:cache,view:cachephp artisan migrate --force(skip with--skip-migrate)
Prerequisites:
- SSH key auth from source to destination
- DB and user must exist on destination with same name/user as source — align password via:
ALTER USER 'app_user'@'%' IDENTIFIED BY '<source_password>'; FLUSH PRIVILEGES;
- RunCloud panel: set Public Path =
/publicfor the destination webapp (else LSWS returns 404)
Examples:
# Setup SSH (one-time)
./laravel-migration.sh runcloud@sg4.codetot.org --setup-ssh
# Migrate with staging URL override
./laravel-migration.sh runcloud@sg4.codetot.org myapp \
--staging-url=http://myapp.staging.temp-site.link
# Skip composer (e.g. vendor was already rsynced)
./laravel-migration.sh runcloud@sg4.codetot.org myapp --skip-composer
# Skip migrations (don't run `php artisan migrate`)
./laravel-migration.sh runcloud@sg4.codetot.org myapp --skip-migrateFlags:
--staging-url=URL— rewriteAPP_URLin destination.env--skip-composer— skipcomposer install--skip-build— skip syncingpublic/build/--skip-storage— skip syncingstorage/app/--skip-migrate— skipphp artisan migrate --force
Fix file ownership and permissions for RunCloud web applications.
./fix-permission.sh
./fix-permission-site.sh myappRecursively chown a RunCloud webapp tree to www-data:www-data while skipping wp-content/uploads.
What it does:
- Resolves the site under
/home/<user>/webapps/<site> - Recursively chowns the site tree to
www-data:www-data - Excludes
wp-content/uploadsso uploaded media keeps its own permissions
Examples:
./chown-site.sh --site=meatdeli
./chown-site.sh --site=meatdeli --user=ubuntuFlags:
--site=NAME— required webapp name--user=NAME— optional system user; defaults toruncloud
Install ioncube loader for all PHP versions.
./install-ioncube.shInstall and run WordPress security audits.
./wp-security-audit-installer.sh
./wp-security-audit.shFree up disk space by cleaning LiteSpeed caches, swap files, journal logs, and WordPress plugin caches. Shows disk usage before/after.
What it cleans:
wp-content/cache/*— WordPress page cachewp-content/litespeed/cssjs/*— LiteSpeed minified CSS/JSwp-content/wpvivid_image_optimization/*— WPvivid optimization cache/tmp/lsws-rc/swap/*— LiteSpeed swap files (often 5-15G)/home/runcloud/lscaches/*— LiteSpeed external caches- Systemd journal logs (vacuums to 500M)
All cleaned items regenerate automatically — no data loss.
Clean all webapps + system:
./cleanup-disk.shClean a specific webapp + system:
./cleanup-disk.sh --site=myappClean system files only (no webapps):
./cleanup-disk.sh --system-onlyPreview what would be cleaned (no deletions):
./cleanup-disk.sh --dry-runCron job examples (add via RunCloud Dashboard > Cron Job, run as root):
# Daily — clean all webapp caches + system
0 0 * * * /root/runcloud-bash-scripts/cleanup-disk.sh
# Weekly Sunday — system cleanup only
0 0 * * 0 /root/runcloud-bash-scripts/cleanup-disk.sh --system-only
# Daily — specific app only
0 0 * * * /root/runcloud-bash-scripts/cleanup-disk.sh --site=myappAuto-tune MariaDB/MySQL settings based on server RAM and CPU. Optimized for WordPress workloads on RunCloud servers.
What it does:
- Detects server RAM and CPU cores
- Detects config file pattern (sg5:
mariadb.cnfvs sg3:runcloud.cnf) - Calculates optimal settings (~50% RAM for InnoDB buffer pool, scaled instances, IO capacity for SSD, etc.)
- Backs up existing config before any changes
- Applies settings and restarts MariaDB
- Auto-rolls back if MariaDB fails to start
Scaling rules:
| Setting | Formula |
|---|---|
innodb_buffer_pool_size |
~50% of total RAM |
innodb_buffer_pool_instances |
1 per GB of buffer pool (max 8) |
tmp_table_size |
64M (<4G), 96M (4-7G), 128M (8G+) |
innodb_io_capacity |
2000 (assumes SSD) |
max_connections |
300 (not default 4096) |
wait_timeout |
300s (not default 28800s) |
Auto-detect and apply:
./tweak-mycnf.shPreview changes without applying:
./tweak-mycnf.sh --dry-runShow current MariaDB settings:
./tweak-mycnf.sh --statusRestore previous config:
./tweak-mycnf.sh --restoreCollect server metrics (CPU, RAM, disk, load, uptime) and discover all web applications under /home/*/webapps/. Detects WordPress sites and checks for available updates (core, plugins, themes). Sends the JSON payload to any webhook endpoint with optional HMAC-SHA256 signing.
What it collects:
- CPU usage, load averages (1m/5m/15m)
- RAM total/used/percent
- Disk total/used/percent (root partition)
- Uptime in seconds
- Per-webapp: username, app name, disk usage in MB
- WordPress: version, site URL, available core/plugin/theme updates
Print metrics to stdout (no HTTP request):
./server-metrics.sh --printSend to a webhook endpoint:
WEBHOOK_URL=https://example.com/api/webhooks/server-metrics ./server-metrics.shSend with HMAC-SHA256 authentication:
WEBHOOK_URL=https://example.com/api/webhooks/server-metrics \
WEBHOOK_SECRET=your-secret \
./server-metrics.shOverride hostname:
WEBHOOK_URL=https://example.com/webhook \
HOSTNAME_OVERRIDE=my-server-01 \
./server-metrics.shCron job examples (run as root):
# Every 5 minutes — send metrics to webhook
*/5 * * * * WEBHOOK_URL=https://example.com/webhook WEBHOOK_SECRET=your-secret /root/runcloud-bash-scripts/server-metrics.sh >> /var/log/server-metrics.log 2>&1
# Every hour — save metrics locally
0 * * * * /root/runcloud-bash-scripts/server-metrics.sh --print >> /var/log/server-metrics.jsonHMAC-SHA256 Signature:
When WEBHOOK_SECRET is set, the script sends two headers:
X-Webhook-Signature— HMAC-SHA256 of{timestamp}.{payload}X-Webhook-Timestamp— Unix timestamp of the request
Verify on the receiving end with timing-safe comparison (hash_equals in PHP, hmac.compare_digest in Python).
Auto-update the repository by pulling the latest changes from GitHub. Skips if already up to date. Automatically chmod +x all scripts after update.
./self-update.shCron job example (daily at 3:30 AM):
30 3 * * * /root/runcloud-bash-scripts/self-update.sh >> /var/log/runcloud-bash-scripts-update.log 2>&1Lock a WordPress site's filesystem and admin capabilities after launch. Prevents plugin/theme installs, file edits, and user management — while keeping post publishing and media uploads fully functional.
Two-layer freeze:
- Filesystem: sets core files to read-only (
444/555), blocks PHP execution in uploads - WordPress: injects
DISALLOW_FILE_MODS,DISALLOW_FILE_EDIT,AUTOMATIC_UPDATER_DISABLEDintowp-config.php - Capability: drops a mu-plugin that removes all user management capabilities and hides the Users menu
Freeze a site:
./wp-freeze.sh --site=myapp --action=freezeUnfreeze before a maintenance window:
./wp-freeze.sh --site=myapp --action=unfreezeCheck status:
./wp-freeze.sh --site=myapp --action=status
# or all sites:
./wp-freeze.sh --action=statusPreview changes without applying:
./wp-freeze.sh --site=myapp --action=freeze --dry-runWhat admin can do after freeze:
| Action | Works? |
|---|---|
| Create / edit / publish posts | Yes |
| Upload media | Yes |
| Install or update plugins/themes | No |
| Edit theme/plugin files in admin | No |
| Create / edit / delete users | No |
| WordPress auto-updates | No |
Maintenance window workflow:
# 1. Unfreeze before updates
./wp-freeze.sh --site=myapp --action=unfreeze
# 2. Run updates (plugins, core, etc.)
# 3. Re-freeze after updates
./wp-freeze.sh --site=myapp --action=freezeCheck installed WordPress plugins and themes for known CVEs using the free WPVulnerability.net API. Reports vulnerability name, CVE ID, CVSS score, and severity.
Check a site:
./wp-vuln-check.sh --site=myappInclude WordPress core version check:
./wp-vuln-check.sh --site=myapp --include-coreJSON output (for automation):
./wp-vuln-check.sh --site=myapp --jsonCheck plugins only or themes only:
./wp-vuln-check.sh --site=myapp --plugins-only
./wp-vuln-check.sh --site=myapp --themes-onlyRequirements: python3 and curl must be available on the server (standard on Ubuntu 20+).
Scan WordPress sites for untracked git files, classify them, and either report or commit them in logical groups. Adds production artifacts to .gitignore automatically. Skips frozen sites.
Classification + commit order:
| Group | Pattern | Commit message |
|---|---|---|
.gitignore |
Cache, backups, logs, litespeed, wpvivid, .tmb/ |
chore: update .gitignore for production artifacts |
| WP Core | wp-admin/, wp-includes/, root wp-*.php |
chore: track wp core files |
| WP Themes | wp-content/themes/THEME/ (one commit per theme) |
chore: track wp theme (THEME) |
| WP Plugins | wp-content/plugins/SLUG/ (one commit per plugin) |
chore: track wp plugin (SLUG) |
| MU-Plugins | wp-content/mu-plugins/ |
chore: track mu-plugins |
| Languages | wp-content/languages/ (.mo/.po/.pot/.l10n.php) |
chore: track wp languages |
Scan all sites (report only):
./wp-git-cleanup.sh --action=scanCleanup a specific site:
./wp-git-cleanup.sh --site=myapp --action=cleanupCleanup all sites:
./wp-git-cleanup.sh --action=cleanupPreview changes without applying:
./wp-git-cleanup.sh --site=myapp --action=cleanup --dry-runNotes:
- Supports multi-user layout (
/home/*/webapps/) - Skips frozen sites (detected via
code-freeze.phpmu-plugin) - Language
.jsonfiles are auto-ignored (WP-generated JED hashes) - Runs git as the site owner to avoid dubious ownership errors
WordPress health auditor for RunCloud servers. Discovers all WordPress webapps and probes each via wp eval invoked through the site's actual FPM/LSPHP binary — bypasses Cloudflare and catches real failures (DB connection broken, plugin/theme fatals).
Key detail: Uses the app's FPM PHP binary (detected from RunCloud FPM pools or OpenLiteSpeed LSAPI) as the PHP interpreter for wp-cli. The system CLI PHP may lack extensions (e.g. mysqli) that the FPM PHP has, so using the wrong PHP causes false-positive "MySQL extension missing" errors.
Probe all sites (show failures only):
./wp-health-check.shProbe a single site:
./wp-health-check.sh --site=myappDry-run (discover sites + FPM versions):
./wp-health-check.sh --dry-runJSON output:
./wp-health-check.sh --format=jsonChange the default SSH port.
./change-ssh-port.sh