Streamlit data-app host
Streamlit data-app host
This pattern composes Compute, Network, and Block Storage into a self-hosted data-app host you run on infrastructure you control.
What this template does#
Provisions a single instance running Streamlit, a Python framework for data apps and interactive demos (a self-hosted alternative to Streamlit Cloud or Hugging Face Spaces). You write Python, Streamlit renders widgets and charts in the browser, and your code reads datasets from the attached volume:
- Compute instance that runs Streamlit in Docker, sized for a single CPU-bound app (2 vCPU and 2 GiB RAM)
- Private network, subnet, router, port, and security group; a floating IP for public access
- A block volume mounted at
/opt/streamlit, so your Python app code and data files live on a volume you can grow rather than on the boot disk - cloud-init installs Docker Engine and starts Streamlit from a compose file on first boot
Gradio fits the same host shape if your team prefers Gradio widgets: swap the container image and entrypoint while keeping the volume layout.
Streamlit hosts internal dashboards, model demos, and analyst-facing tools on a VM you own. Quake AI flavors are CPU-only; GPU training belongs on an external backend.
No credential ships with this template. Add authentication at the reverse proxy or in your app if the dashboard should not be public.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (single Streamlit app on 2 vCPU / 2 GiB) | s1a.small |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | streamlit |
volume_size | Block volume size in GiB, mounted at /opt/streamlit | 20 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.43.0.0/24 |
app_allowed_cidr | CIDR allowed to reach the app on port 8501 | 10.43.0.0/24 |
App access and security#
The app listens on port 8501 over plain HTTP. The security group restricts 8501 to app_allowed_cidr, which defaults to the private network only, so the raw port stays off the public internet. Reach the app one of three ways:
- Put a reverse proxy (Caddy or Nginx) in front of Streamlit and serve the app over HTTPS on 443. Point the domain's DNS A record at the floating IP. This is the recommended path for routine access.
- Tunnel over SSH:
ssh -L 8501:localhost:8501 user@FLOATING_IP, then openhttp://localhost:8501. - Set
app_allowed_cidrtoYOUR_IP/32to reach port 8501 directly from one address.
Ports 80 and 443 stay open for the reverse proxy you put in front; they carry no traffic until you add one.
Deploying your own app#
Replace the sample files under /opt/streamlit/app on the instance with your own Python modules and list dependencies in requirements.txt. Store datasets and artifacts under /opt/streamlit/data. Restart the container with docker compose up -d from /opt/streamlit.
When to use this pattern#
Host a Python data app or demo with charts, filters, and file uploads on a VM you operate. Streamlit suits analyst dashboards, lightweight model demos, and internal tools. For multi-user notebook servers, pair with the JupyterHub template in the data tooling catalog. For workflow automation around your data stack, see n8n workflow automation.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared 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
Streamlit data-app host
s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Runs a Streamlit app in Docker with Python code and data files on an attached volume.
A single Streamlit app runs on 2 vCPU and 2 GiB RAM. Size up for heavier pandas workloads or larger in-memory datasets.
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
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 (50 GiB)
50 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
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.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (7 files)Hide source
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" "streamlit" {
name = "${var.app_name}-sg"
description = "SSH and HTTP/HTTPS for a reverse proxy; Streamlit port 8501 restricted"
}
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.streamlit.id
}
# 80 and 443 carry the app when it is served over a domain with automatic TLS
# through a reverse proxy (Caddy or Nginx). They are not used until you put a
# proxy in front of Streamlit; see the reference page.
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.streamlit.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.streamlit.id
}
# Raw Streamlit HTTP on 8501 is restricted to app_allowed_cidr (the private
# network by default). Prefer a domain with TLS on 443 for routine access.
resource "openstack_networking_secgroup_rule_v2" "app" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 8501
port_range_max = 8501
remote_ip_prefix = var.app_allowed_cidr
security_group_id = openstack_networking_secgroup_v2.streamlit.id
}
resource "openstack_networking_port_v2" "streamlit" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.streamlit.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" "streamlit" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/streamlit.yaml.tftpl", {
app_name = var.app_name
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 30
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.streamlit.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.streamlit.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_v2" "streamlit" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "streamlit" {
floating_ip = openstack_networking_floatingip_v2.streamlit.address
port_id = openstack_networking_port_v2.streamlit.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. A single Streamlit app in Docker runs comfortably on 2 vCPU and 2 GiB RAM. Size up for heavier pandas workloads or larger in-memory datasets."
type = string
default = "s1a.small"
}
variable "image_name" {
description = "Operating system image. 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 = "streamlit"
}
variable "volume_size" {
description = "Block volume size in GiB, mounted at /opt/streamlit so your Python app code and data files live on a volume you can grow rather than on the boot disk."
type = number
default = 20
}
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 instance lives in"
type = string
default = "10.43.0.0/24"
}
variable "app_allowed_cidr" {
description = "CIDR allowed to reach the Streamlit app on port 8501. Defaults to the private network only, so the raw app port is not exposed to the public internet. Reach it over an SSH tunnel, or (recommended) serve it over a domain with HTTPS on 443 behind a reverse proxy. To allow direct access from your workstation, set this to YOUR_IP/32."
type = string
default = "10.43.0.0/24"
}
output "instance_id" {
description = "ID of the compute instance running Streamlit"
value = openstack_compute_instance_v2.streamlit.id
}
output "floating_ip" {
description = "Public floating IP address of the Streamlit host"
value = openstack_networking_floatingip_v2.streamlit.address
}
output "private_ip" {
description = "Private IP address of the instance"
value = openstack_compute_instance_v2.streamlit.access_ip_v4
}
output "app_url" {
description = "Streamlit app URL on port 8501. Reachable from app_allowed_cidr (the private network by default; tunnel over SSH, or put a reverse proxy in front and use HTTPS on 443)."
value = "http://${openstack_networking_floatingip_v2.streamlit.address}:8501"
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Recommended: restrict the Streamlit app (port 8501) to your workstation IP.
# Leave unset to keep 8501 reachable only from the private network and tunnel
# over SSH, or put a reverse proxy in front and use HTTPS on 443.
# app_allowed_cidr = "203.0.113.10/32"
# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "streamlit"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.43.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
write_files:
- path: /tmp/streamlit-bootstrap/app/main.py
permissions: "0644"
content: |
import streamlit as st
st.set_page_config(page_title="${app_name} demo", layout="wide")
st.title("Streamlit data app")
st.write(
"Replace the files under /opt/streamlit/app with your own Python app. "
"Store datasets and artifacts under /opt/streamlit/data."
)
value = st.slider("Pick a number", 0, 100, 25)
st.write(f"The square is {value ** 2}.")
- path: /tmp/streamlit-bootstrap/app/requirements.txt
permissions: "0644"
content: |
streamlit>=1.32
- path: /tmp/streamlit-bootstrap/docker-compose.yml
permissions: "0644"
content: |
# Streamlit data app for ${app_name}. App code lives under /opt/streamlit/app
# and datasets under /opt/streamlit/data on the attached block volume.
# No credential ships with this template. Add auth at the reverse proxy or
# in your app if the dashboard should not be public.
services:
streamlit:
image: python:3.12-slim
restart: unless-stopped
working_dir: /app
ports:
- "8501:8501"
volumes:
- /opt/streamlit/app:/app
- /opt/streamlit/data:/data
command: >
bash -c "pip install --no-cache-dir -r requirements.txt &&
streamlit run main.py
--server.port=8501
--server.address=0.0.0.0
--browser.gatherUsageStats=false"
runcmd:
- |
set -e
# The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
# Mount it at /opt/streamlit before copying app files so code and data
# live 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 streamlitdata "$DEV"; fi
mkdir -p /opt/streamlit
mount "$DEV" /opt/streamlit
grep -q "$DEV" /etc/fstab || echo "$DEV /opt/streamlit ext4 defaults,nofail 0 2" >> /etc/fstab
mkdir -p /opt/streamlit/app /opt/streamlit/data
cp -a /tmp/streamlit-bootstrap/. /opt/streamlit/
rm -rf /tmp/streamlit-bootstrap
curl -fsSL https://get.docker.com | sh
cd /opt/streamlit
docker compose up -d
# Streamlit data-app host
Single compute instance running [Streamlit](https://streamlit.io), a Python framework for data apps and interactive demos, on infrastructure you control. After apply, you open the app in a browser, replace the sample Python code with your own, and serve datasets from the attached volume. [Gradio](https://gradio.app) is an alternative runtime for the same pattern: swap the container image and entrypoint if your team prefers Gradio widgets.
**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 `/opt/streamlit` so your app code and data files live on a resizable volume. cloud-init installs Docker Engine and starts Streamlit from a compose file on first boot.
## Where this fits
Streamlit hosts internal dashboards, model demos, and analyst-facing data apps on a VM you own rather than on a third-party app host. It suits CPU-bound Python workloads: pandas summaries, chart explorers, and lightweight inference demos. You run it yourself; this is a self-hosted tool you operate.
## 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)
## Resource baseline
A single Streamlit app runs on 2 vCPU and 2 GiB RAM. The default `s1a.small` flavor leaves headroom for Docker plus moderate pandas workloads. Size up for larger in-memory datasets. Quake AI flavors are CPU-only; GPU training belongs on an external backend.
## 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, cloud-init takes a few minutes to mount the data volume, install Docker, and start Streamlit on first boot. Then reach `app_url` from the outputs. No credential ships with this template.
## App access and security
The app listens on port 8501 over plain HTTP. The security group restricts 8501 to `app_allowed_cidr`, which defaults to the private network only, so the raw port is not exposed to the public internet. Choose one of:
- **Recommended:** put a reverse proxy (Caddy or Nginx) in front of Streamlit and serve the app over HTTPS on 443. Point the domain's DNS A record at `floating_ip`.
- **SSH tunnel:** `ssh -L 8501:localhost:8501 user@<floating_ip>`, then open `http://localhost:8501`.
- **Direct, scoped:** set `app_allowed_cidr` to your workstation IP (`YOUR_IP/32`) to reach 8501 directly from one address.
Ports 80 and 443 stay open for the reverse proxy you put in front; they carry no traffic until you add one. Streamlit does not ship authentication; add auth at the reverse proxy or in your app if the dashboard should not be public.
## Deploying your own app
Replace the files under `/opt/streamlit/app` on the instance (or copy them over SCP/rsync before restarting the container). List dependencies in `requirements.txt`. Store datasets and artifacts under `/opt/streamlit/data`. Restart with:
```bash
cd /opt/streamlit && sudo docker compose up -d
```
## How the instance is provisioned
cloud-init:
1. Mounts the data volume at `/opt/streamlit` (formatting it on first boot) and adds an `/etc/fstab` entry so it persists across reboots.
2. Copies a sample `main.py`, `requirements.txt`, and `docker-compose.yml` onto the volume.
3. Installs Docker Engine from `https://get.docker.com` and runs `docker compose up -d`, which starts Streamlit on port 8501.
## 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 (2 vCPU / 2 GiB for a single app) |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `streamlit` | Display name prefix for resources |
| `volume_size` | number | no | `20` | Block volume size in GiB, mounted at `/opt/streamlit` |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `10.43.0.0/24` | CIDR for the private subnet |
| `app_allowed_cidr` | string | no | `10.43.0.0/24` | CIDR allowed to reach the app on port 8501 |
## Outputs
| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `app_url` | Streamlit app URL on port 8501 |
| `instance_id` | Compute instance ID |
## Scope
This is a single-VM Streamlit host that you operate, not a managed app platform. It is CPU-only and runs in one region. For multi-user notebook servers, see the JupyterHub template. For workflow glue around your data stack, see [n8n workflow automation](/resources/iac-templates/n8n-workflow).
## Documentation
See also: [Object Storage for datasets](/docs/storage/object), [self-managed PostgreSQL](/resources/iac-templates/self-managed-postgres)
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.small"image_name="Ubuntu-24.04"app_name="streamlit"volume_size=20external_network="PublicStatic"private_cidr="10.43.0.0/24"app_allowed_cidr="10.43.0.0/24"
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.
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups