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

 
 
 

Contributors