Skip to content
IaC Templates

Containerized App on Compute

Template · Updated Jun 2026
Validated Jun 2026

Containerized app on compute

This pattern composes Compute and Network.

Golden-path OpenTofu template for quake.yaml manifests with runtime: container. Provisions a CPU instance with Docker, pulls a container image, and publishes it on a floating IP.

What this template does#

Provisions a single compute instance for container workloads:

  • Private network, subnet, router, and neutron port
  • Security group allowing SSH and the published container port
  • Volume-backed boot disk
  • Floating IP for public access
  • Cloud-init installs Docker, pulls container_image, and runs the container with host port mapping and any container_env variables

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist in your project)No default
flavor_nameInstance sizes1a.small
image_nameOperating system imageUbuntu-24.04
app_nameResource name prefixcontainerized-app
container_imageContainer image referencenginx:alpine
container_envEnvironment variables injected into the container{}
container_portPort inside the container80
host_portPort published on the instance80
external_networkExternal network for floating IPPublicEphemeral
private_cidrPrivate subnet CIDR10.10.10.0/24

When to use this pattern#

Deploy a single VM prepped for Docker workloads on a private network with a floating IP. Add an edge reverse proxy or API gateway when you need a dedicated entry point in front of one or more container hosts.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

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

App tier

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 (20 GiB)

20 GiB at $0.08/GiB/mo

$1.60

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#

7 files. Download the zip or expand to copy any file.Download containerized-app.zip
Show source (7 files)
main.tfHCL
locals {
  # Render container_env into `-e KEY=VALUE` docker flags. Empty map is a no-op.
  container_env_flags = join(
    " ",
    [for k, v in var.container_env : "-e ${k}=${v}"],
  )
}

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" "app" {
  name        = "${var.app_name}-sg"
  description = "Allow SSH and container HTTP traffic"
}

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

resource "openstack_networking_secgroup_rule_v2" "app_http" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = var.host_port
  port_range_max    = var.host_port
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.app.id
}

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

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

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

  user_data = templatefile("${path.module}/cloud-init/app.yaml", {
    container_image = var.container_image
    container_port  = var.container_port
    host_port       = var.host_port
    env_flags       = local.container_env_flags
  })

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

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

resource "openstack_networking_floatingip_associate_v2" "app" {
  floating_ip = openstack_networking_floatingip_v2.app.address
  port_id     = openstack_networking_port_v2.app.id
}
variables.tfHCL
variable "flavor_name" {
  description = "Instance size (maps from quake.yaml environments.*.resources flavor alias)"
  type        = string
  default     = "s1a.small"
}

variable "image_name" {
  description = "Operating system image"
  type        = string
  default     = "Ubuntu-24.04"
}

variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "app_name" {
  description = "Display name prefix for compute and network resources"
  type        = string
  default     = "containerized-app"
}

variable "container_image" {
  description = "Container image to pull and run on first boot"
  type        = string
  default     = "nginx:alpine"
}

variable "container_env" {
  description = "Environment variables injected into the container at boot (maps from quake.yaml secrets and resolved service outputs)"
  type        = map(string)
  default     = {}
}

variable "container_port" {
  description = "Port the container listens on inside the container network namespace"
  type        = number
  default     = 80
}

variable "host_port" {
  description = "Host port published on the instance floating IP"
  type        = number
  default     = 80
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicEphemeral (quickstart-aligned). Set PublicStatic in tfvars for a persisted floating IP."
  type        = string
  default     = "PublicEphemeral"
}

variable "private_cidr" {
  description = "CIDR for the private tenant network the instance lives in"
  type        = string
  default     = "10.10.10.0/24"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance running the container"
  value       = openstack_compute_instance_v2.app.id
}

output "floating_ip" {
  description = "Public floating IP address for the containerized app"
  value       = openstack_networking_floatingip_v2.app.address
}

output "private_ip" {
  description = "Private IP address of the instance"
  value       = openstack_compute_instance_v2.app.access_ip_v4
}

output "container_url" {
  description = "HTTP URL for the published container port"
  value       = "http://${openstack_networking_floatingip_v2.app.address}:${var.host_port}"
}
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"

# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "containerized-app"
# container_image = "nginx:alpine"
# container_port = 80
# host_port = 80
# container_env = { DATABASE_URL = "postgres://...", ASSETS_BUCKET_NAME = "assets" }
# private_cidr = "10.10.10.0/24"
cloud-init/app.yamlYAML
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
runcmd:
  - curl -fsSL https://get.docker.com | sh
  - systemctl enable --now docker
  - docker pull ${container_image}
  - docker run -d --name app --restart unless-stopped ${env_flags} -p ${host_port}:${container_port} ${container_image}
README.mdMarkdown
# Containerized App

Single compute instance with Docker, a published container port, and a floating IP. Golden-path template for `quake.yaml` manifests with `runtime: container`.


**Network class:** throwaway / quickstart-aligned — `external_network` defaults to `PublicEphemeral`; use `PublicStatic` in tfvars when you need a persisted floating IP.

## quake.yaml mapping

When a launch manifest declares `runtime: container`, the handoff POC resolves it to this template slug (`containerized-app`) under `iac/templates/`. The manifest declares intent; this template provisions the CPU VM lane.

| `quake.yaml` field | Maps to |
| --- | --- |
| `runtime: container` | Template slug `containerized-app` (see `RUNTIME_TO_TEMPLATE_SLUG` in `src/lib/launch/quake-manifest.schema.ts`) |
| `source.build: dockerfile` | Build the image in CI, push to a registry, then set `container_image` to that reference before apply |
| `source.build: buildpack` | Enum validated only in the POC; supply a built image reference via `container_image` |
| `environments.production.resources: cpu-standard` | `flavor_name = "m2a.large"` |
| `environments.preview.resources: cpu-small` | `flavor_name = "s1a.small"` |
| `environments.*.branch` | Branch-to-environment routing is control-plane logic; typical mapping below |

### Branch-to-environment mapping

| Environment | Typical branch | Flavor alias | Default flavor |
| --- | --- | --- | --- |
| `production` | `main` | `cpu-standard` | `m2a.large` |
| `preview` | any non-`main` branch (for example PR branches) | `cpu-small` | `s1a.small` |

Preview environments may also carry `ttl_hours` in the manifest; teardown is advisory in the POC and enforced by the control plane when wired.

Optional `services[]` entries (for example `type: object-storage`) resolve to separate templates such as `s3-storage-acl` and compose alongside this runtime template.

## 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 container image reachable from the instance on first boot (public registry or project-accessible registry)

## 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`

After apply, open `container_url` from the outputs (HTTP on the floating IP and published host port).

## Variables

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
| `flavor_name` | string | no | `s1a.small` | Instance size |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `containerized-app` | Display name prefix for resources |
| `container_image` | string | no | `nginx:alpine` | Container image to pull and run |
| `container_env` | map(string) | no | `{}` | Environment variables injected into the container at boot |
| `container_port` | number | no | `80` | Port inside the container |
| `host_port` | number | no | `80` | Port published on the instance |
| `external_network` | string | no | `PublicEphemeral` | Defaults to ephemeral quickstart path; set `PublicStatic` for persisted FIP |
| `private_cidr` | string | no | `10.10.10.0/24` | CIDR for the private subnet |

## Outputs

| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `container_url` | `http://<floating_ip>:<host_port>` for the published container |
| `instance_id` | Compute instance ID |

## Environment variables and composition

`container_env` injects values into the container at boot. The launch handoff plan
populates it from two sources: `quake.yaml` secret names (you supply the values) and the
outputs of service templates applied earlier in the plan (for example an
`s3-storage-acl` bucket name). Apply service templates first, read their outputs, then set
`container_env` before applying this template.

For the POC, `container_env` values are passed as `docker run -e` flags and are visible in
the instance process list and cloud-init logs. For production, source secrets from a secret
store rather than the manifest.

## Documentation

Full documentation: [Containerized app template](/docs/automation/templates/containerized-app)

See also: [Deploy a containerized web application](/resources/tutorials/deploy-containerized-app)
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • flavor_name="s1a.small"
  • image_name="Ubuntu-24.04"
  • key_namerequired
  • app_name="containerized-app"
  • container_image="nginx:alpine"
  • container_env={}
  • container_port=80
  • host_port=80
  • external_network="PublicEphemeral"
  • private_cidr="10.10.10.0/24"

quake.yaml mapping#

Manifest fieldTemplate parameter
runtime: containerResolves to slug containerized-app
environments.production.resources: cpu-standardflavor_name = "m2a.large"
environments.preview.resources: cpu-smallflavor_name = "s1a.small"
source.build: dockerfileBuild in CI, push to a registry, set container_image before apply
secrets and resolved services outputscontainer_env map injected into the running container

Branch-to-environment routing (production on main, preview on other branches) is described in the template's README, included in the template source on this page.

The launch handoff plan populates container_env from quake.yaml secret names (you supply the values) and from the outputs of service templates applied earlier in the plan. For the POC, container_env values pass as docker run -e flags, which are visible in the instance process list. Source production secrets from a secret store.

Outputs#

OutputDescription
floating_ipPublic floating IP assigned to the instance
private_ipPrivate IP address of the instance
container_urlHTTP URL for the published container (floating IP and host port)
instance_idCompute instance ID

Read container_url after apply to verify the deployment and record it in the launch evidence bundle.

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

Was this page helpful?