Skip to content
IaC Templates

Edge reverse proxy

Template · Updated Jul 2026
Validated Jul 2026

Edge reverse proxy

This pattern composes Compute, Network, and Block Storage into a self-hosted reverse proxy with automatic TLS on infrastructure you control.

What this template does#

Provisions a single instance running Caddy, an open-source reverse proxy, that terminates TLS on a floating IP and forwards traffic to a private backend:

  • Compute instance that runs Caddy in Docker, sized for TLS termination and routing (2 vCPU and 2 GiB RAM)
  • Private network, subnet, router, port, and security group; a floating IP on the proxy only
  • A block volume mounted at /data, so Caddy certificate state lives on a volume you can grow rather than on the boot disk
  • cloud-init installs Docker, writes a Caddyfile for your backend, and starts Caddy on first boot

The backend stays on the private subnet with no floating IP of its own. You set upstream_host and upstream_port to the private address of the backend, then lock the backend security group to accept traffic only from the proxy.

No credential ships with this template. Caddy obtains a Let's Encrypt certificate automatically when you set domain and point its DNS A record at the floating IP.

Honest scope#

This proxy runs in one region on a VM you operate. It is a regional reverse proxy, not a global edge network: Quake AI has no anycast, no global PoPs, and no first-party CDN. For geographic distribution and volumetric DDoS absorption at the network edge, front the origin with a third-party CDN.

Alternate engines#

This template leads with Caddy (automatic HTTPS, single binary, minimal config). Traefik 3 fits when the proxy fronts many containers with label-based discovery. Nginx Proxy Manager fits when you want a web UI instead of editing config files. Both are MIT-licensed; swap the container and config in cloud-init if you prefer one of them.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (Caddy runs on 2 vCPU / 2 GiB)s1a.small
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcesedge-reverse-proxy
caddy_versionCaddy container image tag2-alpine
domainPublic domain for automatic HTTPS; empty serves HTTP on the floating IP""
upstream_hostPrivate IP of the backend instance10.42.0.10
upstream_portTCP port the backend listens on8080
volume_sizeBlock volume size in GiB, mounted at /data10
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.42.0.0/24

Ports and access#

PortPurpose
22Host SSH for administration
80HTTP (ACME challenges when domain is set; the public entry when it is not)
443HTTPS when domain is set and Caddy has obtained a certificate

When to use this pattern#

Run your own reverse proxy and TLS front door on a VM you operate, so a private app backend reaches the internet through one controlled entry point instead of exposing the origin directly. This is the foundation template for the edge and gateway family; the WAF, API gateway, and tunnel templates build on the same shape.

For DNS and certificate join points, see point a domain to Quake AI and Let's Encrypt certificates. For the WAF concept this template omits, see front with a WAF.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

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

Proxy

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

$16.50/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

Compute + RAM rate basis

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

—

Block storage (30 GiB)

30 GiB at $0.08/GiB/mo

$2.40

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

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

Template source#

This is a validated OpenTofu template.

7 files. Download the zip or expand to copy any file.Download edge-reverse-proxy.zip
Show source (7 files)
main.tfHCL
locals {
  # With a domain, Caddy serves HTTPS with an automatic Let's Encrypt certificate.
  # Without one, it serves plain HTTP on port 80 at the floating IP.
  caddy_site = var.domain != "" ? var.domain : ":80"
}

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.app_name}-net"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "${var.app_name}-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}

resource "openstack_networking_router_v2" "main" {
  name                = "${var.app_name}-router"
  external_network_id = data.openstack_networking_network_v2.external.id
}

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

resource "openstack_networking_secgroup_v2" "proxy" {
  name        = "${var.app_name}-sg"
  description = "SSH and HTTP/HTTPS for the edge reverse proxy"
}

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.proxy.id
}

resource "openstack_networking_secgroup_rule_v2" "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.proxy.id
}

resource "openstack_networking_secgroup_rule_v2" "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.proxy.id
}

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

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_blockstorage_volume_v3" "data" {
  name = "${var.app_name}-data"
  size = var.volume_size
}

resource "openstack_compute_instance_v2" "proxy" {
  name        = var.app_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/proxy.yaml.tftpl", {
    caddy_site    = local.caddy_site
    caddy_version = var.caddy_version
    upstream_host = var.upstream_host
    upstream_port = var.upstream_port
  })

  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.proxy.id
  }
}

resource "openstack_compute_volume_attach_v2" "data" {
  instance_id = openstack_compute_instance_v2.proxy.id
  volume_id   = openstack_blockstorage_volume_v3.data.id
}

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

resource "openstack_networking_floatingip_associate_v2" "proxy" {
  floating_ip = openstack_networking_floatingip_v2.proxy.address
  port_id     = openstack_networking_port_v2.proxy.id
}
variables.tfHCL
variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "flavor_name" {
  description = "Instance size. Caddy is a lightweight reverse proxy: 2 vCPU and 2 GiB RAM handle steady TLS termination and routing for a typical app backend. Raise the flavor only for very high connection counts or large upload bodies."
  type        = string
  default     = "s1a.small"
}

variable "image_name" {
  description = "Operating system image. cloud-init targets a Debian-family distribution; Ubuntu 24.04 is the recommended base."
  type        = string
  default     = "Ubuntu-24.04"
}

variable "app_name" {
  description = "Display name prefix for compute and network resources"
  type        = string
  default     = "edge-reverse-proxy"
}

variable "caddy_version" {
  description = "Caddy container image tag. The default 2-alpine tracks the current Caddy 2 release on a minimal base; pin a specific tag (for example 2.9.1-alpine) for reproducible rebuilds."
  type        = string
  default     = "2-alpine"
}

variable "domain" {
  description = "Public domain for the proxy front door. When set, Caddy obtains a Let's Encrypt certificate and serves HTTPS automatically. Point the domain's DNS A record at the floating IP before traffic arrives. Leave empty to serve plain HTTP on port 80 at the floating IP."
  type        = string
  default     = ""
}

variable "upstream_host" {
  description = "Private IP address of the backend instance Caddy proxies to. The backend stays on the private subnet with no floating IP of its own; lock its security group to accept traffic only from this proxy's private IP."
  type        = string
  default     = "10.42.0.10"
}

variable "upstream_port" {
  description = "TCP port the backend listens on within the private network"
  type        = number
  default     = 8080
}

variable "volume_size" {
  description = "Block volume size in GiB for Caddy certificates and config state. The volume mounts at /data so TLS material and the Caddy data directory live on a resizable volume rather than the boot disk."
  type        = number
  default     = 10
}

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 tenant network the proxy lives in"
  type        = string
  default     = "10.42.0.0/24"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance running the reverse proxy"
  value       = openstack_compute_instance_v2.proxy.id
}

output "floating_ip" {
  description = "Public floating IP address of the reverse proxy"
  value       = openstack_networking_floatingip_v2.proxy.address
}

output "private_ip" {
  description = "Private IP address of the proxy on the tenant network"
  value       = openstack_compute_instance_v2.proxy.access_ip_v4
}

output "proxy_url" {
  description = "URL for the proxy front door. HTTPS on the domain when set, otherwise HTTP on the floating IP. Point DNS at the floating IP before relying on automatic TLS."
  value       = var.domain != "" ? "https://${var.domain}" : "http://${openstack_networking_floatingip_v2.proxy.address}"
}
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: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"

# Backend on the private subnet (no floating IP). Lock its security group to the
# proxy private IP after apply.
# upstream_host = "10.42.0.10"
# upstream_port = 8080

# Recommended: set a domain so Caddy obtains a Let's Encrypt certificate.
# Point its DNS A record at the floating IP from the outputs.
# domain = "app.example.com"

# caddy_version = "2-alpine"
# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "edge-reverse-proxy"
# volume_size = 10
# external_network = "PublicStatic"
# private_cidr = "10.42.0.0/24"
cloud-init/proxy.yaml.tftpl
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
write_files:
  - path: /opt/edge-proxy/Caddyfile
    permissions: "0644"
    content: |
      ${caddy_site} {
          reverse_proxy ${upstream_host}:${upstream_port}
      }
  - path: /opt/edge-proxy/docker-compose.yml
    permissions: "0644"
    content: |
      services:
        caddy:
          image: caddy:${caddy_version}
          restart: unless-stopped
          ports:
            - "80:80"
            - "443:443"
          volumes:
            - /opt/edge-proxy/Caddyfile:/etc/caddy/Caddyfile:ro
            - /data/caddy:/data
            - /data/caddy-config:/config
runcmd:
  - |
    set -e
    # The data volume attaches as /dev/sdb on this platform. Mount it at /data
    # before Docker starts so Caddy certificate state lives on the resizable
    # volume rather than the boot disk.
    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 proxydata "$DEV"; fi
    mkdir -p /data/caddy /data/caddy-config
    mount "$DEV" /data
    grep -q "$DEV" /etc/fstab || echo "$DEV /data ext4 defaults,nofail 0 2" >> /etc/fstab
    curl -fsSL https://get.docker.com | sh
    cd /opt/edge-proxy
    docker compose up -d
README.mdMarkdown
# Edge reverse proxy

Single compute instance running [Caddy](https://caddyserver.com), an open-source reverse proxy with automatic HTTPS, on infrastructure you control. The proxy terminates TLS on a floating IP and forwards traffic to a private backend that has no public address of its own.


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

The instance provisions a private network, a floating IP, and a block volume mounted at `/data` so Caddy certificate state lives on a resizable volume rather than the boot disk. cloud-init installs Docker, writes a Caddyfile that reverse-proxies to your backend, and starts Caddy on first boot.

## Where this fits

This template provides the reverse proxy and TLS foundation for the edge and gateway template family. It implements the self-managed front door shape from the [front with a WAF](/docs/network/how-to/front-with-waf) how-to without the WAF engine. Pair it with a [containerized app](/resources/iac-templates/containerized-app) or [three-tier app](/resources/iac-templates/three-tier-app) backend on the same private network.

This proxy runs in one region on a VM you operate. It is not a global edge network. For geographic distribution, [front the origin with a third-party CDN](/docs/network/how-to/front-with-cdn).

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)
- A backend instance on the private network (or plan to deploy one after the proxy)

## Resource baseline

Caddy is a single lightweight process. The default `s1a.small` flavor (2 shared vCPU, 2 GiB RAM) handles steady TLS termination for a typical app backend. The boot disk is 20 GiB; certificate and config state live on the separate data volume (`volume_size`, default 10 GiB).

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and set `key_name`, `upstream_host`, and `upstream_port` (and `domain` if you have one)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

cloud-init takes a few minutes on first boot to install Docker, mount the data volume, and start Caddy. The proxy then answers on `proxy_url` from the outputs.

## Domain and TLS

With `domain` set and its DNS A record pointed at `floating_ip`, Caddy obtains a Let's Encrypt certificate automatically and serves HTTPS on 443. With `domain` empty, Caddy serves plain HTTP on port 80 at the floating IP.

For the DNS steps, see [point a domain to Quake AI](/docs/network/how-to/point-domain-to-quake-ai) and [Let's Encrypt certificates](/docs/network/how-to/lets-encrypt-certificate).

## Backend security

Set `upstream_host` to the private IP of your backend. The backend should have no floating IP. After apply, lock the backend security group so it accepts traffic on `upstream_port` only from the proxy's `private_ip` output.

## Alternate engines

This template leads with Caddy for automatic HTTPS and minimal config. [Traefik 3](https://traefik.io) fits stacks that need dynamic service discovery across many containers. [Nginx Proxy Manager](https://nginxproxymanager.com) fits operators who want a web UI instead of editing config files. Both are MIT-licensed and run on the same single-VM shape; swap the container image and config in cloud-init if you prefer one of them.

## Outputs

| Output | Description |
| --- | --- |
| `floating_ip` | Public floating IP of the proxy |
| `private_ip` | Private IP of the proxy on the tenant network |
| `proxy_url` | HTTPS on the domain when set, otherwise HTTP on the floating IP |
| `instance_id` | Compute instance ID |

## Validation

This template passes `tofu validate` in CI. That check confirms the OpenTofu configuration is well-formed against the provider schema; it does not run `tofu apply` against a live account.
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="s1a.small"
  • image_name="Ubuntu-24.04"
  • app_name="edge-reverse-proxy"
  • caddy_version="2-alpine"
  • domain=""
  • upstream_host="10.42.0.10"
  • upstream_port=8080
  • volume_size=10
  • external_network="PublicStatic"
  • private_cidr="10.42.0.0/24"

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: 01.07.2026

Was this page helpful?