Deploy Web Analytics with Terraform & Azure Verified Modules

Deploy Web Analytics with Terraform & Azure Verified Modules

Cookieless, privacy-respecting analytics, self-hosted on Azure.

Deploy Web Analytics with Terraform & Azure Verified Modules

You’ve built a personal blog, portfolio site, or hobby project. You want to know who’s visiting, which posts are popular, and where traffic comes from.

Google Analytics is the obvious default. But if your site has EU or UK visitors, you’ll need cookie consent banners. What if you could skip that entirely? How about simple analytics that shows what matters, not 290+ overwhelming metrics?

Enter Vince Analytics

Vince Analytics is a Go fork of Plausible Analytics, designed exactly for this scenario:

  • A single go binary: Super portable, easy to deploy, with clear docker deployment instructions.
  • Beautiful interface: Clean, simple dashboard that shows what matters
  • No cookie banners: Privacy-focused by design, no consent popups needed
  • Self-hosted: Run it on your Azure infrastructure for cents per month
  • Minimal dependencies: The ‘pebble’ data store only requires Azure Files storage.
  • Privacy-first: Visitors aren’t tracked across the web or profiled

Perfect for personal blogs, portfolio sites, and privacy-concious small businesses where you want insights without complexity.

vince analytics screenshot

Building with Azure Verified Modules

As one of the maintainers of the Container Apps Environment AVM module (and a contributor to others), I’ll show how to deploy Vince on Azure Container Apps (ACA) using Azure Verified Modules (AVM) and Terraform.

This solution uses:

  • Container Apps Environment
  • Container App
  • Storage Account
  • Key Vault

Why IaC for Container Apps?

You can deploy Container Apps with az containerapp and YAML. That works, but AVM + Terraform adds real advantages:

  • One source of truth across environment, app, storage, and secrets
  • Repeatable, reviewable, drift‑resistant deployments
  • Easier cross‑resource wiring (e.g., storage -> environment -> app -> keyvault)
  • Defining the container spec in a locals block lets us express everything in one place and feed it to the modules.

Here’s the Vince definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  locals {
    container_vince_app = {
      name         = "vince"
      azure_name   = "ca-vince-app-${var.env_code}"
      image        = "ghcr.io/vinceanalytics/vince:v1.11.8"
      cpu          = 0.5
      memory       = "1Gi"
      max_replicas = 1
      min_replicas = 0
      env_vars = {
        VINCE_LISTEN     = ":8080"
        VINCE_DATA       = "/data"
        VINCE_ADMIN_NAME = "[email protected]"
        VINCE_URL        = "https://ca-vince-app-${var.env_code}.${module.container_app_environment.default_domain}"
      }
      secrets = {
        VINCE_ADMIN_PASSWORD = "vince-admin-password"
      }
      volume_mounts = [
        {
          name = "vince-data"
          path = "/data"
        }
      ]
      volumes = [
        {
          name         = "vince-data"
          storage_type = "AzureFile"
          storage_name = "vince-data"
        }
      ]
      args = ["serve"]
      ingress = {
        target_port      = 8080
        external_enabled = true
        transport        = "http"
        traffic_weight = [
          {
            percentage      = 100
            latest_revision = true
          }
        ]
      }
    }
  }

Scaling to zero (and handling cold starts)

Container Apps is most cost‑effective when your app can scale to zero. When you do, you need to consider cold starts: the time from the first incoming request to when the app is ready to serve traffic.

The good news: Vince is a single static binary, so app initialization is essentially instant. You’re mostly waiting on the platform to spin up the container.

Two practical considerations:

  • You don’t want to lose page views if the tracker script times out. Vince’s embed uses the defer attribute, so it won’t block page load.
  • Measure your cold‑start behavior under real conditions. If it’s too slow for your needs, set min_replicas = 1 to keep one instance warm.

Add Vince to your sites

After you log in with the admin password stored in Key Vault, Vince provides a snippet like this:

1
<script defer data-api="/api/event" data-domain="yourdomain.com" src="https://your-vince-url/js/script.js"></script>

As Vince doesn’t use cookies or track users across sites, you can skip cookie consent banners. One Vince instance can serve multiple domains, making it very cost‑effective.

That’s it—clean, simple, privacy‑respecting analytics.

Local Development and Testing

Being containerised, you can easily run it locally with Docker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mkdir -p ./local-data

docker run --rm \
  -e VINCE_LISTEN=":8080" \
  -e VINCE_DATA="/data" \
  -e VINCE_ADMIN_NAME="[email protected]" \
  -e VINCE_ADMIN_PASSWORD="your-password" \
  -p 8080:8080 \
  -v "$(pwd)/local-data:/data" \
  ghcr.io/vinceanalytics/vince:v1.11.8 serve

Visit http://localhost:8080 to see the dashboard.

Deployment using GitHub Actions (or locally)

The GitHub repo (linked below) includes a workflow that deploys the stack with Terraform. You can also run it locally:

1
2
3
4
5
6
cd iac
terraform init

terraform plan -var-file="environments/dev.terraform.tfvars"

terraform apply -var-file="environments/dev.terraform.tfvars"

The combination of Vince’s simplicity and Azure’s AVM modules is a great fit for personal projects, hobby sites, and small businesses.


Ready to add clean analytics to your hobby site? The complete Terraform configuration using AVM modules is available on GitHub.


Thoughts on alternative approaches

Azure Functions might prove lighter and scale better — curious about other thoughts? Let me know in the comments.

If you enjoyed this post, follow me on LinkedIn — I feel a part 2 coming!

Additional Resources

This post is licensed under CC BY 4.0 by the author.