Back to list
dev_to 2026年4月25日

Managed ホスティングからの脱却:WooCommerce サイトを VPS へ移行した際、(そして攻撃された)何が起きたのか

Escaping Managed Hosting: What Happened When We Migrated a WooCommerce Site to a VPS (And Got Attacked)

Translated: 2026/4/25 3:39:46

Japanese Translation

Managed WordPress ホスティングは魅力的なコストパフォーマンスに見えますが、そうはいかないこともあります。これは、Major Managed Host から WooCommerce と WPML サイトを移行し、その直後に発生した混乱、そして Managed ホスティングが静默的に私たちに何をやっていたかについて学んだ重要な教訓を、それが去ったときようやく完全に理解するに至ったという物語です。 その決定は劇的ではなかった。3 つの積み重なる不満の集積に由来した:コスト対コントロール。エンタープライズティアの Managed WordPress ホスティングは安くない。トラフィックと複雑性が増大するにつれて請求書も増えた。しかし、コントロールは依然として制限されたままでした。カスタムサーバー構成のなし、リッチチューニングの制限、サーバーに実際にヒットしているものを低レベルで確認する能力のなし。 パフォーマンスの天井。WooCommerce サイトが WPML(多言語)を含む場合、ユニークな URL の数が膨大になります。フィルタリングされた製品ページ、言語バリエーション、ページネーションされたアーカイブなど。Managed ホスティングのキャッシュレイヤーはブラックボックスでした。パフォーマンスが低下すると、答えは常に「プランをアップグレードしてください」でした。何が起こっているのかを以下で診断する方法がありませんでした。 可視性。何か間違いが起きたとき、私たちはリアルタイムでアクセスログを見ることができなかった、PHP ワーカー数を確認することができず、サーバーレベルの設定を調整することができませんでした。すべては抽象化されていました。ホスティング사는 私たちと実際のマシンとの間に守衛役としていました。 RunCloud と OpenLiteSpeed (OLS) を使用するセルフマネージド VPS への移行は、完全な可視性とコントロールを約束しました。それはその約束を果たしましたが、また、Managed ホスティングが代わりに私たちが静默的に吸収していたすべてに私たちが直面する危険を即座に暴露しました。 スタック:WordPress + WooCommerce と WPML(多言語、2 つの言語)Elementor ページビルダーLSCache キャッシング、Redis オブジェクトキャッシング Managed ホスティングから RunCloud と OpenLiteSpeed を使用した VPS への移行 移行自体は技術的には単純でした:エクスポート、インポート、DNS の更新。 しかし、簡単でなかったのは、移行の後にデータベースで見出されたことです。移行中は、サイトが一時的にステージングドメイン(mysite.staging.temphost.link など)上で動作しました。標準的なリプレイス検索は、古いドメインのすべての参照を新しいものに置き換えるはずです。それはすべてを捕まえませんでした。数々のプラグインは、シリアライズされた PHP アレイとして WordPress の wp_options テーブルにデータを保存します。通常の文字列の検索・置換はシリアライズされたデータを破損します—シリアライズされた文字列は自分自身の長さを取り扱い、URL を変更するだけだと文字列の長さは変更されますが、長さプレフィックスが更新されないためです。汚染を引き起こしたプラグイン:メディアカリーサルプラグイン—wp_options に 400KB 以上のシリアライズされた画像データ(画像パスに一時ドメインが含まれる)を保存Elementor—disk に wp-content/uploads/elementor/google-fonts/css/ に CSS ファイルを生成し、http:// URL がハードコードされ、移行後に再生成されませんでした画像最適化プラグイン—WebP キャッシュディレクトリが古いドメインの参照に満ちた 修正には、WP-CLI の --precise フラグ付き search-replace を使用する必要がありました。これはシリアライズされたデータを正しく処理します:wp search-replace 'mysite.staging.temphost.link' 'mysite.com' --precise --all-tables Elementor ファオン CSS ファイルに対して disk 上の直接の find + sed が必要で、WP-CLI はファイルを触らないからです:find /var/www/mysite/wp-content/uploads/elementor/google-fonts/css/ -name "*.css" \ -exec sed -i 's|http://mysite.staging.temphost.link|https://mysite.com|g' {} \ サイトが HTTPS で動作しても、Elementor は http://アセット URL を生成し続けていました。理由は、wp-config.php に WordPress が HTTPS behind であることを知らせる 2 行が欠けていたからです:$_SERVER['HTTPS'] = 'on'; define('FORCE_SSL_ADMIN', true);これらの行がないと、WordPress はリクエストが SSL 経由で入ったことを知らず(特にプロキシやロードバランサー behind の場合)、動的 URL はデフォルトで http://になります。予想外の多くの混合コンテンツ問題を引き起こした微妙な課題の一つ。 インストールされているセキュリティプラグインの 1 つが、.htaccess ルールを追加しました。それはバグ...</content> },

Original Content

Escaping Managed Hosting: What Happened When We Migrated a WooCommerce Site to a VPS (And Got Attacked) Managed WordPress hosting sounds like a great deal — until it isn't. This is the story of migrating a WooCommerce + WPML site off a major managed host, the chaos that followed immediately after, and the hard lessons learned about what managed hosting was silently doing for us that we didn't fully appreciate until it was gone. The decision wasn't dramatic. It came down to three compounding frustrations: Cost vs. control. Managed WordPress hosting at the enterprise tier isn't cheap. As traffic and complexity grew, so did the invoice. But the control stayed locked down — no custom server config, limited cache tuning, no ability to see what was actually hitting the server at a low level. Performance ceiling. A WooCommerce store with WPML (multilingual) generates a lot of unique URLs — filtered shop pages, language variants, paginated archives. The managed host's caching layer was a black box. When performance degraded, the answer was always "upgrade your plan." There was no way to diagnose what was actually happening underneath. Visibility. When something went wrong, we couldn't see access logs in real time, couldn't inspect PHP worker counts, couldn't adjust server-level settings. Everything was abstracted. The host was the gatekeeper between us and the actual machine. The move to a self-managed VPS with RunCloud and OpenLiteSpeed (OLS) promised full visibility and control. It delivered on that promise — but it also immediately exposed us to everything the managed host had been silently absorbing on our behalf. The stack: WordPress + WooCommerce with WPML (multilingual, two languages) Elementor as the page builder LSCache for caching, Redis for object caching Migrating from managed hosting to a VPS via RunCloud with OpenLiteSpeed The migration itself was technically straightforward: export, import, update DNS. What wasn't straightforward was what we discovered in the database afterward. During migration, the site temporarily lived on a staging domain (something like mysite.staging.temphost.link). The standard search-replace after migration should catch all references to the old domain and replace them with the new one. It didn't catch everything. Several plugins store data in WordPress's wp_options table as serialized PHP arrays. A normal string search-replace on serialized data corrupts it — because serialized strings encode their own length, and changing the URL changes the string length without updating the length prefix. The plugins that caused contamination: A media carousel plugin — stored 400KB+ of serialized image data in wp_options, all with the temp domain baked in for image paths Elementor — had generated CSS files on disk at wp-content/uploads/elementor/google-fonts/css/ with http:// URLs hardcoded, not regenerated after migration An image optimization plugin — had a WebP cache directory full of references to the old domain The fix required using WP-CLI's search-replace with the --precise flag, which handles serialized data correctly: wp search-replace 'mysite.staging.temphost.link' 'mysite.com' --precise --all-tables For the Elementor font CSS files on disk, a direct find + sed was needed since WP-CLI doesn't touch files: find /var/www/mysite/wp-content/uploads/elementor/google-fonts/css/ -name "*.css" \ -exec sed -i 's|http://mysite.staging.temphost.link|https://mysite.com|g' {} \; Despite the site running on HTTPS, Elementor kept generating http:// asset URLs. The reason was that wp-config.php was missing two lines that tell WordPress it's behind HTTPS: $_SERVER['HTTPS'] = 'on'; define('FORCE_SSL_ADMIN', true); Without these, WordPress doesn't know the request came in over SSL (especially behind a proxy or load balancer), so dynamic URLs default to http://. A subtle issue that caused a surprisingly large number of mixed content problems. One of the installed security plugins had added a .htaccess rule that was blocking all PHP file access: RewriteRule ^.*\.php$ - [F,L,NC] This rule was intended to block direct PHP file execution in upload directories. But it was placed at the wrong level — it blocked wp-admin, wp-login.php, and every other PHP file on the site. The admin panel was completely inaccessible. The fix was removing that rule from .htaccess manually via SSH before anything else could be done. The migration was complete. The site was live. And then within hours, the server was at 700%+ CPU — that's 7 full cores pinned on a machine that should have been comfortably handling the traffic. On managed hosting, this had never happened. Not because the traffic wasn't there — but because the managed host was absorbing it silently. Now it was hitting our VPS directly. Access log analysis revealed two suspicious IPs hammering the site: grep "111.88.x.x" /var/log/ols/mysite.com_access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -30 Both IPs were sending dozens of requests per minute to wp-admin/admin-ajax.php, WooCommerce AJAX endpoints, and Contact Form 7 REST endpoints — the fingerprint of bots probing for vulnerabilities and scraping data. Fix: blocked in .htaccess at the top of the file, before any WordPress rules: # Block malicious IPs Require all granted Require not ip 111.88.x.x Require not ip 45.77.x.x Blocking the malicious IPs brought load down — but not to safe levels. Something else was still hammering the server. Back to the access logs: grep "meta-externalagent" /var/log/ols/mysite.com_access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -30 Facebook's Meta crawler (meta-externalagent/1.1) was systematically crawling every combination of the WooCommerce shop's filter URLs: /shop/?filter_color=red&filter_size=small /shop/?filter_color=red&filter_size=medium /shop/?filter_color=blue&filter_size=small ... hundreds of unique combinations Here's why this is catastrophic for a WooCommerce + WPML site: Every unique URL is a cache miss. LSCache serves cached pages instantly with zero PHP. But a cached page is keyed by URL. Each filter combination is a different URL — so each one bypasses the cache entirely, boots WordPress, boots WooCommerce, boots WPML, runs a database query, and renders a response. The crawler was generating thousands of cache misses per hour. The fix — immediate: # Block Meta crawler RewriteEngine On RewriteCond %{HTTP_USER_AGENT} meta-externalagent [NC] RewriteRule ^ - [F,L] After adding this rule and restarting OLS: load average: 0.94 — 4 lsphp processes From 700%+ CPU to essentially idle. The Meta crawler was the primary load driver the entire time. The longer-term fix: add the shop's filter URL pattern to robots.txt so crawlers stop attempting them: User-agent: meta-externalagent Disallow: / User-agent: * Disallow: /shop/?* This is the part that changed how we think about managed hosting. The managed host had several layers we never thought about: Bot filtering at the edge — known bad actors and aggressive crawlers were blocked before they reached WordPress at all DDoS mitigation — volumetric attacks were absorbed by the host's network layer Rate limiting — aggressive crawlers were throttled automatically None of this was documented prominently. It was just... happening. Moving to a raw VPS removed all of it at once. The site went from being behind a shield to being fully exposed to the internet with only .htaccess and OLS between it and every bot on the planet. The visibility was exactly what we wanted — we could finally see everything. But we also now had to handle everything ourselves. Fixing bots with .htaccess rules is whack-a-mole. Block one IP, another appears. Block one user agent, it rotates. The real fix is a layer in front of the server that handles this at scale before it ever reaches OLS. Cloudflare's free plan provides: Bot Fight Mode — automatically identifies and blocks known bot fingerprints Rate limiting — caps any single IP's request rate before it can cause load spikes Edge caching — WooCommerce shop pages can be cached at Cloudflare's edge, so even cache misses on the origin become cache hits at the CDN level Real-time traffic analytics — finally, full visibility into what's hitting the site and from where The architecture after adding Cloudflare: Internet → Cloudflare edge (bot filtering, rate limiting, CDN cache) → VPS / OpenLiteSpeed (LSCache, Redis) → WordPress / WooCommerce / WPML Bad traffic is rejected at Cloudflare before it touches the server. The PHP worker pool stays available for real users. This is what managed hosting was providing implicitly. Cloudflare makes it explicit, configurable, and visible — and the free tier handles the vast majority of what a typical WooCommerce store needs. With the immediate crisis resolved, we locked down the remaining attack surface: Security headers via OLS: X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Strict-Transport-Security: max-age=31536000; includeSubDomains PHP worker limits — OLS was configured with a hard cap on concurrent PHP workers: maxConns 8 PHP_LSAPI_CHILDREN=8 Without this cap, a flood of requests spawns unlimited PHP workers and exhausts RAM. With the cap, excess requests queue rather than spawning new processes. Snapshot immediately after stabilization — once the server was clean and stable, we took a VPS snapshot as a known-good baseline. If anything goes wrong in future, rollback is one click. 1. Managed hosting hides its value until it's gone. 2. Always use --precise for search-replace on migrated WordPress sites. --precise flag in WP-CLI handles it correctly. Make it a default step in every migration checklist. 3. Cloudflare is not optional for a self-managed WooCommerce store. 4. WooCommerce filter URLs are a crawler trap. robots.txt before crawlers index them. 5. Access logs are your best friend on a VPS. The site runs more reliably now on the VPS than it ever did on managed hosting — and at significantly lower cost. Load averages stay under 1.0 under normal traffic. The Cloudflare layer handles bot traffic before it reaches the server. LSCache and Redis handle the WordPress-level caching. And when something goes wrong, we can actually see it. The migration pain was real. But it was a one-time cost that permanently increased visibility, control, and resilience. The managed host was comfortable — but comfort was masking problems we couldn't see or fix.