Hosting WordPress on AWS ECS with Fargate, EFS, and CDK

Over the years, I’ve had my share of developing and hosting WordPress websites, both for personal projects and for my clients. My feelings towards WordPress are mixed. On the one hand, it excels as a content management system, especially with its intuitive Block editor within the admin. On the other hand, I have reservations about the plugin system and the general experience of hosting WordPress sites; dealing with site hacks on several occasions has been challenging. However, I have to admit my perspective might be colored by the time I spent managing WordPress installs for my web development students.

Lately, I’ve been focused on creating solutions and applications on AWS and I wanted to document the journey, I figured it could be fascinating to try WordPress again, particularly hosting it with AWS’s serverless containers. This article will share my experience as a case study. For those who prefer to get straight to the solution, all the necessary code is available at the provided Github repository.

Hosting WordPress Cheaply on the Cloud

I wanted to set up WordPress in a way that was very cost-effective, aiming to spend no more than $20 each month. This is a better deal than what you would spend with WP Engine, probably the best WordPress hosting provider that also created the Frost theme used on this site.

Typically, running WordPress isn’t the cheapest option because it needs a relational database like MySQL or PostgreSQL. You could try to keep costs under $20 using Amazon’s EC2 + EBS for your website and RDS for your database. However, if your site gets busier, and you require more resources like load balancers or additional EC2 instances, prices go up.

To tackle this, I began by researching how others have managed similar situations and what the most recommended strategies are. I was interested in using AWS Fargate, a serverless solution that can adjust its resources based on what is required. The costs can get really low if Spot instances are used, which WordPress with PHP could be ideal for this.

While there are plenty of guides on how to put WordPress on services like EC2, Elastic Beanstalk, and containers, these options didn’t fit the serverless criteria I was looking for. Luckily, I did find an article that explained how to set up WordPress just the way I wanted.

Running WordPress on Amazon ECS on AWS Fargate with Amazon EFS
by Re Alvarez-Parmar and Jimmy Hayes

The article showed the solution to the storage issue with a WordPress website. WordPress requires persistent storage. With EC2, this can be provided by Elastic Block Store volumes. The Elastic File System is an even better solution because it can be used by many instances simultaneously. Fortunately, EFS can also be mounted on ECS containers. Then, once you integrate multiple serverless Fargate task containers in ECS with a load balancer, you have the perfect solution to host a WordPress site on serverless infrastructure that can scale in and out while keeping costs low.

The WordPress CDK Stack

Now to the implementation. In Re and Jimmy’s article, they used a combination of CloudFormation and AWS CLI commands to implement the infrastructure. Instead of following along with their process, I wanted to see if I could code out the infrastructure with the CDK, so I went about the process of converting parts of their CloudFormation template and CLI commands using the CDK.

This CDK stack that I created uses the following services:

  • Route53
  • EC2 NAT Instances
  • VPC with 2 AZs
  • Secrets Manager
  • Elastic File System
  • Application Load Balancer
  • ECS Cluster with Fargate Spot
  • Bitnami WordPress image

Most of the work is done using the ECS Patterns ApplicationLoadBalancedFargateService construct. This does most of the configuration for the Fargate tasks with a Load Balancer.

// Configure the Fargate service with a load balancer
const albFargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'FargateService', {
  listenerPort: 443,
  cpu: 256, 
  capacityProviderStrategies: [{
    capacityProvider: 'FARGATE_SPOT',
    base: 2,
    weight: 2
  desiredCount: 2,
  enableExecuteCommand: true,
  taskImageOptions: { 
    image: ecs.ContainerImage.fromRegistry("bitnami/wordpress"),
    containerName: 'WordpressContainer',
    containerPort: 8080,
    enableLogging: true,
    environment: {
      "MARIADB_HOST": dbInstance.dbInstanceEndpointAddress,
      "WORDPRESS_DATABASE_PASSWORD": wordpressSecret.secretValueFromJson('password').unsafeUnwrap(),
      "PHP_MEMORY_LIMIT": "512M",
      "enabled": "false",
  memoryLimitMiB: 512,
  publicLoadBalancer: true,
  certificate: certificate,
  taskSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
  vpc: vpc

WP Authentication Loop Issue

Once I worked through the initial implementation of the stack, I ran into a couple of issues. The first was an authentication loop when trying to log in into the WordPress Admin. This required a couple of edits to the wp-config.php.

define( 'FORCE_SSL_ADMIN', true); \
define( 'WP_HOME', 'https://' . $_SERVER['HTTP_HOST'] . '/' ); \
define( 'WP_SITEURL', 'https://' . $_SERVER['HTTP_HOST'] . '/' );

To change the file, it’s a simple one-time task since WordPress is set up on a storage area that stays the same for the Fargate services. However, accessing the file is not as straightforward. I had to use a special command line tool for AWS to connect to and use a Bash shell within a Fargate task to change this file directly on the EFS.

aws ecs execute-command  \
    --region us-east-1 \
    --cluster [cluster-name] \
    --task [task id, for example 0f9de17a6465404e8b1b2356dc13c2f8] \
    --container WordPressContainer \
    --command "/bin/bash" \

The Daily Costs of Managed NAT Gateways

I had to tackle a second problem, which was about keeping costs down. I had my website running for a few weeks before I realized the expected bill for using the hosting service (AWS) was going to be about $80 for the month. That was way more than my budget of $20 to $30. When I looked closer, I found out that the extra costs were coming from a part of the service labeled as “EC2 Other.” The main expense was from something called managed NAT Gateways, which I was using in my setup. These were adding around $2 to my bill every day. They got set up because of the way I chose to manage traffic to my website. To cut costs, I decided to try turning off these gateways to see if my website could still work fine without them.

NAT Gateways are like bridges that allow a hidden area of a computer network to connect to the Internet. Technically, you can have a WordPress website without this bridge. But, to get new features or security improvements for your website’s add-ons, designs, or the WordPress program itself, you need this connection to the outside world. Also, without a NAT Gateway, your website might run slower for visitors and while you’re managing it. This is because WordPress tries to reach out to the internet even when it’s just loading pages or doing routine tasks.

The EC2 NatInstanceProvider helps you set up NAT (Network Address Translation) instances in your cloud network. These instances perform the same job as the more expensive, pre-managed gateways, but you’ll have to look after them yourself. This includes taking care of their performance and resolving any issues. To save on costs, I’m not using an automatic system to scale and monitor them right now, but I might do so in the future.

Good Availability and Performance with Low Cost

I’m satisfied with how the final setup is working, both in how it performs and what it costs. I’m running a minimum of 2 to 4 Fargate Spot instances across two AZs, so the performance and availability are pretty good. I’m running snapshot backups on the RDS because I would rather not turn on Multi-AZ just yet. The only bottleneck I’m tracking is with the small MySQL RDS instance, and I’m watching that to scale it up, add a read replica, work on caching, or will probably move it to Aurora in the future.

For more details and to follow updates, checkout the repo on GitHub.