Skip to content

bzhr/infra

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Analytics Stack on an Existing DigitalOcean Droplet (Terraform)

Motivation

If you want to self-host multiple open source projects within one compute machine, this is your project. This is a Terraform setup that's using Traefik as a reverse proxy and it is currently installing: Plausible Analytics, ListMonk (mailing lists) and Grafana, Prometheus and Traefik for monitoring.

More services can be added in the future. I will also accept pull request in order to grow this into a framework.

This deploys on ONE droplet:

  • Traefik v3 (TLS, routing, dashboard, Prometheus metrics)
  • Plausible (Postgres + ClickHouse)
  • Listmonk (Postgres)
  • Prometheus + Grafana

DNS: add A-records manually.


Prereqs

  • Terraform ≥ 1.5
  • DigitalOcean API token (export as DIGITALOCEAN_TOKEN)
  • A reachable SSH key on the droplet (you’ll SSH as root by default)

Install:

brew install terraform doctl || true

Find your droplet ID:

doctl auth init
doctl compute droplet list

Configure

Copy example vars and edit:

cp terraform.tfvars.example terraform.tfvars

Edit:

  • droplet_id (existing droplet)
  • domain and hosts (subdomains)
  • le_email, SMTP vars

SMTP for Plausible vs Listmonk

  • Plausible reads specific env keys. This stack writes the correct keys to plausible-conf.env:
    • MAILER_EMAIL, MAILER_NAME
    • SMTP_HOST_ADDR, SMTP_HOST_PORT, SMTP_USER_NAME, SMTP_USER_PWD
    • SMTP_HOST_TLS_ENABLED, SMTP_HOST_SSL_ENABLED
  • Listmonk takes SMTP settings via its own LISTMONK_smtp__* env vars from the Compose file. It does not use plausible-conf.env.

Email senders

  • Plausible: mailer_email and optional mailer_name (default "Plausible").
  • Listmonk: listmonk_from_email and the global SMTP vars (smtp_*). From name defaults to "Newsletter" (change later in Listmonk UI if desired).

TLS/SSL

  • Use smtp_tls_enabled for STARTTLS (typically true on port 587) and smtp_ssl_enabled for SSL-on-connect (typically true on port 465). Set only the one your SMTP relay needs.

Google Search Console (optional)

  • To enable GSC imports in Plausible CE, set in terraform.tfvars:
    • google_client_id
    • google_client_secret
  • The stack writes these to plausible-conf.env as GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
  • After applying, restart only Plausible if needed: docker compose up -d --no-deps plausible.

Run

terraform init
terraform plan
terraform apply -auto-approve

When apply finishes, give the box 1–3 minutes to pull images and for Traefik to issue certs. Outputs will show your URLs.

Day-2

SSH in:

ssh root@<droplet-ip>
cd /opt/analytics-stack
docker compose ps
docker compose pull && docker compose up -d
docker logs -f traefik

DNS Notes

  • Create A-records for: plausible, listmonk, traefik, prom, grafana → droplet IPv4
  • Keep port 80 open for Let’s Encrypt HTTP-01.

Security

Dashboards (Traefik/Prom/Grafana) are exposed. Add Basic Auth by extending the Traefik labels in templates/docker-compose.tmpl (search for “Optional basic auth” comment).

Backups

Snapshot the droplet or back up Docker volumes:

  • plausible-db, plausible-ch, listmonk-db, grafana-data, prometheus-data.

Custom Listmonk Templates (Opt‑in Email)

This repo includes a static directory scaffold for overriding Listmonk’s system templates (e.g., the subscriber opt‑in email) via --static-dir.

  • Edit analytics-infra/static/email-templates/subscriber-optin.html and paste the upstream default content from the Listmonk repo, then apply your branding. Replace the placeholder confirmation URL variable with the correct one from upstream.
  • The Compose template mounts /opt/analytics-stack/static into the Listmonk container and starts Listmonk with --static-dir=/listmonk/static.

Deploy without restarting other services:

  1. Copy static files to the server
scp -r analytics-infra/static root@<droplet-ip>:/opt/analytics-stack/static
  1. Update the Compose file on the server
  • The template templates/docker-compose.tmpl is already updated to mount the static dir and pass --static-dir.
  • If you manage /opt/analytics-stack/docker-compose.yml manually, ensure the listmonk service has:
    • volumes: ["/opt/analytics-stack/static:/listmonk/static:ro"]
    • command: ./listmonk --static-dir=/listmonk/static (it’s appended after the install step in our template)
  1. Restart only Listmonk (no other services)
ssh root@<droplet-ip>
cd /opt/analytics-stack
docker compose pull listmonk            # optional
docker compose up -d --no-deps listmonk # restarts only listmonk

Data safety

  • The Postgres data for Listmonk lives in the listmonk-postgres container with the named volume listmonk-db; restarting the listmonk app service does not touch the database or other services.
  • The container mounts the static dir read‑only.

Verification

  • docker logs -f listmonk should log that it’s using the provided static dir.
  • Subscribe using a fresh email; the confirmation email should reflect your template.

Optional: Compose override to pin DB volume and static-dir

To avoid accidental volume changes and ensure the static dir is always mounted without editing the base compose file, use the provided override:

  1. Copy the override file to the server
scp analytics-infra/templates/docker-compose.override.yml \
    root@<droplet-ip>:/opt/analytics-stack/docker-compose.override.yml
  1. Copy the static folder if not already done
scp -r analytics-infra/static root@<droplet-ip>:/opt/analytics-stack/static
  1. (Recommended) Fix the compose project name to keep volume names stable
ssh root@<droplet-ip>
cd /opt/analytics-stack
echo COMPOSE_PROJECT_NAME=analytics-stack | sudo tee .env >/dev/null
  1. Apply only to Listmonk
ssh root@<droplet-ip>
cd /opt/analytics-stack
docker compose up -d --no-deps listmonk

Checks

  • docker compose exec listmonk ls -la /listmonk/static/email-templates
  • docker inspect $(docker compose ps -q listmonk) | grep -- --static-dir
  • docker volume ls | grep listmonk-db should show analytics-stack_listmonk-db

One-time init variant (first boot only)

If you’re provisioning a brand new Listmonk database, you can run a one-time init that installs the schema and then starts the app:

  1. Copy the init override alongside the regular override
scp analytics-infra/templates/docker-compose.override.init.yml \
    root@<droplet-ip>:/opt/analytics-stack/docker-compose.override.init.yml
  1. Include both overrides for the first start (only Listmonk)
ssh root@<droplet-ip>
cd /opt/analytics-stack
docker compose \
  -f docker-compose.yml \
  -f docker-compose.override.yml \
  -f docker-compose.override.init.yml \
  up -d --no-deps listmonk
  1. After Listmonk is healthy, stop using the init override
# Either remove the file or simply stop including it in future commands
rm /opt/analytics-stack/docker-compose.override.init.yml || true

# From now on, just use the base + regular override
docker compose up -d --no-deps listmonk

Safety notes

  • Only use the init override on a fresh/empty DB. To check, run: docker compose exec listmonk-postgres psql -U listmonk -d listmonk -c '\dt' and ensure there are no tables.
  • The DB volume is pinned as analytics-stack_listmonk-db; do not change it unless you intend to switch databases.

About

The Best Bang For Your Buck Blogging Infrastructure

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published