Skip to content

WordPress + MySQL on Compute

Template · Updated Jul 2026
Validated Jul 2026

WordPress + MySQL on compute

This pattern composes Compute, Network, and Block Storage.

What this template does#

Provisions a WordPress application server and a dedicated MySQL database instance:

  • WordPress instance with Nginx and PHP-FPM, installed and wired to the database by cloud-init
  • MySQL (MariaDB) instance on a private network (no public access), with its data directory on the attached volume (mounted at /var/lib/mysql on /dev/sdb)
  • Block storage volume for the MySQL data directory
  • Security groups restricting MySQL access to the private subnet only
  • Floating IP for public WordPress access

On apply, cloud-init installs MariaDB and creates the db_name database and db_user role on the MySQL instance, then installs Nginx, PHP-FPM, and WordPress on the web instance and points wp-config.php at the database. Reach the WordPress installation screen at the floating IP once cloud-init finishes. Set db_password to a strong value when you apply; the template ships no default password.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist in your project)required
wp_flavorWordPress instance sizes1a.small
db_flavorMySQL instance sizem2a.large
db_volume_sizeMySQL volume in GB20
image_nameOperating system imageUbuntu-24.04
wp_domainWordPress site domainNo default
db_nameWordPress database namewordpress
db_userWordPress database userwordpress
db_passwordPassword for the database user (required, no default)none
external_networkExternal network for router gateway and floating IPPublicStatic
private_cidrAddress range for the private subnet between WordPress and MySQL192.168.10.0/24

When to use this pattern#

Run WordPress and MySQL on separate compute instances with block-backed boot disks. Choose Self-Managed PostgreSQL for a Postgres-only database host, MySQL/MariaDB Database for a standalone MySQL-compatible target without the WordPress application tier, or Full-Stack Application when you need web, app, and database tiers with distinct subnets.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on a mix of shared and dedicated vCPU.

Starting template$82.30/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

Wordpress

s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50/mo

MySQL database

m2a.large · 2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

s1a.small

2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50

m2a.large

2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps

$66.00

Compute + RAM rate basis

4 vCPU + 10 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (60 GiB)

60 GiB at $0.08/GiB/mo

$4.80

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Dev/test vs production

Start on shared CPU for dev/test, then promote to dedicated for production with a flavor resize. The network, storage, and template stay the same.

Dev/test on shared CPU

Burstable s1a flavors; suited to prototyping and low or bursty load.

$32.80/mo

Production on the configured CPU

The headline estimate above; predictable steady-load performance.

$82.30/mo

Saves $49.50/mo while you build on shared CPU.

Shared flavors carry less RAM (m2a.large (8 GiB RAM) -> s1a.small (2 GiB RAM)). A resize reboots the instance; data on attached volumes persists. Size the dedicated flavor for the RAM your production workload needs.

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

8 files. Download the zip or expand to copy any file.Download wordpress-mysql.zip
Show source (8 files)
main.tfHCL
locals {
  name_prefix = replace(var.wp_domain, ".", "-")
}

data "openstack_images_image_v2" "image" {
  name        = var.image_name
  most_recent = true
}

data "openstack_networking_network_v2" "external" {
  name = var.external_network
}


resource "openstack_networking_network_v2" "private" {
  name           = "${local.name_prefix}-private"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "${local.name_prefix}-private-sn"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  enable_dhcp     = true
  dns_nameservers = ["8.8.8.8", "8.8.4.4"]
}

resource "openstack_networking_router_v2" "router" {
  name                = "${local.name_prefix}-router"
  external_network_id = data.openstack_networking_network_v2.external.id
  admin_state_up      = true
}

resource "openstack_networking_router_interface_v2" "private" {
  router_id = openstack_networking_router_v2.router.id
  subnet_id = openstack_networking_subnet_v2.private.id
}

resource "openstack_networking_secgroup_v2" "wordpress" {
  name        = "${local.name_prefix}-wordpress-sg"
  description = "SSH, HTTP, and HTTPS from any IPv4"
}

resource "openstack_networking_secgroup_rule_v2" "wordpress_ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.wordpress.id
}

resource "openstack_networking_secgroup_rule_v2" "wordpress_http" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 80
  port_range_max    = 80
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.wordpress.id
}

resource "openstack_networking_secgroup_rule_v2" "wordpress_https" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 443
  port_range_max    = 443
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.wordpress.id
}

resource "openstack_networking_secgroup_v2" "mysql" {
  name        = "${local.name_prefix}-mysql-sg"
  description = "MySQL only from the private subnet"
}

resource "openstack_networking_secgroup_rule_v2" "mysql_mysql" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 3306
  port_range_max    = 3306
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.mysql.id
}

resource "openstack_networking_port_v2" "wordpress" {
  name               = "${local.name_prefix}-wordpress-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.wordpress.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "wordpress" {
  name        = "${local.name_prefix}-wordpress"
  flavor_name = var.wp_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/wordpress.yaml", {
    db_ip       = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
    db_name     = var.db_name
    db_user     = var.db_user
    db_password = var.db_password
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.image.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.wordpress.id
  }
}

resource "openstack_networking_port_v2" "mysql" {
  name               = "${local.name_prefix}-mysql-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.mysql.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "mysql" {
  name        = "${local.name_prefix}-mysql"
  flavor_name = var.db_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/mysql.yaml", {
    db_name     = var.db_name
    db_user     = var.db_user
    db_password = var.db_password
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.image.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.mysql.id
  }
}

resource "openstack_blockstorage_volume_v3" "mysql_data" {
  name = "${local.name_prefix}-mysql-data"
  size = var.db_volume_size
}

resource "openstack_compute_volume_attach_v2" "mysql_data" {
  instance_id = openstack_compute_instance_v2.mysql.id
  volume_id   = openstack_blockstorage_volume_v3.mysql_data.id
}

resource "openstack_networking_floatingip_v2" "wordpress" {
  pool = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "wordpress" {
  floating_ip = openstack_networking_floatingip_v2.wordpress.address
  port_id     = openstack_networking_port_v2.wordpress.id
}
variables.tfHCL
variable "wp_domain" {
  description = "FQDN for the WordPress site (used for resource naming)"
  type        = string
}

variable "wp_flavor" {
  description = "Flavor for the WordPress compute instance"
  type        = string
  default     = "s1a.small"
}

variable "db_flavor" {
  description = "Flavor for the MySQL compute instance"
  type        = string
  default     = "m2a.large"
}

variable "db_volume_size" {
  description = "Size in GB for the MySQL data block volume"
  type        = number
  default     = 20
}

variable "image_name" {
  description = "Name of the image to boot instances from"
  type        = string
  default     = "Ubuntu-24.04"
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
  type        = string
  default     = "PublicStatic"
}

variable "key_name" {
  description = "Existing SSH keypair name in the project for compute instances"
  type        = string
}

variable "private_cidr" {
  description = "CIDR of the private subnet between WordPress and MySQL"
  type        = string
  default     = "192.168.10.0/24"
}

variable "db_name" {
  description = "WordPress database name created on the MySQL instance"
  type        = string
  default     = "wordpress"
}

variable "db_user" {
  description = "WordPress database user the application connects as"
  type        = string
  default     = "wordpress"
}

variable "db_password" {
  description = "Password for the WordPress database user. Required, no default, so no credential ships with the template."
  type        = string
  sensitive   = true
}
outputs.tfHCL
output "wordpress_ip" {
  description = "Floating IPv4 address of the WordPress instance"
  value       = openstack_networking_floatingip_v2.wordpress.address
}

output "mysql_private_ip" {
  description = "Fixed IPv4 of the MySQL instance on the private network"
  value       = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
}
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
# Required
wp_domain = "YOUR_WORDPRESS_FQDN"
key_name  = "YOUR_KEY_NAME"

# wp_flavor = "s1a.small"
# db_flavor = "m2a.large"
# db_volume_size = 20
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# private_cidr = "192.168.10.0/24"
cloud-init/mysql.yamlYAML
#cloud-config
package_update: true
runcmd:
  - |
    set -e
    export DEBIAN_FRONTEND=noninteractive
    # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
    DEV=/dev/sdb
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
    mkdir -p /var/lib/mysql
    mount "$DEV" /var/lib/mysql
    grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
    for i in 1 2 3; do apt-get update && break; sleep 10; done
    apt-get install -y mariadb-server
    printf '[mysqld]\nbind-address = 0.0.0.0\n' > /etc/mysql/mariadb.conf.d/99-bind.cnf
    systemctl enable mariadb
    systemctl restart mariadb
    mysql -e "CREATE DATABASE IF NOT EXISTS ${db_name};"
    mysql -e "CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_password}';"
    mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%'; FLUSH PRIVILEGES;"
cloud-init/wordpress.yamlYAML
#cloud-config
package_update: true
packages:
  - nginx
  - php-fpm
  - php-mysql
  - curl
  - tar
write_files:
  - path: /tmp/wordpress.nginx
    owner: root:root
    permissions: "0644"
    content: |
      server {
          listen 80 default_server;
          root /var/www/html;
          index index.php index.html;
          server_name _;

          location / {
              try_files $uri $uri/ /index.php?$args;
          }

          location ~ \.php$ {
              include snippets/fastcgi-php.conf;
              fastcgi_pass unix:__PHP_SOCK__;
          }
      }
runcmd:
  - |
    set -e
    export DEBIAN_FRONTEND=noninteractive
    # Install WordPress under /var/www/html
    curl -fsSL https://wordpress.org/latest.tar.gz -o /tmp/wp.tar.gz
    tar -xzf /tmp/wp.tar.gz -C /tmp
    rm -f /var/www/html/index.nginx-debian.html
    cp -a /tmp/wordpress/. /var/www/html/
    cd /var/www/html
    cp wp-config-sample.php wp-config.php
    sed -i "s/database_name_here/${db_name}/" wp-config.php
    sed -i "s/username_here/${db_user}/" wp-config.php
    sed -i "s/password_here/${db_password}/" wp-config.php
    sed -i "s/'localhost'/'${db_ip}'/" wp-config.php
    if curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/ -o /tmp/wp-salts.php; then
      sed -i "/AUTH_KEY/d;/SECURE_AUTH_KEY/d;/LOGGED_IN_KEY/d;/NONCE_KEY/d;/AUTH_SALT/d;/SECURE_AUTH_SALT/d;/LOGGED_IN_SALT/d;/NONCE_SALT/d" wp-config.php
      sed -i "/table_prefix = /r /tmp/wp-salts.php" wp-config.php
    fi
    chown -R www-data:www-data /var/www/html
    # Point nginx at the versioned PHP-FPM socket (avoid the generic php-fpm.sock alias)
    PHP_VER=$(ls /etc/php/ | sort -V | tail -1)
    PHP_FPM="php$${PHP_VER}-fpm"
    PHP_SOCK="/run/php/$${PHP_FPM}.sock"
    sed "s#__PHP_SOCK__#$PHP_SOCK#" /tmp/wordpress.nginx > /etc/nginx/sites-available/default
    systemctl enable nginx "$PHP_FPM"
    systemctl restart "$PHP_FPM"
    systemctl restart nginx
README.mdMarkdown
# WordPress + MySQL

WordPress application server with a dedicated MySQL database instance on a private subnet, router, and appropriate security groups.


**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- A valid FQDN or hostname label for naming and configuration (`wp_domain`)

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

## Variables

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `wp_domain` | string | yes | n/a | FQDN for the WordPress site (used for resource naming) |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `wp_flavor` | string | no | `s1a.small` | Flavor for the WordPress compute instance |
| `db_flavor` | string | no | `m2a.large` | Flavor for the MySQL compute instance |
| `db_volume_size` | number | no | `20` | Size in GB for the MySQL data block volume |
| `image_name` | string | no | `Ubuntu-24.04` | Name of the image to boot instances from |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `192.168.10.0/24` | CIDR of the private subnet between WordPress and MySQL |

## Documentation

Full documentation: [WordPress MySQL template](/docs/automation/templates/wordpress-mysql)

Validated variants of this template, each adding one capability the base does not include.

  • Cachewordpress-mysql-cache+$17/mo over the base

    Valkey object cache for WordPress page and query acceleration

    Show source and download
    9 files. Download the zip or copy any file.Download wordpress-mysql-cache.zip
    main.tfHCL
    locals {
      name_prefix = replace(var.wp_domain, ".", "-")
    }
    
    data "openstack_images_image_v2" "image" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    
    resource "openstack_networking_network_v2" "private" {
      name           = "${local.name_prefix}-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "${local.name_prefix}-private-sn"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      enable_dhcp     = true
      dns_nameservers = ["8.8.8.8", "8.8.4.4"]
    }
    
    resource "openstack_networking_router_v2" "router" {
      name                = "${local.name_prefix}-router"
      external_network_id = data.openstack_networking_network_v2.external.id
      admin_state_up      = true
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.router.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "wordpress" {
      name        = "${local.name_prefix}-wordpress-sg"
      description = "SSH, HTTP, and HTTPS from any IPv4"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 80
      port_range_max    = 80
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_https" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 443
      port_range_max    = 443
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_v2" "redis" {
      name        = "${local.name_prefix}-redis-sg"
      description = "Valkey object cache from the private subnet only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "redis_valkey" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 6379
      port_range_max    = 6379
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.redis.id
    }
    
    resource "openstack_networking_secgroup_v2" "mysql" {
      name        = "${local.name_prefix}-mysql-sg"
      description = "MySQL only from the private subnet"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "mysql_mysql" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 3306
      port_range_max    = 3306
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.mysql.id
    }
    
    resource "openstack_networking_port_v2" "wordpress" {
      name               = "${local.name_prefix}-wordpress-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.wordpress.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "wordpress" {
      name        = "${local.name_prefix}-wordpress"
      flavor_name = var.wp_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/wordpress.yaml", {
        db_ip       = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
        redis_ip    = openstack_compute_instance_v2.redis.network[0].fixed_ip_v4
        db_name     = var.db_name
        db_user     = var.db_user
        db_password = var.db_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.wordpress.id
      }
    }
    
    resource "openstack_networking_port_v2" "redis" {
      name               = "${local.name_prefix}-redis-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.redis.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "redis" {
      name        = "${local.name_prefix}-redis"
      flavor_name = var.cache_flavor
      key_pair    = var.key_name
    
      user_data = file("${path.module}/cloud-init/redis.yaml")
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 10
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.redis.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "mysql" {
      name               = "${local.name_prefix}-mysql-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.mysql.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "mysql" {
      name        = "${local.name_prefix}-mysql"
      flavor_name = var.db_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/mysql.yaml", {
        db_name     = var.db_name
        db_user     = var.db_user
        db_password = var.db_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.mysql.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_blockstorage_volume_v3" "mysql_data" {
      name = "${local.name_prefix}-mysql-data"
      size = var.db_volume_size
    }
    
    resource "openstack_compute_volume_attach_v2" "mysql_data" {
      instance_id = openstack_compute_instance_v2.mysql.id
      volume_id   = openstack_blockstorage_volume_v3.mysql_data.id
    }
    
    resource "openstack_networking_floatingip_v2" "wordpress" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "wordpress" {
      floating_ip = openstack_networking_floatingip_v2.wordpress.address
      port_id     = openstack_networking_port_v2.wordpress.id
    }
    
    variables.tfHCL
    variable "wp_domain" {
      description = "FQDN for the WordPress site (used for resource naming)"
      type        = string
    }
    
    variable "wp_flavor" {
      description = "Flavor for the WordPress compute instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "cache_flavor" {
      description = "Flavor for the Valkey object-cache instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "db_flavor" {
      description = "Flavor for the MySQL compute instance"
      type        = string
      default     = "m2a.large"
    }
    
    variable "db_volume_size" {
      description = "Size in GB for the MySQL data block volume"
      type        = number
      default     = 20
    }
    
    variable "image_name" {
      description = "Name of the image to boot instances from"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "private_cidr" {
      description = "CIDR of the private subnet between WordPress and MySQL"
      type        = string
      default     = "192.168.10.0/24"
    }
    
    variable "db_name" {
      description = "WordPress database name created on the MySQL instance"
      type        = string
      default     = "wordpress"
    }
    
    variable "db_user" {
      description = "WordPress database user the application connects as"
      type        = string
      default     = "wordpress"
    }
    
    variable "db_password" {
      description = "Password for the WordPress database user. Required, no default, so no credential ships with the template."
      type        = string
      sensitive   = true
    }
    
    outputs.tfHCL
    output "wordpress_ip" {
      description = "Floating IPv4 address of the WordPress instance"
      value       = openstack_networking_floatingip_v2.wordpress.address
    }
    
    output "redis_private_ip" {
      description = "Fixed IPv4 of the Valkey cache instance on the private network"
      value       = openstack_compute_instance_v2.redis.network[0].fixed_ip_v4
    }
    
    output "mysql_private_ip" {
      description = "Fixed IPv4 of the MySQL instance on the private network"
      value       = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
      }
    }
    
    provider "openstack" {}
    
    terraform.tfvars.exampleHCL
    # Required
    wp_domain = "YOUR_WORDPRESS_FQDN"
    key_name  = "YOUR_KEY_NAME"
    
    # wp_flavor = "s1a.small"
    # db_flavor = "m2a.large"
    # db_volume_size = 20
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.10.0/24"
    
    cloud-init/mysql.yamlYAML
    #cloud-config
    package_update: true
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
        DEV=/dev/sdb
        for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
        if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
        mkdir -p /var/lib/mysql
        mount "$DEV" /var/lib/mysql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y mariadb-server
        printf '[mysqld]\nbind-address = 0.0.0.0\n' > /etc/mysql/mariadb.conf.d/99-bind.cnf
        systemctl enable mariadb
        systemctl restart mariadb
        mysql -e "CREATE DATABASE IF NOT EXISTS ${db_name};"
        mysql -e "CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_password}';"
        mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%'; FLUSH PRIVILEGES;"
    
    cloud-init/redis.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - valkey-server
    runcmd:
      - |
        set -e
        # Valkey listens on the private interface only; security group restricts clients to the subnet.
        sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf
        sed -i 's/^protected-mode .*/protected-mode yes/' /etc/valkey/valkey.conf
        systemctl enable valkey-server
        systemctl restart valkey-server
    
    cloud-init/wordpress.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - nginx
      - php-fpm
      - php-mysql
      - php-redis
      - curl
      - tar
      - unzip
    write_files:
      - path: /tmp/wordpress.nginx
        owner: root:root
        permissions: "0644"
        content: |
          server {
              listen 80 default_server;
              root /var/www/html;
              index index.php index.html;
              server_name _;
    
              location / {
                  try_files $uri $uri/ /index.php?$args;
              }
    
              location ~ \.php$ {
                  include snippets/fastcgi-php.conf;
                  fastcgi_pass unix:__PHP_SOCK__;
              }
          }
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        curl -fsSL https://wordpress.org/latest.tar.gz -o /tmp/wp.tar.gz
        tar -xzf /tmp/wp.tar.gz -C /tmp
        rm -f /var/www/html/index.nginx-debian.html
        cp -a /tmp/wordpress/. /var/www/html/
        cd /var/www/html
        cp wp-config-sample.php wp-config.php
        sed -i "s/database_name_here/${db_name}/" wp-config.php
        sed -i "s/username_here/${db_user}/" wp-config.php
        sed -i "s/password_here/${db_password}/" wp-config.php
        sed -i "s/'localhost'/'${db_ip}'/" wp-config.php
        if curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/ -o /tmp/wp-salts.php; then
          sed -i "/AUTH_KEY/d;/SECURE_AUTH_KEY/d;/LOGGED_IN_KEY/d;/NONCE_KEY/d;/AUTH_SALT/d;/SECURE_AUTH_SALT/d;/LOGGED_IN_SALT/d;/NONCE_SALT/d" wp-config.php
          sed -i "/table_prefix = /r /tmp/wp-salts.php" wp-config.php
        fi
        cat >> wp-config.php <<WPCFG
        define('WP_REDIS_HOST', '${redis_ip}');
        define('WP_REDIS_PORT', 6379);
        define('WP_CACHE', true);
        WPCFG
        mkdir -p /var/www/html/wp-content/plugins
        curl -fsSL https://downloads.wordpress.org/plugin/redis-cache.latest-stable.zip -o /tmp/redis-cache.zip
        unzip -q /tmp/redis-cache.zip -d /var/www/html/wp-content/plugins/
        chown -R www-data:www-data /var/www/html
        PHP_VER=$(ls /etc/php/ | sort -V | tail -1)
        PHP_FPM="php$${PHP_VER}-fpm"
        PHP_SOCK="/run/php/$${PHP_FPM}.sock"
        sed "s#__PHP_SOCK__#$PHP_SOCK#" /tmp/wordpress.nginx > /etc/nginx/sites-available/default
        systemctl enable nginx "$PHP_FPM"
        systemctl restart "$PHP_FPM"
        systemctl restart nginx
    
    README.mdMarkdown
    # WordPress + MySQL + Valkey object cache
    
    WordPress application server with a dedicated MySQL database and a Valkey (Redis-compatible) object cache on a private subnet. Extends the [WordPress + MySQL](/docs/automation/templates/wordpress-mysql) base with a cache tier for page and query acceleration.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials
    - A valid FQDN or hostname label for naming and configuration (`wp_domain`)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    7. After deploy, activate the Redis Object Cache plugin in the WordPress admin (Plugins screen)
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `wp_domain` | string | yes | n/a | FQDN for the WordPress site (used for resource naming) |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `wp_flavor` | string | no | `s1a.small` | Flavor for the WordPress compute instance |
    | `db_flavor` | string | no | `m2a.large` | Flavor for the MySQL compute instance |
    | `cache_flavor` | string | no | `s1a.small` | Flavor for the Valkey object-cache instance |
    | `db_volume_size` | number | no | `20` | Size in GB for the MySQL data block volume |
    | `image_name` | string | no | `Ubuntu-24.04` | Name of the image to boot instances from |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `192.168.10.0/24` | CIDR of the private subnet between WordPress, MySQL, and Valkey |
    
    ## Documentation
    
    Base template: [WordPress MySQL template](/docs/automation/templates/wordpress-mysql)
    
    Full documentation: [WordPress MySQL cache variation](/docs/automation/templates/wordpress-mysql-cache)
    
  • Object Storagewordpress-mysql-media

    Object Storage offload for WordPress uploads and media

    Show source and download
    8 files. Download the zip or copy any file.Download wordpress-mysql-media.zip
    main.tfHCL
    locals {
      name_prefix = replace(var.wp_domain, ".", "-")
    }
    
    data "openstack_images_image_v2" "image" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "aws_s3_bucket" "media" {
      bucket = var.media_bucket_name
    }
    
    resource "aws_s3_bucket_acl" "media" {
      bucket = aws_s3_bucket.media.id
      acl    = "private"
    }
    
    resource "openstack_compute_keypair_v2" "deploy" {
      name = var.key_name
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "${local.name_prefix}-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "${local.name_prefix}-private-sn"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      enable_dhcp     = true
      dns_nameservers = ["8.8.8.8", "8.8.4.4"]
    }
    
    resource "openstack_networking_router_v2" "router" {
      name                = "${local.name_prefix}-router"
      external_network_id = data.openstack_networking_network_v2.external.id
      admin_state_up      = true
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.router.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "wordpress" {
      name        = "${local.name_prefix}-wordpress-sg"
      description = "SSH, HTTP, and HTTPS from any IPv4"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 80
      port_range_max    = 80
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "wordpress_https" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 443
      port_range_max    = 443
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.wordpress.id
    }
    
    resource "openstack_networking_secgroup_v2" "mysql" {
      name        = "${local.name_prefix}-mysql-sg"
      description = "MySQL only from the private subnet"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "mysql_mysql" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 3306
      port_range_max    = 3306
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.mysql.id
    }
    
    resource "openstack_networking_port_v2" "wordpress" {
      name               = "${local.name_prefix}-wordpress-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.wordpress.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "wordpress" {
      name        = "${local.name_prefix}-wordpress"
      flavor_name = var.wp_flavor
      key_pair    = openstack_compute_keypair_v2.deploy.name
    
      user_data = templatefile("${path.module}/cloud-init/wordpress.yaml", {
        db_ip         = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
        db_name       = var.db_name
        db_user       = var.db_user
        db_password   = var.db_password
        s3_access_key = var.s3_access_key
        s3_secret_key = var.s3_secret_key
        s3_endpoint   = var.s3_endpoint
        s3_region     = var.s3_region
        media_bucket  = aws_s3_bucket.media.id
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.wordpress.id
      }
    }
    
    resource "openstack_networking_port_v2" "mysql" {
      name               = "${local.name_prefix}-mysql-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.mysql.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "mysql" {
      name        = "${local.name_prefix}-mysql"
      flavor_name = var.db_flavor
      key_pair    = openstack_compute_keypair_v2.deploy.name
    
      user_data = templatefile("${path.module}/cloud-init/mysql.yaml", {
        db_name     = var.db_name
        db_user     = var.db_user
        db_password = var.db_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.image.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.mysql.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_blockstorage_volume_v3" "mysql_data" {
      name = "${local.name_prefix}-mysql-data"
      size = var.db_volume_size
    }
    
    resource "openstack_compute_volume_attach_v2" "mysql_data" {
      instance_id = openstack_compute_instance_v2.mysql.id
      volume_id   = openstack_blockstorage_volume_v3.mysql_data.id
    }
    
    resource "openstack_networking_floatingip_v2" "wordpress" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "wordpress" {
      floating_ip = openstack_networking_floatingip_v2.wordpress.address
      port_id     = openstack_networking_port_v2.wordpress.id
    }
    
    variables.tfHCL
    variable "wp_domain" {
      description = "FQDN for the WordPress site (used for resource naming)"
      type        = string
    }
    
    variable "wp_flavor" {
      description = "Flavor for the WordPress compute instance"
      type        = string
      default     = "s1a.small"
    }
    
    variable "db_flavor" {
      description = "Flavor for the MySQL compute instance"
      type        = string
      default     = "m2a.large"
    }
    
    variable "db_volume_size" {
      description = "Size in GB for the MySQL data block volume"
      type        = number
      default     = 20
    }
    
    variable "image_name" {
      description = "Name of the image to boot instances from"
      type        = string
      default     = "Ubuntu-24.04"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "private_cidr" {
      description = "CIDR of the private subnet between WordPress and MySQL"
      type        = string
      default     = "192.168.10.0/24"
    }
    
    variable "db_name" {
      description = "WordPress database name created on the MySQL instance"
      type        = string
      default     = "wordpress"
    }
    
    variable "db_user" {
      description = "WordPress database user the application connects as"
      type        = string
      default     = "wordpress"
    }
    
    variable "db_password" {
      description = "Password for the WordPress database user. Required, no default, so no credential ships with the template."
      type        = string
      sensitive   = true
    }
    
    variable "s3_access_key" {
      description = "EC2-compatible access key for Object Storage (Keystone ec2 credentials create)"
      type        = string
      sensitive   = true
    }
    
    variable "s3_secret_key" {
      description = "EC2-compatible secret key paired with s3_access_key"
      type        = string
      sensitive   = true
    }
    
    variable "media_bucket_name" {
      description = "S3 bucket name for WordPress uploads and media offload"
      type        = string
    }
    
    variable "s3_endpoint" {
      description = "Quake AI S3-compatible endpoint URL"
      type        = string
      default     = "https://object.us-east-1.rumble.cloud"
    }
    
    variable "s3_region" {
      description = "S3 region identifier passed to the AWS provider"
      type        = string
      default     = "us-east-1"
    }
    
    outputs.tfHCL
    output "wordpress_ip" {
      description = "Floating IPv4 address of the WordPress instance"
      value       = openstack_networking_floatingip_v2.wordpress.address
    }
    
    output "media_bucket" {
      description = "Object Storage bucket name for WordPress uploads"
      value       = aws_s3_bucket.media.id
    }
    
    output "mysql_private_ip" {
      description = "Fixed IPv4 of the MySQL instance on the private network"
      value       = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
    }
    
    output "keypair_private_key" {
      description = "PEM private key for the generated OpenStack keypair"
      value       = openstack_compute_keypair_v2.deploy.private_key
      sensitive   = true
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
      }
    }
    
    provider "openstack" {}
    
    provider "aws" {
      region                      = var.s3_region
      access_key                  = var.s3_access_key
      secret_key                  = var.s3_secret_key
      skip_credentials_validation = true
      skip_metadata_api_check     = true
      skip_requesting_account_id  = true
    
      endpoints {
        s3 = var.s3_endpoint
      }
    }
    
    terraform.tfvars.exampleHCL
    # Required
    wp_domain         = "YOUR_WORDPRESS_FQDN"
    key_name          = "YOUR_KEY_NAME"
    s3_access_key     = "YOUR_S3_ACCESS_KEY"
    s3_secret_key     = "YOUR_S3_SECRET_KEY"
    media_bucket_name = "YOUR_UNIQUE_MEDIA_BUCKET"
    
    # wp_flavor = "s1a.small"
    # db_flavor = "m2a.large"
    # db_volume_size = 20
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.10.0/24"
    # s3_endpoint = "https://object.us-east-1.rumble.cloud"
    # s3_region = "us-east-1"
    
    cloud-init/mysql.yamlYAML
    #cloud-config
    package_update: true
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
        DEV=/dev/sdb
        for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
        if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F "$DEV"; fi
        mkdir -p /var/lib/mysql
        mount "$DEV" /var/lib/mysql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/mysql ext4 defaults,nofail 0 2" >> /etc/fstab
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y mariadb-server
        printf '[mysqld]\nbind-address = 0.0.0.0\n' > /etc/mysql/mariadb.conf.d/99-bind.cnf
        systemctl enable mariadb
        systemctl restart mariadb
        mysql -e "CREATE DATABASE IF NOT EXISTS ${db_name};"
        mysql -e "CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_password}';"
        mysql -e "GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%'; FLUSH PRIVILEGES;"
    
    cloud-init/wordpress.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - nginx
      - php-fpm
      - php-mysql
      - curl
      - tar
      - unzip
    write_files:
      - path: /tmp/wordpress.nginx
        owner: root:root
        permissions: "0644"
        content: |
          server {
              listen 80 default_server;
              root /var/www/html;
              index index.php index.html;
              server_name _;
    
              location / {
                  try_files $uri $uri/ /index.php?$args;
              }
    
              location ~ \.php$ {
                  include snippets/fastcgi-php.conf;
                  fastcgi_pass unix:__PHP_SOCK__;
              }
          }
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DB="${db_ip}"
        for i in $(seq 1 60); do
          if nc -z "$DB" 3306 2>/dev/null; then break; fi
          sleep 5
        done
        curl -fsSL https://wordpress.org/latest.tar.gz -o /tmp/wp.tar.gz
        tar -xzf /tmp/wp.tar.gz -C /tmp
        rm -f /var/www/html/index.nginx-debian.html
        cp -a /tmp/wordpress/. /var/www/html/
        cd /var/www/html
        cp wp-config-sample.php wp-config.php
        sed -i "s/database_name_here/${db_name}/" wp-config.php
        sed -i "s/username_here/${db_user}/" wp-config.php
        sed -i "s/password_here/${db_password}/" wp-config.php
        sed -i "s/'localhost'/'${db_ip}'/" wp-config.php
        if curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/ -o /tmp/wp-salts.php; then
          sed -i "/AUTH_KEY/d;/SECURE_AUTH_KEY/d;/LOGGED_IN_KEY/d;/NONCE_KEY/d;/AUTH_SALT/d;/SECURE_AUTH_SALT/d;/LOGGED_IN_SALT/d;/NONCE_SALT/d" wp-config.php
          sed -i "/table_prefix = /r /tmp/wp-salts.php" wp-config.php
        fi
        mkdir -p /var/www/html/wp-content/plugins
        curl -fsSL https://github.com/humanmade/S3-Uploads/archive/refs/heads/master.zip -o /tmp/s3-uploads.zip
        unzip -q /tmp/s3-uploads.zip -d /var/www/html/wp-content/plugins/
        mv /var/www/html/wp-content/plugins/S3-Uploads-master /var/www/html/wp-content/plugins/s3-uploads
        cat >> wp-config.php <<WPCFG
        define('S3_UPLOADS_BUCKET', '${media_bucket}');
        define('S3_UPLOADS_REGION', '${s3_region}');
        define('S3_UPLOADS_KEY', '${s3_access_key}');
        define('S3_UPLOADS_SECRET', '${s3_secret_key}');
        define('S3_UPLOADS_BUCKET_URL', '${s3_endpoint}/${media_bucket}');
        define('S3_UPLOADS_USE_INSTANCE_PROFILE', false);
        WPCFG
        chown -R www-data:www-data /var/www/html
        PHP_VER=$(ls /etc/php/ | sort -V | tail -1)
        PHP_FPM="php$${PHP_VER}-fpm"
        PHP_SOCK="/run/php/$${PHP_FPM}.sock"
        sed "s#__PHP_SOCK__#$PHP_SOCK#" /tmp/wordpress.nginx > /etc/nginx/sites-available/default
        systemctl enable nginx "$PHP_FPM"
        systemctl restart "$PHP_FPM"
        systemctl restart nginx
    
    README.mdMarkdown
    # WordPress + MySQL + Object Storage media offload
    
    WordPress application server with a dedicated MySQL database and an Object Storage bucket for uploads and media. Extends the [WordPress + MySQL](/docs/automation/templates/wordpress-mysql) base with S3-compatible media offload so the web tier disk stays small.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials and S3-compatible Object Storage credentials
    - A valid FQDN or hostname label for naming and configuration (`wp_domain`)
    - A globally unique bucket name (`media_bucket_name`)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    7. After deploy, activate the S3 Uploads plugin in the WordPress admin (Plugins screen)
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `wp_domain` | string | yes | n/a | FQDN for the WordPress site (used for resource naming) |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `s3_access_key` | string | yes | n/a | EC2-compatible Object Storage access key |
    | `s3_secret_key` | string | yes | n/a | EC2-compatible Object Storage secret key |
    | `media_bucket_name` | string | yes | n/a | S3 bucket name for WordPress uploads |
    | `wp_flavor` | string | no | `s1a.small` | Flavor for the WordPress compute instance |
    | `db_flavor` | string | no | `m2a.large` | Flavor for the MySQL compute instance |
    | `db_volume_size` | number | no | `20` | Size in GB for the MySQL data block volume |
    
    ## Documentation
    
    Base template: [WordPress MySQL template](/docs/automation/templates/wordpress-mysql)
    
    Full documentation: [WordPress MySQL media variation](/docs/automation/templates/wordpress-mysql-media)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • wp_domainrequired
  • wp_flavor="s1a.small"
  • db_flavor="m2a.large"
  • db_volume_size=20
  • image_name="Ubuntu-24.04"
  • external_network="PublicStatic"
  • key_namerequired
  • private_cidr="192.168.10.0/24"
  • db_name="wordpress"
  • db_user="wordpress"
  • db_passwordrequired

Customize this pattern#

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 07.07.2026

Was this page helpful?