Skip to content
IaC Templates

Umami self-hosted analytics

Template

Umami self-hosted analytics

This pattern composes Compute, Network, and Block Storage into a self-hosted web and product analytics host you run on infrastructure you control.

What this template does#

Provisions a single instance running Umami, an open-source analytics tool (a self-hosted alternative to Plausible, Vercel Analytics, or the metered tiers of Mixpanel and Amplitude). Your sites load a small tracking script served from this host, and the page-view and event data stays on your infrastructure:

  • Compute instance that runs Umami in Docker, sized for the app plus a bundled PostgreSQL (4 vCPU and 4 GiB RAM)
  • Private network, subnet, router, port, and security group; a floating IP for public access
  • A block volume mounted at /var/lib/docker, so the analytics data lives on a volume you can grow rather than on the boot disk
  • cloud-init installs Docker Engine and starts Umami from a compose file on first boot

In bundled mode a PostgreSQL container runs alongside Umami. In external mode Umami points at a database you already run.

Umami creates a default admin account (username admin, password umami) on first start. Change the password the first time you sign in. The app secret and, in bundled mode, the database password are generated on first boot and written to /opt/umami/.env; no credential ships with this template.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (Umami plus bundled PostgreSQL runs on 4 vCPU / 4 GiB)s1a.medium
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcesumami
volume_sizeBlock volume size in GiB, mounted at /var/lib/docker20
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.44.0.0/24
dashboard_allowed_cidrCIDR allowed to reach the dashboard on port 300010.44.0.0/24
db_modeDatastore mode: bundled or externalbundled
postgres_hostPostgreSQL host (when db_mode is external)""
postgres_dbPostgreSQL database nameumami
postgres_userPostgreSQL userumami

Dashboard access and security#

The dashboard listens on port 3000 over plain HTTP. The security group restricts 3000 to dashboard_allowed_cidr, which defaults to the private network only, so the raw dashboard stays off the public internet. The tracking script your sites load is served from the same host, so put a reverse proxy in front before you send production traffic. Reach the host one of three ways:

  • Put a reverse proxy (Caddy or Nginx) in front of Umami and serve the dashboard and tracking script over HTTPS on 443. Point the domain's DNS A record at the floating IP. This is the recommended path.
  • Tunnel over SSH: ssh -L 3000:localhost:3000 user@FLOATING_IP, then open http://localhost:3000. Use this for setup; your sites cannot load the tracking script through a tunnel.
  • Set dashboard_allowed_cidr to YOUR_IP/32 to reach port 3000 directly from one address for setup.

Ports 80 and 443 stay open for the reverse proxy you put in front; they carry no traffic until you add one.

Datastore#

The db_mode parameter selects the backend:

  • bundled (default): a PostgreSQL container runs alongside Umami, with the database password generated on first boot and the data on the /var/lib/docker volume. This suits getting started and small-to-moderate traffic.
  • external: points Umami at an existing PostgreSQL database, such as a self-managed PostgreSQL instance. Set postgres_host, postgres_db, and postgres_user, then replace CHANGEME in DATABASE_URL in /opt/umami/.env on the instance and run docker compose up -d. The password stays out of tfvars and the repo.

When to use this pattern#

Collect page views and custom events for your sites and products on a host you operate, and keep the data on your own infrastructure. Umami covers web analytics and lightweight product events. For the fuller product-analytics feature set (funnels, session replay, experiments), PostHog self-hosts too, on a heavier stack (ClickHouse plus Redis) that this template does not cover. For error and performance telemetry, pair this with GlitchTip; for infrastructure metrics, use the monitoring stack.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

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

Umami analytics host

s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps

Runs Umami in Docker (the analytics dashboard and the tracking API your sites send events to). In bundled mode a PostgreSQL container runs alongside it; the database lives on an attached volume.

Umami plus a bundled PostgreSQL runs on 4 vCPU and 4 GiB RAM. In external-database mode the app alone fits a smaller flavor.

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

s1a.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

Compute + RAM rate basis

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

$4.00

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 analytics-umami.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.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" "umami" {
  name        = "${var.app_name}-sg"
  description = "SSH and HTTP/HTTPS for a reverse proxy; dashboard port 3000 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.umami.id
}

# 80 and 443 carry the dashboard and the tracking script when they are 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 Umami; 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.umami.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.umami.id
}

# Raw dashboard HTTP on 3000 is restricted to dashboard_allowed_cidr (the
# private network by default). Umami creates a default admin on first start that
# you change immediately, but the port stays off the public internet by default.
# Prefer a domain with TLS on 443 for routine access and for serving the
# tracking script to your sites.
resource "openstack_networking_secgroup_rule_v2" "dashboard" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 3000
  port_range_max    = 3000
  remote_ip_prefix  = var.dashboard_allowed_cidr
  security_group_id = openstack_networking_secgroup_v2.umami.id
}

resource "openstack_networking_port_v2" "umami" {
  name               = "${var.app_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.umami.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" "umami" {
  name        = var.app_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/umami.yaml.tftpl", {
    app_name      = var.app_name
    db_mode       = var.db_mode
    postgres_host = var.postgres_host
    postgres_db   = var.postgres_db
    postgres_user = var.postgres_user
  })

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

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

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

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

variable "flavor_name" {
  description = "Instance size. Umami plus a bundled PostgreSQL runs comfortably on 4 vCPU and 4 GiB RAM (s1a.medium). In external-database mode the app alone fits a smaller flavor."
  type        = string
  default     = "s1a.medium"
}

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     = "umami"
}

variable "volume_size" {
  description = "Block volume size in GiB, mounted at /var/lib/docker so the analytics data (the PostgreSQL database in bundled mode, plus the Docker images and named volumes) lives 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.44.0.0/24"
}

variable "dashboard_allowed_cidr" {
  description = "CIDR allowed to reach the Umami dashboard on port 3000. Defaults to the private network only, so the dashboard is not exposed to the public internet on its raw port. 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. The tracking script that your sites load is served from the same host, so put a reverse proxy in front before sending production traffic."
  type        = string
  default     = "10.44.0.0/24"
}

variable "db_mode" {
  description = "Datastore mode. 'bundled' (default) runs a PostgreSQL container alongside Umami on this instance, with a database password generated on first boot. 'external' points Umami at an existing PostgreSQL database (for example a self-managed-postgres instance); supply the connection in the postgres_* variables and add the password to /opt/umami/.env on the instance (never in tfvars)."
  type        = string
  default     = "bundled"

  validation {
    condition     = contains(["bundled", "external"], var.db_mode)
    error_message = "db_mode must be either \"bundled\" or \"external\"."
  }
}

variable "postgres_host" {
  description = "PostgreSQL host for db_mode = \"external\" (for example the private IP of a self-managed-postgres instance). Ignored when db_mode is bundled."
  type        = string
  default     = ""
}

variable "postgres_db" {
  description = "PostgreSQL database name. Used as the bundled database name and as the external database name."
  type        = string
  default     = "umami"
}

variable "postgres_user" {
  description = "PostgreSQL user. Used for both bundled and external mode. In external mode the password is never set here: add it to DATABASE_URL in /opt/umami/.env on the instance and restart."
  type        = string
  default     = "umami"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance running Umami"
  value       = openstack_compute_instance_v2.umami.id
}

output "floating_ip" {
  description = "Public floating IP address of the Umami host"
  value       = openstack_networking_floatingip_v2.umami.address
}

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

output "dashboard_url" {
  description = "Umami dashboard URL on port 3000. Reachable from dashboard_allowed_cidr (the private network by default; tunnel over SSH, or put a reverse proxy in front and use HTTPS on 443). The tracking script your sites load is served from the same host."
  value       = "http://${openstack_networking_floatingip_v2.umami.address}:3000"
}
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"

# Recommended: restrict the dashboard (port 3000) to your workstation IP.
# Leave unset to keep 3000 reachable only from the private network and tunnel
# over SSH, or put a reverse proxy in front and use HTTPS on 443. The tracking
# script your sites load is served from the same host, so add the reverse proxy
# before sending production traffic.
# dashboard_allowed_cidr = "203.0.113.10/32"

# Datastore: bundled (default, a PostgreSQL container on this instance) or
# external (an existing PostgreSQL database). For external, set the connection
# below and add the password to DATABASE_URL in /opt/umami/.env on the instance.
# db_mode       = "external"
# postgres_host = "10.50.0.12"
# postgres_db   = "umami"
# postgres_user = "umami"

# flavor_name = "s1a.medium"
# image_name = "Ubuntu-24.04"
# app_name = "umami"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.44.0.0/24"
cloud-init/umami.yaml.tftpl
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
write_files:
  - path: /opt/umami/docker-compose.yml
    permissions: "0644"
    content: |
      # Umami analytics for ${app_name}. The dashboard and tracking API listen
      # on port 3000. Umami creates a default admin account (username "admin",
      # password "umami") on first start; change the password immediately from
      # the dashboard. No credential ships with this template: the app secret
      # and (in bundled mode) the database password are generated on first boot
      # and written to /opt/umami/.env.
      services:
        umami:
          image: ghcr.io/umami-software/umami:postgresql-latest
          restart: unless-stopped
          ports:
            - "3000:3000"
          env_file:
            - /opt/umami/.env
%{ if db_mode == "bundled" ~}
          depends_on:
            - db
        db:
          image: postgres:16-alpine
          restart: unless-stopped
          env_file:
            - /opt/umami/.env
          volumes:
            - umami_db:/var/lib/postgresql/data
      volumes:
        umami_db:
%{ endif ~}
runcmd:
  - |
    set -e
    # The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
    # Mount it at /var/lib/docker before Docker is installed so the bundled
    # PostgreSQL data and the Docker named volumes 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 umamidata "$DEV"; fi
    mkdir -p /var/lib/docker
    mount "$DEV" /var/lib/docker
    grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/docker ext4 defaults,nofail 0 2" >> /etc/fstab
    # Install Docker Engine plus the compose plugin from Docker's convenience
    # script.
    curl -fsSL https://get.docker.com | sh
    # Generate the Umami app secret on first boot and write the runtime env.
    APP_SECRET=$(openssl rand -hex 32)
    umask 077
%{ if db_mode == "bundled" ~}
    # Bundled mode: generate the PostgreSQL password and point Umami at the
    # db container. The password never leaves this instance.
    PGPASS=$(openssl rand -hex 24)
    {
      echo "DATABASE_TYPE=postgresql"
      echo "DATABASE_URL=postgresql://${postgres_user}:$PGPASS@db:5432/${postgres_db}"
      echo "POSTGRES_USER=${postgres_user}"
      echo "POSTGRES_PASSWORD=$PGPASS"
      echo "POSTGRES_DB=${postgres_db}"
      echo "APP_SECRET=$APP_SECRET"
    } > /opt/umami/.env
    chmod 600 /opt/umami/.env
    cd /opt/umami
    docker compose up -d
%{ else ~}
    # External mode: write a partial env and stop. Add the database password to
    # DATABASE_URL, then start Umami: cd /opt/umami && docker compose up -d
    {
      echo "DATABASE_TYPE=postgresql"
      echo "# Replace CHANGEME with the external database password, then run:"
      echo "#   cd /opt/umami && docker compose up -d"
      echo "DATABASE_URL=postgresql://${postgres_user}:CHANGEME@${postgres_host}:5432/${postgres_db}"
      echo "APP_SECRET=$APP_SECRET"
    } > /opt/umami/.env
    chmod 600 /opt/umami/.env
%{ endif ~}
README.mdMarkdown
# Umami self-hosted analytics

Single compute instance running [Umami](https://umami.is), a self-hosted web and product analytics tool (a self-hosted alternative to Plausible, Vercel Analytics, or the metered tiers of Mixpanel and Amplitude) on infrastructure you control. After apply, you open the dashboard, change the default admin password, add a website, and drop the tracking script into your site to collect page views and events.


**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 `/var/lib/docker` so the analytics data lives on a resizable volume. cloud-init installs Docker Engine and brings Umami up from a compose file on first boot. In bundled mode a PostgreSQL container runs alongside Umami; in external mode Umami points at a database you already run.

## Where this fits

Umami collapses metered analytics SaaS onto a VM you own. Your sites load a small tracking script served from this host, and the page-view and event data stays on your infrastructure rather than on a third-party meter. It pairs with [GlitchTip](https://glitchtip.com) for error telemetry and the [monitoring stack](/resources/iac-templates/monitoring-stack) for infrastructure metrics.

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

Umami plus a bundled PostgreSQL runs on 4 vCPU and 4 GiB RAM. The default `s1a.medium` flavor leaves headroom for the app and the database. In external-database mode the app alone fits a smaller flavor such as `s1a.small`.

## 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 install Docker and start Umami on first boot. Then reach `dashboard_url` from the outputs.

## First login and security

Umami creates a default admin account (username `admin`, password `umami`) on first start. Change the password immediately from **Settings** > **Profile** the first time you sign in. No other credential ships with this template: the app secret and, in bundled mode, the database password are generated on first boot and written to `/opt/umami/.env`.

The dashboard listens on port 3000 over plain HTTP. The security group restricts 3000 to `dashboard_allowed_cidr`, which defaults to the private network only, so the raw dashboard stays off the public internet. The tracking script your sites load is served from the same host, so put a reverse proxy in front before you send production traffic. Choose one of:

- **Recommended:** put a reverse proxy (Caddy or Nginx) in front of Umami and serve the dashboard and tracking script over HTTPS on 443. Point the domain's DNS A record at `floating_ip`.
- **SSH tunnel:** `ssh -L 3000:localhost:3000 user@<floating_ip>`, then open `http://localhost:3000` (use this for setup only; sites cannot load the script through your tunnel).
- **Direct, scoped:** set `dashboard_allowed_cidr` to your workstation IP (`YOUR_IP/32`) to reach 3000 directly from one address for setup.

Ports 80 and 443 stay open for the reverse proxy you put in front; they carry no traffic until you add one.

## Datastore

`db_mode` selects the backend:

- `bundled` (default): a PostgreSQL container runs alongside Umami on this instance, with the database password generated on first boot and the data on the `/var/lib/docker` volume. Best for getting started and small-to-moderate traffic.
- `external`: points Umami at an existing PostgreSQL database, such as a [self-managed PostgreSQL](/resources/iac-templates/self-managed-postgres) instance. Set `postgres_host`, `postgres_db`, and `postgres_user`, then replace `CHANGEME` in `DATABASE_URL` in `/opt/umami/.env` on the instance and run `docker compose up -d`. The password stays out of tfvars and the repo.

## How the instance is provisioned

cloud-init:

1. Mounts the data volume at `/var/lib/docker` (formatting it on first boot) and adds an `/etc/fstab` entry so it persists across reboots.
2. Writes `/opt/umami/docker-compose.yml` (the bundled PostgreSQL service is present only in bundled mode).
3. Installs Docker Engine from `https://get.docker.com`, generates the app secret (and the bundled database password) into `/opt/umami/.env`, and in bundled mode runs `docker compose up -d` to start Umami on port 3000.

## 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.medium` | Instance size (Umami plus bundled PostgreSQL runs on 4 vCPU / 4 GiB) |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `umami` | Display name prefix for resources |
| `volume_size` | number | no | `20` | Block volume size in GiB, mounted at `/var/lib/docker` |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `10.44.0.0/24` | CIDR for the private subnet |
| `dashboard_allowed_cidr` | string | no | `10.44.0.0/24` | CIDR allowed to reach the dashboard on port 3000 |
| `db_mode` | string | no | `bundled` | Datastore mode: `bundled` or `external` |
| `postgres_host` | string | no | `""` | PostgreSQL host (db_mode = external) |
| `postgres_db` | string | no | `umami` | PostgreSQL database name |
| `postgres_user` | string | no | `umami` | PostgreSQL user |

## Outputs

| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `dashboard_url` | Umami dashboard URL on port 3000 |
| `instance_id` | Compute instance ID |

## Scope

This is a single-VM Umami host that you operate, not a managed analytics cloud. It is CPU-only and runs in one region. You operate the instance, Docker, Umami, the database, and the data volume yourself. For higher volume, move to external mode against a larger PostgreSQL and size the host up. For the fuller product-analytics feature set (funnels, session replay, experiments), [PostHog](https://posthog.com) self-hosts too, but it is a heavier stack (ClickHouse plus Redis) and is not templatized here.

## Documentation

See also: [self-managed PostgreSQL](/resources/iac-templates/self-managed-postgres), [monitoring stack](/resources/iac-templates/monitoring-stack), [Uptime Kuma](/resources/iac-templates/uptime-kuma)
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="s1a.medium"
  • image_name="Ubuntu-24.04"
  • app_name="umami"
  • volume_size=20
  • external_network="PublicStatic"
  • private_cidr="10.44.0.0/24"
  • dashboard_allowed_cidr="10.44.0.0/24"
  • db_mode="bundled" validation {
  • postgres_host=""
  • postgres_db="umami"
  • postgres_user="umami"

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.

Was this page helpful?