Leave GoDaddy and Go Completely Serverless Using Just Cloudflare and S3 Buckets: Part Two

This is the continuation of Part One. The goal, again, is:

Goal: Completely leave GoDaddy, move email services to Cloudflare, run WordPress offline, and serve static HTML pages from Amazon S3; pay only a fraction of the ever-rising GoDaddy hosting fees; and finally move off GoDaddy’s underpowered, EOL’d shared server.
Overview

In Part Two, I show you how to handle the .htaccess file, use Cloudflare Workers to serve dynamic content, create cron jobs, protect your S3 buckets, cache hard, handle comments, set up a WAF and firewall, and cover more advanced topics.

Results

Here is my serverless WordPress website, now:

Ericdraken.com serverless website performance
Ericdraken.com serverless website performance
Checklist: If you would like to skip to the end, here is a checklist of the steps I take when I move a site from GoDaddy.

Table of Contents

Part Two


Part One


Serverless: Serve a WordPress Website without a Server

Congratulations. At this point, we have a working website and do not even need Workers or Lambdas: HTML and images load fast, virtual subdomains work, the 404 page functions, and the whole site responds via HTTPS.

Think About It: What do we really need from serverless?

Some people throw a lot of services at serverless, overcomplicating serverless. But, why? Before you read an article that talks you into using Route 53, API Gateway, Lambdas, CloudFront, CloudWatch, RDS, and Terraform to hold it all together, stop and ask yourself what you need, not what is in vogue for a résumé.

My vision of serverless looks closer to this:

Limit serverless

Where to start? Notice that in the production/ folder there is a remnant .htaccess file containing directives such as:

There are more directives—different cache timings for various assets, a rewrite rule to redirect www to the root domain, rules to block bad bots, and so forth. These are handy Apache rules, but do we still need all of them?

My Identified Serverless Needs

Before following anyone’s “Set up Serverless in 10 Minutes” article, I need to think through what I actually need in a serverless setup to get away from GoDaddy without becoming a full-time SRE1.

Do I need a gateway? Am I just trying to be cool by throwing every service at the problem? Am I fooling myself into thinking that if every AWS service touches my serverless site it will make me marketable?

The best solution is one that works and keeps working.

Here is what I have identified as my needs:

  • Serve a custom 404 page.
  • Honor the accept-encoding: gzip, deflate, br request.2
  • Achieve great SEO performance.
  • Redirect moved pages over time.
  • Serve vanity affiliate links.
  • Serve tracking links in emails.
  • Run cron jobs with custom PHP.
  • Collect information with a 1×1 pixel in PHP.
  • Mitigate bot scraping.
  • Publish the site and forget it.
  • Keep backups.

Some Htaccess Thought Experiments:

Now that I’ve listed my serverless requirements, we can brainstorm how to handle the .htaccess directives:

  • If we simply delete the .htaccess file, then:
    • No rewrites at all.
    • No 401, 403, 301, 302, 500, etc. response codes—ever.
    • Only 200 and 404 response codes are natively supported by S3.
    • Deleted pages in the future return 404 instead of 410 (Gone).
    • It’s impossible to 301-redirect pages—only duplicate content remains.
    • Cloudflare allows only three Page Rules, and one is already used for HTTP → HTTPS.
    • We will face major SEO issues in the future.
  • If we run a thin Apache server, then:
    • We’ll need a host or an EC2 instance.
    • It’ll be slower than serving an index.html directly from S3.
    • It will make me sad—if I must use a server, it should be Nginx.
  • If we use an AWS Gateway with an incoming Lambda, then:
    • Problem solved, but…
    • Each GET request incurs a cold-start Lambda delay (bad for SEO).
    • All images and assets are either piped through the Lambda or redirected with a 302 (bad for SEO).
    • I’d be tightly coupled to AWS and the fragile glue between its services.
    • I’d need to write a mini-server in Python or Node, hard-code and maintain rules, and then…
      • I will cry myself into a coma.
  • If we serve static files from S3 and use the 50 allowed redirection rules, then:
    • 404 errors are easy to handle.
    • Maintaining a JSON file of URL redirects is manageable.
    • Still no status-code headers—just 302 responses.
    • S3 accepts only HTTP requests and drops HTTPS.
    • S3 doesn’t use gzip to return objects.
    • Metadata (Last-Modified, ETag) can be set, but only via standalone PUT requests.
    • Easy redirects can go in JSON, and 410-Gone rules can live in a Lambda, but…
      • I’d have to maintain redirect rules in both S3 and Lambda—two places, deeper coma.
  • If we use Cloudflare Pages, then:
    • We’d be locked into another vendor’s configuration system.
    • You’d immediately notice that we’re not using Vue, Angular, React, Jekyll, or Hugo.
    • Those gigabytes of images and assets still need a home—and definitely not GitHub!
  • If we use Cloudflare Workers, then:
    • We’re still vendor-locked.
    • The supported languages are vanilla JavaScript, TypeScript, and Rust.
    • It’s still coding a thin server—just in a simpler, Lambda-like Worker.
    • We get WAF, caching, redirects, TLS (HTTPS), logging, and more—for free.
Fun Fact: I once worked with a company that tried to offload thousands of WordPress images to S3 from WordPress and keep them in sync (delete, resize, etc.), and it was an unmitigated disaster. I’ve learned that either everything lives in S3, or nothing does. Also, do not shard3 your website across GitHub repos just to bypass the 1 GiB repository limit.

Top ↩


The Plan for Apache’s Htaccess

So, what am I going to do with that core Apache file?

  • Turn off any Hide My WordPress obfuscation plugins. With static HTML, who cares if anyone sees /wp-uploads/ anymore? A bot can hammer the non-existent /wp-admin/ all day long. Most of the rewrite rules will then elegantly vanish. These can all be removed:
  • Turn off local compression. Yes, you read that right. I have an abandoned plugin called BWP Minify that caches content in gzip-compressed files. There are too many ways compression can backfire, especially since S3 assets won’t have a compression header. By turning off compression for offline assets, we ensure HTML, CSS, and JS are stored and crawled correctly for static offline use.
  • Remove all the wonderful attack-mitigation directives. For a static presentation site hosted on S3, I say go ahead—automated XSS injection? Bot farmers? Bring it on. These can all be removed:

  • Set edge cache expiration and browser cache settings in Cloudflare under the caching section, instead of using .htaccess. These can all be removed:
  • Remove all the WordPress rules in .htaccess.

  • Remove security rules in .htaccess. These can all be removed:

  • Remove all the 410-gone redirects because “currently Google treats 410s (Gone) the same as 404s (Not found), so it’s immaterial to [Google] whether you return one or the other. (ref)”

  • What is left is dynamic redirects and important headers concerning affiliate links. Plus, I will sometimes give a custom URL in an email link (e.g. ericdraken.com/linkedin4/ –> ericdraken.com/) and can detect if the recipient has clicked on it. For this, there has to be some dynamic processing. The remainder of the .htaccess essentially looks like this:

  • For dynamic URL redirects, I will use S3 redirects and explore vanilla JS with Cloudflare Workers. See the next section.

  • For static redirects, S3 redirects work, as well as using regular HTML redirects like so:

Summary

For most static WordPress sites, you can just delete the .htaccess file. For typical URL redirecting needs, there are easy solutions like those above. We no longer need security rules, so those can go. If you have a lot of dynamic affiliate links or cloaked URLs, that is when you need S3 redirect rules, Worker JavaScript redirects, or paid Cloudflare redirects. For static redirects, you can effectively use HTML redirects.

Top ↩


Step 15. Set up a Cloudflare Worker Dev Environment

Say I need something dynamic triggered by a web page. Cloudflare has a Lambda alternative called Workers that runs JavaScript or Rust and has no cold start (there are severe limitations, however).

Cloudflare workers

This is a simpler solution than breaking out IAMs, Roles, Route53, cold-start Lambdas, monitoring, metrics, maybe Terraform, yadda-yadda because Cloudflare takes care of all of that in your account.

FYI, the Worker needs to be uploaded from the console, but the good news is that it can be kept under version control offline. An example of a vanilla JS worker is this:

Install the Workers CLI

Cloudflare suggests installing Node.

828 MB just to install Node? Are you kidding?

828 MB just to install Node? Are you kidding?

Let’s use a Node Docker container with Wrangler (Cloudflare’s CLI tool) instead. I’ve created a Dockerfile and updated the docker-compose.yaml and .env files (I’ll refine these further later on):

Why is this so cool? Now I can run docker-compose run --rm wrangler2 and communicate with Cloudflare Workers from the VM. I can also test my Workers offline in dev mode. Awesome. First, I’ll need to create a Workers token in the Cloudflare dashboard.

API Token: As of this writing, a beta version of Wrangler v2 is available. To use an API token, you must pass CLOUDFLARE_API_TOKEN as an environment variable to the Docker container.

Cloudflare API token

I’ve created a sample Worker and am running it locally:

Gotchas: The Wrangler v2 CLI is fantastic, but there are a few gotchas. Run wrangler dev --ip :: to bind to all IPs—instead of using 0.0.0.0 or 127.0.0.1.
Signs of life from a Worker
Signs of life from a Worker
Wrangler dev is helpful for debugging
Wrangler dev is helpful for debugging

Now we can have fun exploring Workers.

With rich request information and a map function, we can recreate those .htaccess rules.

Top ↩


Step 16. Serve Dynamic Content, Formerly PHP Scripts

I have a tracking URL that looks like this:

https://innisfailapartments.com/pixel/
  ?loc=/contact/form
  &ua=Mozilla/4.0 (MSIE 6.0; Windows NT 5.1)
  &rnd=6242006519216

I created a tracking pixel many years ago, and I’m not even sure it does anything anymore. But it’s supposed to execute a PHP script:

Okay, so it sends visitor logs to Twitter (for some reason). Back then, Slack wasn’t mainstream, and I was overseas. I think I casually wanted to gauge visitor interest in that site.

Additionally, the active WordPress theme renders HTML based on the User-Agent header (e.g. iPhone, Edge, Chrome, etc.). For example:

How would I run PHP in a serverless setup?

Some ideas:

  • Rewrite the PHP into TypeScript to run in a Worker.
    • Easiest to implement and test locally. Cloudflare provides a transpiler.
  • Host a tiny PHP-FastCGI server on Vultr, DigitalOcean, Linode, etc., just to execute PHP.
    • Too much overhead, but might be necessary in rare cases.
  • Run the PHP script in AWS Lambda, which supports PHP.
    • A rewrite of the PHP code will still be needed—and possibly more.
    • We’d then have both a Lambda and a Worker.
  • Use AWS or Cloudflare metrics and decommission the script.4
    • Easiest to implement: just use Cloudflare metrics and/or GTM metrics.
  • Remove theme elements that inspect headers and switch to responsive CSS.
    • A good idea to implement regardless.

Fortunately, I can drop the tracking pixel and scrape detection for this simple website. I found it easy to rewrite some PHP scripts into JavaScript.

Saving Files: You might be caught off guard when you need persistent storage that previously relied on a server’s filesystem. Consider using Durable Objects (a paid option) or the key-value store—a free, slow, eventually consistent storage solution with a 25 MB limit.

Top ↩


Step 17. Publish a Cloudflare Worker

Worker Quota: We are limited to 100,000 Worker invocations per day across all sites. If you front-load your website with a Worker, and a typical page with images, scripts, styles, icons, etc. triggers, say, 25 requests, then you can only serve 4,000 views per day. If you have ten sites, that’s just 400 views per site per day. If a bot attempts to brute-force your site, that could consume hundreds of requests. And if Googlebot comes along to index your site(s), you’re completely overwhelmed. Do not front-load your website with Workers.
Worker Duration: We are limited to 10 ms of CPU time per request on the free plan. If you proxy a large file from S3, you’ll exceed that limit. Do not front-load your website with Workers.

When the 100,000 invocation limit is reached, an error will be returned to the web browser. The good news: you can define multiple routes for a Worker. For example,

https://innisfailapartments.com/pixel/*

can be a route. Let S3 serve 404s and real pages, and invoke the Worker only when needed. Here’s a production Worker that doesn’t do much—except exist:

Given the following wrangler.toml:

Publishing is as simple as:

Additional Workers Topics

Please explore these concepts if you are excited to use Workers for your dynamic needs.

  • Durable Objects (paid feature)
  • Key-Value Storage (free, 25MB limit, eventual consistency)
  • HTMLRewriter (because DOMParser is unavailable)

Top ↩


Step 18. Prevent Bots from Hammering Your Workers

Someone with an ax to grind might come along, write a multi-threaded URL blaster, and hammer your Worker until you hit the daily limit—in under 2 minutes.5 Suppose they really dislike you and set a cron job to run every day at 12:01. You’re hooped. Why do I even come up with these scenarios? I have experience with AdWords click fraud.

Mitigation: WAF

You could enable WAF rate limiting, but it costs about a nickel per 10,000 legitimate requests—this can add up quickly.

Cloudflare WAF rate limiting at 5¢ for every 10,000 good requests
Cloudflare WAF rate limiting at 5¢ per 10,000 good requests

Mitigation: Short Cache

You could add a Page Rule to cache Worker responses for 30 seconds and ignore query strings for caching. Combine that with a Transform Rule to strip any no-cache headers from clients. This works, but it’s advanced and has trade-offs. It works because a bad actor might spin up 16 threads for I/O to your Worker, but only one thread actually triggers execution. The downside: the Worker might cache things like GeoIP data if you’re not careful.

Mitigation: Polymorphic Worker Endpoints

If you’re a Champion of Cheap—or just enjoy mental gymnastics—you can do something clever for free: Have a Worker create a (cache) object in S3 (not Cloudflare), and configure an S3 Policy to redirect to the Worker only if that object doesn’t exist. After a few seconds, a subsequent Worker request can delete and regenerate that object for caching again. S3 handles the load, and you can cache for as little as 5 seconds.

Want to get cheeky? Have a Worker polymorphically change its endpoint via the Cloudflare API (and update S3 too) to keep the b@stards from hitting the same Worker URL directly.

Or do nothing and be kind to everyone. Or just pay Cloudflare. Your call.

Top ↩


Step 19. Cron Jobs without a Server

How can you schedule cron jobs in this simple serverless design? Cloudflare is awesome—and once again comes to the rescue:

Cloudflare cron triggers

It’s straightforward to create a cron job that periodically invokes a Worker. That Worker can do anything—from rotating logs in S3 to fetching the latest Cloudflare CIDR blocks, to running batch GeoIP lookups on visitor IPs and generating neat graphs. The sky’s the limit.

To leave GoDaddy, I don’t actually need a cron job here.

Top ↩


Step 20. Move Comments and Schedules to a Third Party

If you allow comments on your WordPress site: stop.

There are too many common XSS vulnerabilities where a bad actor leaves a malicious message like:

I love your site! This is so helpful. A+ <img src=”data-uri:base64 1×1-pixel” onload=”alert(‘Add backdoor user.’)”></a>

As an admin, you check the comments “held for moderation,” but as soon as you do, it’s too late. While logged in as an admin, you’ve just triggered JavaScript:

You’d never know. The hacker—or a bot—can modify your WordPress site with full admin privileges. Trust me.

When you move to S3, you’ll no longer have comments or interactive WordPress scheduling. I suggest using something like Disqus and JaneApp to retain interactivity. For my needs, I prefer no comments at all.

Top ↩


Advanced Serverless Options

Honestly, I just need to get away from GoDaddy. Here are some impressive features that Cloudflare offers—for free. Feel free to let your imagination run wild with the possibilities:

  • Serve compressed HTML and assets.
  • Enable HTTP/3 with QUIC for better speed.
  • Use TLS stapling for faster TLS handshakes.
  • Send Early Hints for additional resources on a page.
  • Inject GTM and scripts via Cloudflare.
  • Create custom firewall rules.
  • Set up a scrape shield.
  • Block bots from visiting.
  • Enable DDoS mitigation (save money on S3 and Workers).
  • Dynamically rewrite parts of HTML using Workers.
S3 Compression: Did you know that objects stored in S3 are served uncompressed? If you upload a 100 kB file of spaces to S3, that full 100 kB is sent to the visitor. Some suggest compressing files offline before uploading—but wait: Cloudflare caches responses from S3 and compresses them for you. Let’s see how Cloudflare handles compression:

Cloudflare compresses objects from S3 in their cache
Cloudflare compresses objects from S3 in their cache

Cloudflare rocks. Brotli compression rocks too.

Top ↩


Advanced Concepts and Gotchas

SEO Juice

Do you want Google to give SEO juice to example.com.s3-...-amazonaws.com or example.com? Add this to your website on each page:

Protect S3

Want to prevent users from visiting example.com.s3-...-amazonaws.com?

Try a combination of these handy header scripts on every page (in a header.php). Combine them or mix and match. I use them all.

Rewrite HTML

Want to edit server-side HTML on the fly with Cloudflare’s HTMLRewriter? You can do some pretty cool server-side rewrites and the client would never know.

However, there’s a major gotcha: you only get 10 ms and limited memory to run a Worker. So forget using DOMParser or complex regex if your goal is to modify HTML templates on the fly. You’ll need to get creative if you want to serve dynamically populated HTML without relying on client-side JavaScript (since SEO strongly favors server-side-rendered content).

Hint: If you can lazy-load an image, you can solve this elegantly using Cloudflare’s fast, linear-scanning HTMLRewriter. Have fun—it makes a great interview question.

Serve Third-Party Fonts ‘Locally’

Let’s serve a cached copy of Google Fonts instead of pulling them from their CDN. Why? Every external domain requires a DNS lookup and a TLS handshake, both of which slow down asset loading—and may delay the site from painting. In DevTools, it’s easy to find the CSS and WOFF files and “host” them yourself.

Reverse-engineer the CSS and WOFF files for third-party fonts
Reverse-engineer the CSS and WOFF files for third-party fonts

Top ↩


Checklist: Move Sites from GoDaddy

I operate dozens of sites doing all kinds of things, but I really want to get this site off GoDaddy. Let’s review and automate the steps to break up with GoDaddy.

  1. Create or reuse the ByeDaddy VM.
  2. Convenience symlink: ln -s ~/godaddy/homedir/public_html/example_com ~/example.
  3. Confirm a ‘staging’ folder for the site exists.
  4. Create a docker/ folder; copy the docker-compose.yaml, .env, Dockerfile, and php.ini.
  5. Update the .env file with the wp-config.php values.
  6. Create the logs/ folder; add debug.log and apache_errors.log.
  7. Copy the aws/ folder with the policy and CORS JSON files.
  8. Update the JSON files with the new site domain name.
  9. Run aws configure if this is the first time.
  10. Create the two S3 buckets: example.com and static.example.com.

  11. Change the buckets to static websites.

  12. Set the public policy of the buckets.

  13. Allow CORS for the buckets.

  14. Stop any other local sites: docker-compose down or Ctrl+C.
  15. Block commercial plugins from phoning home:

  16. Start the WordPress staging site: docker-compose up; confirm the SQL import succeeded.
  17. Edit /etc/hosts; add 127.0.0.1 example.com and 127.0.0.1 static.example.com.
  18. Visit https://example.com/wp-admin/; verify the site looks correct.
  19. (Optional) Keep yourself logged in longer for local WordPress dev work.

  20. Edit .htaccess; remove any .htpasswd references.
  21. Disable any Hide My WP plugins or similar.
  22. Remove any production/* contents (not the folder itself!).
  23. Generate static HTML and assets to a newly created production/ folder (confirm /var/www/production/).
  24. Fix any problems with static HTML generation.

    Repair any static HTML generation problems
    Repair any static HTML generation problems

  25. Address any 404 or 30X issues, and especially any 50X errors.

    Address any HTML error codes on static generation
    Address any HTML error codes on static generation

  26. Generate static HTML and assets, again.
  27. View the static production site offline with a custom vhosts.conf.

    Tip: To view the static HTML files, you will still need a vhosts. I recommend modifying the WordPress Docker container to serve the production/ folder. Don’t forget about CORS, even offline. Hint:

  28. Spot-check for broken images or broken pages.
  29. (Optional) Replace strings or URLs throughout the site:

  30. (Optional) Create a Worker; test offline; deploy; set the DNS entry if needed.
  31. (Optional) Create a redirect.json for S3 for URL redirects and/or Workers. Include the index and error documents again:

  32. (Optional) Push the bucket policy file to all site buckets:

  33. Sync the production folder to (both of) the S3 buckets:

  34. Enroll in the Cloudflare Email Routing service; enable catch-all.
  35. Add or replace MX and TXT records in the DNS settings.
  36. Send a test email to catchme@example.com to verify that email forwarding works.
  37. Update Cloudflare DNS CNAME records to point to the corresponding S3 buckets:
    • CNAME: example.com → example.com.s3-website-us-west-2.amazonaws.com
    • CNAME: static → static.example.com.s3-website-us-west-2.amazonaws.com
  38. Clear the Cloudflare cache: Purge Everything. Here is a shell script:

  39. Test URL redirection; test affiliate links, etc.
  40. Check for broken outbound links and fix or remove:

    Check for broken outbound links
    Check for broken outbound links

  41. Back up the running SQL database. Please see my Stack Overflow post for a detailed explanation.

  42. Cancel your GoDaddy hosting account.
  43. Set Cloudflare Edge Cache rules.

    Set Cloudflare Edge Cache rules
    Set Cloudflare Edge Cache rules

Top ↩


The GoDaddy Experience Simulator

Friends, GoDaddy’s cachet from the dot-com boom has long faded. Since the founder sold his majority stake in 2011, it has transformed into a profit grinder—built on aging hardware, cobbled-together third-party portals, and a mess of upselling. GoDaddy is a remnant of a bygone era, in a landscape where AWS, Linode, and Vultr (to name a few) treat hosting as their core business.

Here is a video of me trying to cancel my hosting account. This is me in real time, struggling through the broken GoDaddy interface. You’ll see that many pages are non-functional, so I had to reverse-engineer the JavaScript in the browser console to bypass the “chat with an agent first” blocker—and jump straight to the satisfying denouement.

GoDaddy, just… ByeDaddy.

Did you experience how broken and painful the GoDaddy backend is?

Top ↩


Results and Summary

Hello from S3. I’ve moved ericdraken.com to S3 + Cloudflare, and you’re reading this from Cloudflare’s cache with S3 as the origin. I expect to pay about 12 cents a month to host this site. Let’s see how fast it loads:

Ericdraken.com serverless website performance
Ericdraken.com serverless website performance

The waterfall and timings look beautiful, too.

Excellent load speeds with S3 + Cloudflare
Excellent load speeds with S3 + Cloudflare

Success

When you consider that a WordPress site with a heavy theme and dozens of plugins can take 5–8 seconds to load under Apache and PHP, achieving sub-one-second load times feels nearly miraculous.

Success: I broke up with GoDaddy. I no longer pay $18/month for one shared vCPU competing with hundreds of tenants on a near-decade-old, 6-core, EOL’d Linux server, getting only 512MB of RAM on a tired, maxed-out machine. I’m now serverless. My sites have never been faster—amazingly faster.
Success: I don’t have to lose my mind with IAMs, ACLs, Roles, Route53, regions, AZs, CloudFront, AMIs, RDS, SES, or the weeks of pain it takes to properly set up AWS hosting—or to set up and troubleshoot Terraform. I no longer maintain AWS infrastructure, migrations, deprecations, new versions, patches, and so on.
Success: My email works. I’m no longer hackable via WordPress. I have an industry-hardened Web Application Firewall (WAF) and DDoS protection through Cloudflare. I get a global CDN by default. I even get GeoIP data for free. Best of all, I pay only a few cents per month. Truly a success.

ByeDaddy featured


Notes:

  1. SRE = Site Reliability Engineer
  2. You probably didn’t consider that serverless buckets return content uncompressed, which can actually slow down your site.
  3. Shard = splitting your website across multiple repositories.
  4. 404 errors from this tracking pixel still show up in logs.
  5. A nasty technique is to skip waiting for ACK responses before launching the next salvo.