Skip to content
IaC Templates

Self-Managed PostgreSQL

Template · Updated Jul 2026
Validated Jul 2026

Self-managed PostgreSQL

This pattern composes Compute, Network, and Block Storage.

What this template does#

Provisions a self-managed PostgreSQL server with private networking and WAL archiving:

  • Dedicated compute instance optimized for database workloads
  • Block storage volume formatted and mounted at /var/lib/postgresql (on /dev/sdb) for the PostgreSQL data directory
  • Cloud-init installs PostgreSQL pg_version, creates the db_name database and db_user role, and enables local WAL archiving to /var/lib/postgresql/wal_archive
  • Automated pg_dumpall backup script on backup_schedule (writes to /var/lib/postgresql/backups)
  • Private network: no public access to the database port
  • Security group allowing PostgreSQL traffic only from allowed_cidrs

WAL segments archive locally on the data volume by default. To ship them to object storage, point the archive command at a bucket using wal_archive_bucket and your S3 credentials.

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
db_flavorDatabase instance sizem2a.xlarge
volume_sizeData volume in GB50
pg_versionPostgreSQL major version16
backup_scheduleCron expression for backups0 2 * * *
allowed_cidrsCIDRs allowed to connectNo default
wal_archive_bucketObject storage bucket for WALnull
db_nameApplication database created on first bootappdb
db_userApplication database user created on first bootappuser
db_passwordPassword for the database user (required, no default)none
image_nameOperating system imageUbuntu-24.04
external_networkExternal network for router gatewayPublicStatic
private_cidrAddress range for the private subnet192.168.40.0/24
instance_nameCompute instance display namepostgres

When to use this pattern#

Run PostgreSQL on a single compute instance with a data volume and private network access. Choose MySQL/MariaDB Database for the sibling relational-database option when the workload speaks the MySQL wire protocol, WordPress + MySQL on Compute for a CMS plus MySQL pair, or 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.

Starting template$132.60/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

PostgreSQL database

m2a.xlarge · 4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.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

m2a.xlarge

4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00

Compute + RAM rate basis

4 vCPU + 16 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

$5.60

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.

$33.60/mo

Production on dedicated CPU

The headline estimate above; predictable steady-load performance.

$132.60/mo

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

Shared flavors carry less RAM (m2a.xlarge (16 GiB RAM) -> s1a.medium (4 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#

7 files. Download the zip or expand to copy any file.Download self-managed-postgres.zip
Show source (7 files)
main.tfHCL
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" "postgres" {
  name        = "${var.instance_name}-sg"
  description = "SSH from any IPv4; PostgreSQL from configured CIDRs only"
}

resource "openstack_networking_secgroup_rule_v2" "postgres" {
  for_each = toset(var.allowed_cidrs)

  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 5432
  port_range_max    = 5432
  remote_ip_prefix  = each.value
  security_group_id = openstack_networking_secgroup_v2.postgres.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.postgres.id
}

resource "openstack_blockstorage_volume_v3" "pg_data" {
  name = "${var.instance_name}-pg-data"
  size = var.volume_size
}

resource "openstack_networking_port_v2" "postgres" {
  name               = "${var.instance_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.postgres.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "postgres" {
  name        = var.instance_name
  flavor_name = var.db_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/postgres.yaml", {
    pg_version      = var.pg_version
    backup_schedule = var.backup_schedule
    private_cidr    = var.private_cidr
    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.postgres.id
  }
}

resource "openstack_compute_volume_attach_v2" "pg_data" {
  instance_id = openstack_compute_instance_v2.postgres.id
  volume_id   = openstack_blockstorage_volume_v3.pg_data.id
}
variables.tfHCL
variable "allowed_cidrs" {
  description = "IPv4 CIDRs allowed to reach PostgreSQL on port 5432"
  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 PostgreSQL instance"
  type        = string
  default     = "m2a.xlarge"
}

variable "volume_size" {
  description = "Cinder volume size in GiB for PostgreSQL data"
  type        = number
  default     = 50
}

variable "pg_version" {
  description = "PostgreSQL major version (for cloud-init or operator reference)"
  type        = string
  default     = "16"
}

variable "backup_schedule" {
  description = "Cron schedule for backups (for cloud-init or operator reference)"
  type        = string
  default     = "0 2 * * *"
}

variable "wal_archive_bucket" {
  description = "Optional object storage bucket for WAL archive (for cloud-init or operator reference)"
  type        = string
  default     = null
}

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.40.0/24"
}

variable "instance_name" {
  description = "Compute instance display name"
  type        = string
  default     = "postgres"
}

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
}
outputs.tfHCL
output "instance_id" {
  description = "Nova instance ID"
  value       = openstack_compute_instance_v2.postgres.id
}

output "private_ip" {
  description = "Private IPv4 address on the dedicated subnet"
  value       = openstack_compute_instance_v2.postgres.network[0].fixed_ip_v4
}

output "data_volume_id" {
  description = "Cinder volume ID for PostgreSQL data"
  value       = openstack_blockstorage_volume_v3.pg_data.id
}
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
allowed_cidrs = ["YOUR_ALLOWED_CIDR"]
key_name      = "YOUR_KEY_NAME"

# db_flavor = "m2a.xlarge"
# volume_size = 50
# pg_version = "16"
# backup_schedule = "0 2 * * *"
# wal_archive_bucket = null
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# private_cidr = "192.168.40.0/24"
# instance_name = "postgres"
cloud-init/postgres.yamlYAML
#cloud-config
package_update: true
write_files:
  - path: /tmp/zz-template.conf
    content: |
      listen_addresses = '*'
      archive_mode = on
      archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
    owner: root:root
    permissions: "0644"
  - path: /usr/local/bin/pg-backup.sh
    permissions: "0755"
    content: |
      #!/bin/bash
      set -e
      install -d -o postgres -g postgres /var/lib/postgresql/backups
      ts=$(date +%Y%m%d-%H%M%S)
      sudo -u postgres pg_dumpall | gzip > /var/lib/postgresql/backups/pgdumpall-$ts.sql.gz
  - path: /etc/cron.d/pg-backup
    owner: root:root
    permissions: "0644"
    content: |
      ${backup_schedule} root /usr/local/bin/pg-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 pgdata "$DEV"; fi
    mkdir -p /var/lib/postgresql
    mount "$DEV" /var/lib/postgresql
    grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/postgresql 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 postgresql-${pg_version}
    install -d -o postgres -g postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups
    cp /tmp/zz-template.conf /etc/postgresql/${pg_version}/main/conf.d/zz-template.conf
    echo "host all all ${private_cidr} scram-sha-256" >> /etc/postgresql/${pg_version}/main/pg_hba.conf
    systemctl enable postgresql
    systemctl restart postgresql
    sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE DATABASE ${db_name};"
    sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
    sudo -u postgres psql -v ON_ERROR_STOP=0 -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};"
README.mdMarkdown
# Self-Managed PostgreSQL

Self-managed PostgreSQL on a dedicated instance with data volume, security rules for client CIDRs, and automation hooks for backups. Suited to workloads you may later migrate to Rumble Managed Database.


**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 PostgreSQL on port 5432

## 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 PostgreSQL on port 5432 |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `db_flavor` | string | no | `m2a.xlarge` | Flavor for the PostgreSQL instance |
| `volume_size` | number | no | `50` | Cinder volume size in GiB for PostgreSQL data |
| `pg_version` | string | no | `16` | PostgreSQL major version (reference for cloud-init or operators) |
| `backup_schedule` | string | no | `0 2 * * *` | Cron schedule for backups (reference) |
| `wal_archive_bucket` | string | no | `null` | Optional object storage bucket for WAL archive |
| `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.40.0/24` | Private subnet CIDR for the database |
| `instance_name` | string | no | `postgres` | Compute instance display name |

## Documentation

Full documentation: [Self-managed PostgreSQL template](/docs/automation/templates/self-managed-postgres)

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

  • Vector searchself-managed-postgres-pgvector

    pgvector extension for embedding storage and similarity search

    Show source and download
    7 files. Download the zip or copy any file.Download self-managed-postgres-pgvector.zip
    main.tfHCL
    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" "postgres" {
      name        = "${var.instance_name}-sg"
      description = "SSH from any IPv4; PostgreSQL from configured CIDRs only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "postgres" {
      for_each = toset(var.allowed_cidrs)
    
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 5432
      port_range_max    = 5432
      remote_ip_prefix  = each.value
      security_group_id = openstack_networking_secgroup_v2.postgres.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.postgres.id
    }
    
    resource "openstack_blockstorage_volume_v3" "pg_data" {
      name = "${var.instance_name}-pg-data"
      size = var.volume_size
    }
    
    resource "openstack_networking_port_v2" "postgres" {
      name               = "${var.instance_name}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.postgres.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "postgres" {
      name        = var.instance_name
      flavor_name = var.db_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/postgres.yaml", {
        pg_version      = var.pg_version
        backup_schedule = var.backup_schedule
        private_cidr    = var.private_cidr
        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.postgres.id
      }
    }
    
    resource "openstack_compute_volume_attach_v2" "pg_data" {
      instance_id = openstack_compute_instance_v2.postgres.id
      volume_id   = openstack_blockstorage_volume_v3.pg_data.id
    }
    
    variables.tfHCL
    variable "allowed_cidrs" {
      description = "IPv4 CIDRs allowed to reach PostgreSQL on port 5432"
      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 PostgreSQL instance"
      type        = string
      default     = "m2a.xlarge"
    }
    
    variable "volume_size" {
      description = "Cinder volume size in GiB for PostgreSQL data"
      type        = number
      default     = 50
    }
    
    variable "pg_version" {
      description = "PostgreSQL major version (for cloud-init or operator reference)"
      type        = string
      default     = "16"
    }
    
    variable "backup_schedule" {
      description = "Cron schedule for backups (for cloud-init or operator reference)"
      type        = string
      default     = "0 2 * * *"
    }
    
    variable "wal_archive_bucket" {
      description = "Optional object storage bucket for WAL archive (for cloud-init or operator reference)"
      type        = string
      default     = null
    }
    
    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.40.0/24"
    }
    
    variable "instance_name" {
      description = "Compute instance display name"
      type        = string
      default     = "postgres-pgvector"
    }
    
    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
    }
    
    outputs.tfHCL
    output "instance_id" {
      description = "Nova instance ID"
      value       = openstack_compute_instance_v2.postgres.id
    }
    
    output "private_ip" {
      description = "Private IPv4 address on the dedicated subnet"
      value       = openstack_compute_instance_v2.postgres.network[0].fixed_ip_v4
    }
    
    output "data_volume_id" {
      description = "Cinder volume ID for PostgreSQL data"
      value       = openstack_blockstorage_volume_v3.pg_data.id
    }
    
    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
    allowed_cidrs = ["YOUR_ALLOWED_CIDR"]
    key_name      = "YOUR_KEY_NAME"
    
    # db_flavor = "m2a.xlarge"
    # volume_size = 50
    # pg_version = "16"
    # backup_schedule = "0 2 * * *"
    # wal_archive_bucket = null
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.40.0/24"
    # instance_name = "postgres"
    
    cloud-init/postgres.yamlYAML
    #cloud-config
    package_update: true
    write_files:
      - path: /tmp/zz-template.conf
        content: |
          listen_addresses = '*'
          archive_mode = on
          archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
        owner: root:root
        permissions: "0644"
      - path: /usr/local/bin/pg-backup.sh
        permissions: "0755"
        content: |
          #!/bin/bash
          set -e
          install -d -o postgres -g postgres /var/lib/postgresql/backups
          ts=$(date +%Y%m%d-%H%M%S)
          sudo -u postgres pg_dumpall | gzip > /var/lib/postgresql/backups/pgdumpall-$ts.sql.gz
      - path: /etc/cron.d/pg-backup
        owner: root:root
        permissions: "0644"
        content: |
          ${backup_schedule} root /usr/local/bin/pg-backup.sh
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        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 pgdata "$DEV"; fi
        mkdir -p /var/lib/postgresql
        mount "$DEV" /var/lib/postgresql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/postgresql 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 postgresql-${pg_version} postgresql-${pg_version}-pgvector
        install -d -o postgres -g postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups
        cp /tmp/zz-template.conf /etc/postgresql/${pg_version}/main/conf.d/zz-template.conf
        echo "host all all ${private_cidr} scram-sha-256" >> /etc/postgresql/${pg_version}/main/pg_hba.conf
        systemctl enable postgresql
        systemctl restart postgresql
        for i in $(seq 1 30); do
          sudo -u postgres psql -c "SELECT 1" >/dev/null 2>&1 && break
          sleep 2
        done
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE DATABASE ${db_name};"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};"
        for i in $(seq 1 10); do
          if sudo -u postgres psql -d ${db_name} -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS vector;"; then
            break
          fi
          sleep 5
        done
        sudo -u postgres psql -d ${db_name} -tAc "SELECT extname FROM pg_extension WHERE extname='vector';"
    
    README.mdMarkdown
    # Self-managed PostgreSQL with pgvector
    
    Self-managed PostgreSQL on a dedicated instance with data volume, the `pgvector` extension enabled for embedding storage and similarity search, and automation hooks for backups. Extends the [self-managed PostgreSQL](/docs/automation/templates/self-managed-postgres) base with vector-index capability for RAG and retrieval workloads.
    
    
    **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 PostgreSQL on port 5432
    
    ## 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. Confirm the extension: `psql -h <private_ip> -U <db_user> -d <db_name> -c '\\dx vector'`
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `allowed_cidrs` | list(string) | yes | n/a | IPv4 CIDRs allowed to reach PostgreSQL on port 5432 |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `db_flavor` | string | no | `m2a.xlarge` | Flavor for the PostgreSQL instance |
    | `volume_size` | number | no | `50` | Cinder volume size in GiB for PostgreSQL data |
    | `pg_version` | string | no | `16` | PostgreSQL major version installed by cloud-init |
    | `backup_schedule` | string | no | `0 2 * * *` | Cron schedule for backups |
    | `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.40.0/24` | Private subnet CIDR for the database |
    | `instance_name` | string | no | `postgres-pgvector` | Compute instance display name |
    
    ## Documentation
    
    Base template: [Self-managed PostgreSQL template](/docs/automation/templates/self-managed-postgres)
    
    Full documentation: [Self-managed PostgreSQL pgvector variation](/docs/automation/templates/self-managed-postgres-pgvector)
    
  • Connection poolingself-managed-postgres-pooled+$17/mo over the base

    PgBouncer connection pooler in front of PostgreSQL

    Show source and download
    8 files. Download the zip or copy any file.Download self-managed-postgres-pooled.zip
    main.tfHCL
    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" "postgres" {
      name        = "${var.instance_name}-sg"
      description = "SSH from any IPv4; PostgreSQL from configured CIDRs only"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "postgres" {
      for_each = toset(var.allowed_cidrs)
    
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 5432
      port_range_max    = 5432
      remote_ip_prefix  = each.value
      security_group_id = openstack_networking_secgroup_v2.postgres.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.postgres.id
    }
    
    resource "openstack_blockstorage_volume_v3" "pg_data" {
      name = "${var.instance_name}-pg-data"
      size = var.volume_size
    }
    
    resource "openstack_networking_port_v2" "postgres" {
      name               = "${var.instance_name}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.postgres.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "postgres" {
      name        = var.instance_name
      flavor_name = var.db_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/postgres.yaml", {
        pg_version      = var.pg_version
        backup_schedule = var.backup_schedule
        private_cidr    = var.private_cidr
        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.postgres.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_volume_attach_v2" "pg_data" {
      instance_id = openstack_compute_instance_v2.postgres.id
      volume_id   = openstack_blockstorage_volume_v3.pg_data.id
    }
    
    resource "openstack_networking_secgroup_v2" "pgbouncer" {
      name        = "${var.instance_name}-pgbouncer-sg"
      description = "PgBouncer from allowed CIDRs; SSH from anywhere"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "pgbouncer_pool" {
      for_each = toset(var.allowed_cidrs)
    
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 6432
      port_range_max    = 6432
      remote_ip_prefix  = each.value
      security_group_id = openstack_networking_secgroup_v2.pgbouncer.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "pgbouncer_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.pgbouncer.id
    }
    
    resource "openstack_networking_port_v2" "pgbouncer" {
      name               = "${var.instance_name}-pgbouncer-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.pgbouncer.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "pgbouncer" {
      name        = "${var.instance_name}-pgbouncer"
      flavor_name = var.pooler_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/pgbouncer.yaml", {
        postgres_ip = openstack_compute_instance_v2.postgres.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.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 10
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.pgbouncer.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.private,
        openstack_compute_volume_attach_v2.pg_data,
      ]
    }
    
    variables.tfHCL
    variable "allowed_cidrs" {
      description = "IPv4 CIDRs allowed to reach PostgreSQL on port 5432"
      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 PostgreSQL instance"
      type        = string
      default     = "m2a.xlarge"
    }
    
    variable "volume_size" {
      description = "Cinder volume size in GiB for PostgreSQL data"
      type        = number
      default     = 50
    }
    
    variable "pg_version" {
      description = "PostgreSQL major version (for cloud-init or operator reference)"
      type        = string
      default     = "16"
    }
    
    variable "backup_schedule" {
      description = "Cron schedule for backups (for cloud-init or operator reference)"
      type        = string
      default     = "0 2 * * *"
    }
    
    variable "wal_archive_bucket" {
      description = "Optional object storage bucket for WAL archive (for cloud-init or operator reference)"
      type        = string
      default     = null
    }
    
    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.40.0/24"
    }
    
    variable "instance_name" {
      description = "Compute instance display name"
      type        = string
      default     = "postgres"
    }
    
    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
    }
    
    variable "pooler_flavor" {
      description = "Flavor for the PgBouncer connection pooler instance"
      type        = string
      default     = "s1a.small"
    }
    
    outputs.tfHCL
    output "instance_id" {
      description = "Nova instance ID"
      value       = openstack_compute_instance_v2.postgres.id
    }
    
    output "private_ip" {
      description = "Private IPv4 address on the dedicated subnet"
      value       = openstack_compute_instance_v2.postgres.network[0].fixed_ip_v4
    }
    
    output "data_volume_id" {
      description = "Cinder volume ID for PostgreSQL data"
      value       = openstack_blockstorage_volume_v3.pg_data.id
    }
    
    
    output "pgbouncer_private_ip" {
      description = "Fixed IPv4 of the PgBouncer pooler on the private network"
      value       = openstack_compute_instance_v2.pgbouncer.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
    allowed_cidrs = ["YOUR_ALLOWED_CIDR"]
    key_name      = "YOUR_KEY_NAME"
    
    # db_flavor = "m2a.xlarge"
    # volume_size = 50
    # pg_version = "16"
    # backup_schedule = "0 2 * * *"
    # wal_archive_bucket = null
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.40.0/24"
    # instance_name = "postgres"
    
    cloud-init/pgbouncer.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - pgbouncer
    write_files:
      - path: /etc/pgbouncer/pgbouncer.ini
        owner: root:root
        permissions: "0640"
        content: |
          [databases]
          ${db_name} = host=${postgres_ip} port=5432 dbname=${db_name}
    
          [pgbouncer]
          listen_addr = 0.0.0.0
          listen_port = 6432
          auth_type = scram-sha-256
          auth_file = /etc/pgbouncer/userlist.txt
          pool_mode = transaction
          max_client_conn = 200
          default_pool_size = 20
      - path: /etc/pgbouncer/userlist.txt
        owner: root:root
        permissions: "0640"
        content: |
          "${db_user}" "${db_password}"
    runcmd:
      - systemctl enable pgbouncer
      - systemctl restart pgbouncer
    
    cloud-init/postgres.yamlYAML
    #cloud-config
    package_update: true
    write_files:
      - path: /tmp/zz-template.conf
        content: |
          listen_addresses = '*'
          archive_mode = on
          archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'
        owner: root:root
        permissions: "0644"
      - path: /usr/local/bin/pg-backup.sh
        permissions: "0755"
        content: |
          #!/bin/bash
          set -e
          install -d -o postgres -g postgres /var/lib/postgresql/backups
          ts=$(date +%Y%m%d-%H%M%S)
          sudo -u postgres pg_dumpall | gzip > /var/lib/postgresql/backups/pgdumpall-$ts.sql.gz
      - path: /etc/cron.d/pg-backup
        owner: root:root
        permissions: "0644"
        content: |
          ${backup_schedule} root /usr/local/bin/pg-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 pgdata "$DEV"; fi
        mkdir -p /var/lib/postgresql
        mount "$DEV" /var/lib/postgresql
        grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/postgresql 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 postgresql-${pg_version}
        install -d -o postgres -g postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups
        cp /tmp/zz-template.conf /etc/postgresql/${pg_version}/main/conf.d/zz-template.conf
        echo "host all all ${private_cidr} scram-sha-256" >> /etc/postgresql/${pg_version}/main/pg_hba.conf
        systemctl enable postgresql
        systemctl restart postgresql
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE DATABASE ${db_name};"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
        sudo -u postgres psql -v ON_ERROR_STOP=0 -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};"
    
    README.mdMarkdown
    # Self-Managed PostgreSQL
    
    Self-managed PostgreSQL on a dedicated instance with data volume, security rules for client CIDRs, and automation hooks for backups. Suited to workloads you may later migrate to Rumble Managed Database.
    
    
    **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 PostgreSQL on port 5432
    
    ## 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 PostgreSQL on port 5432 |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `db_flavor` | string | no | `m2a.xlarge` | Flavor for the PostgreSQL instance |
    | `volume_size` | number | no | `50` | Cinder volume size in GiB for PostgreSQL data |
    | `pg_version` | string | no | `16` | PostgreSQL major version (reference for cloud-init or operators) |
    | `backup_schedule` | string | no | `0 2 * * *` | Cron schedule for backups (reference) |
    | `wal_archive_bucket` | string | no | `null` | Optional object storage bucket for WAL archive |
    | `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.40.0/24` | Private subnet CIDR for the database |
    | `instance_name` | string | no | `postgres` | Compute instance display name |
    
    ## Documentation
    
    Full documentation: [Self-managed PostgreSQL template](/docs/automation/templates/self-managed-postgres)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • allowed_cidrsrequired
  • key_namerequired
  • db_flavor="m2a.xlarge"
  • volume_size=50
  • pg_version="16"
  • backup_schedule="0 2 * * *"
  • wal_archive_bucket=null
  • image_name="Ubuntu-24.04"
  • external_network="PublicStatic"
  • private_cidr="192.168.40.0/24"
  • instance_name="postgres"
  • db_name="appdb"
  • db_user="appuser"
  • 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?