MySQL/MariaDB Database
MySQL/MariaDB database
This pattern composes Compute, Network, and Block Storage.
What this template does#
Provisions a standalone self-managed MySQL/MariaDB server with private networking, independent of any application tier:
- Dedicated compute instance sized for database workloads
- Block storage volume formatted and mounted at
/var/lib/mysql(on/dev/sdb) for the database data directory - Cloud-init installs the database engine (
db_engine_package, MariaDB by default), creates thedb_namedatabase anddb_userrole, and binds the server to all interfaces - Scheduled
mysqldump --all-databasesbackup script onbackup_schedule(writes to/var/lib/mysql/backups) - Private network: no public access to the database port
- Security group allowing MySQL/MariaDB traffic only from
allowed_cidrs
Set db_password to a strong value when you apply; the template ships no default password.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
db_flavor | Database instance size | m2a.large |
volume_size | Data volume in GB | 50 |
db_engine_package | APT package for the database engine | mariadb-server |
backup_schedule | Cron expression for backups | 0 2 * * * |
allowed_cidrs | CIDRs allowed to connect | No default |
db_name | Application database created on first boot | appdb |
db_user | Application database user created on first boot | appuser |
db_password | Password for the database user (required, no default) | none |
image_name | Operating system image | Ubuntu-24.04 |
external_network | External network for router gateway | PublicStatic |
private_cidr | Address range for the private subnet | 192.168.50.0/24 |
instance_name | Compute instance display name | mysql |
When to use this pattern#
Run MySQL or MariaDB on a single compute instance with a data volume and private network access, independent of an application tier. Choose this template over WordPress + MySQL on Compute when the workload needs a MySQL-compatible database without the bundled WordPress application, and over Self-managed PostgreSQL when the workload speaks the MySQL wire protocol rather than PostgreSQL's. Choose Full-Stack Application when the database sits behind separate web and app tiers.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on dedicated vCPU.
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
MySQL database
m2a.large · 2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
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
m2a.large
2 dedicated vCPU, 8 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 8 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (70 GiB)
70 GiB at $0.08/GiB/mo
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
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 morePrivate 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.
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.
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.
Production on dedicated CPU
The headline estimate above; predictable steady-load performance.
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#
Show source (7 files)Hide source
locals {
# The systemd service name and conf.d directory both follow the package name:
# mariadb-server -> mariadb(.conf.d), mysql-server -> mysql(.conf.d).
db_service_name = strcontains(var.db_engine_package, "maria") ? "mariadb" : "mysql"
}
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "${var.instance_name}-private"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.instance_name}-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 = "${var.instance_name}-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" "mysql" {
name = "${var.instance_name}-sg"
description = "SSH from any IPv4; MySQL/MariaDB from configured CIDRs only"
}
resource "openstack_networking_secgroup_rule_v2" "mysql" {
for_each = toset(var.allowed_cidrs)
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 3306
port_range_max = 3306
remote_ip_prefix = each.value
security_group_id = openstack_networking_secgroup_v2.mysql.id
}
resource "openstack_networking_secgroup_rule_v2" "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.mysql.id
}
resource "openstack_blockstorage_volume_v3" "mysql_data" {
name = "${var.instance_name}-data"
size = var.volume_size
}
resource "openstack_networking_port_v2" "mysql" {
name = "${var.instance_name}-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 = var.instance_name
flavor_name = var.db_flavor
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/mysql.yaml", {
db_engine_package = var.db_engine_package
db_service_name = local.db_service_name
backup_schedule = var.backup_schedule
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
})
block_device {
uuid = data.openstack_images_image_v2.os.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_compute_volume_attach_v2" "mysql_data" {
instance_id = openstack_compute_instance_v2.mysql.id
volume_id = openstack_blockstorage_volume_v3.mysql_data.id
}
variable "allowed_cidrs" {
description = "IPv4 CIDRs allowed to reach MySQL/MariaDB on port 3306"
type = list(string)
}
variable "key_name" {
description = "Existing SSH keypair name in the project for compute instances"
type = string
}
variable "db_flavor" {
description = "Flavor for the database instance"
type = string
default = "m2a.large"
}
variable "volume_size" {
description = "Cinder volume size in GiB for MySQL/MariaDB data"
type = number
default = 50
}
variable "db_engine_package" {
description = "APT package installed for the database engine. Defaults to MariaDB, a drop-in MySQL-compatible replacement and the same engine wordpress-mysql already installs. Set to mysql-server for upstream MySQL on distributions that carry it in the default repositories."
type = string
default = "mariadb-server"
}
variable "backup_schedule" {
description = "Cron schedule for the mysqldump backup script"
type = string
default = "0 2 * * *"
}
variable "image_name" {
description = "Boot image name"
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 "private_cidr" {
description = "CIDR for the private subnet hosting the database"
type = string
default = "192.168.50.0/24"
}
variable "instance_name" {
description = "Compute instance display name"
type = string
default = "mysql"
}
variable "db_name" {
description = "Application database created on first boot"
type = string
default = "appdb"
}
variable "db_user" {
description = "Application database user created on first boot"
type = string
default = "appuser"
}
variable "db_password" {
description = "Password for the application database user. Required, no default, so no credential ships with the template."
type = string
sensitive = true
}
output "instance_id" {
description = "Nova instance ID"
value = openstack_compute_instance_v2.mysql.id
}
output "private_ip" {
description = "Private IPv4 address on the dedicated subnet"
value = openstack_compute_instance_v2.mysql.network[0].fixed_ip_v4
}
output "data_volume_id" {
description = "Cinder volume ID for MySQL/MariaDB data"
value = openstack_blockstorage_volume_v3.mysql_data.id
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required
allowed_cidrs = ["YOUR_ALLOWED_CIDR"]
key_name = "YOUR_KEY_NAME"
db_password = "CHOOSE_A_STRONG_PASSWORD"
# db_flavor = "m2a.large"
# volume_size = 50
# db_engine_package = "mariadb-server"
# backup_schedule = "0 2 * * *"
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# private_cidr = "192.168.50.0/24"
# instance_name = "mysql"
# db_name = "appdb"
# db_user = "appuser"
#cloud-config
package_update: true
write_files:
- path: /usr/local/bin/mysql-backup.sh
permissions: "0755"
content: |
#!/bin/bash
set -e
install -d -o mysql -g mysql /var/lib/mysql/backups
ts=$(date +%Y%m%d-%H%M%S)
mysqldump --all-databases | gzip > /var/lib/mysql/backups/mysqldump-$ts.sql.gz
- path: /etc/cron.d/mysql-backup
owner: root:root
permissions: "0644"
content: |
${backup_schedule} root /usr/local/bin/mysql-backup.sh
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 -L mysqldata "$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 ${db_engine_package}
install -d /etc/mysql/${db_service_name}.conf.d
printf '[mysqld]\nbind-address = 0.0.0.0\ndatadir = /var/lib/mysql\n' > /etc/mysql/${db_service_name}.conf.d/99-template.cnf
install -d -o mysql -g mysql /var/lib/mysql/backups
systemctl enable ${db_service_name}
systemctl restart ${db_service_name}
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;"
# MySQL/MariaDB Database
Standalone self-managed MySQL/MariaDB on a dedicated instance with a data volume, security
rules scoped to client CIDRs, and a scheduled `mysqldump` backup. Unlike
`wordpress-mysql`, this template ships no application tier: use it when the workload needs a
relational MySQL-compatible database without a bundled CMS.
**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
- One or more trusted IPv4 CIDRs that should reach MySQL/MariaDB on port 3306
## 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 |
| --- | --- | --- | --- | --- |
| `allowed_cidrs` | list(string) | yes | n/a | IPv4 CIDRs allowed to reach MySQL/MariaDB on port 3306 |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `db_flavor` | string | no | `m2a.large` | Flavor for the database instance |
| `volume_size` | number | no | `50` | Cinder volume size in GiB for database data |
| `db_engine_package` | string | no | `mariadb-server` | APT package for the database engine |
| `backup_schedule` | string | no | `0 2 * * *` | Cron schedule for the `mysqldump` backup |
| `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `192.168.50.0/24` | Private subnet CIDR for the database |
| `instance_name` | string | no | `mysql` | Compute instance display name |
| `db_name` | string | no | `appdb` | Application database created on first boot |
| `db_user` | string | no | `appuser` | Application database user created on first boot |
| `db_password` | string | yes | n/a | Password for the application database user |
## Documentation
Full documentation: [MySQL/MariaDB Database](/resources/iac-templates/mysql-database)
Resources, parameters, and variables
allowed_cidrsrequiredkey_namerequireddb_flavor="m2a.large"volume_size=50db_engine_package="mariadb-server"backup_schedule="0 2 * * *"image_name="Ubuntu-24.04"external_network="PublicStatic"private_cidr="192.168.50.0/24"instance_name="mysql"db_name="appdb"db_user="appuser"db_passwordrequired
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
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