diff --git a/README.md b/README.md index 58a4a350..04d79404 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,7 @@ Every stack starts from the same [standard setup](documentation/standard-setup.m | 🛰️ **Beszel Agent** | The Beszel Agent collects stats and reports them back to the Hub. | [Details](services/beszel-agent) | | 🔎 **Portracker** | A simple, self-hosted port monitoring and tracking tool for auditing open ports. | [Details](services/portracker) | | 🚀 **Speedtest Tracker** | A self-hosted tool to monitor and log internet speed tests with detailed visualizations. | [Details](services/speedtest-tracker) | +| 📊 **Umami** | An open-source web analytics platform that respects user privacy. | [Details](services/umami) | | 📊 **Uptime Kuma** | A self-hosted monitoring tool like "Uptime Robot". | [Details](services/uptime-kuma) | ### 🏠 Smart Home diff --git a/services/umami/.env b/services/umami/.env new file mode 100644 index 00000000..21cbbb16 --- /dev/null +++ b/services/umami/.env @@ -0,0 +1,43 @@ +#version=1.1 +#URL=https://github.com/tailscale-dev/ScaleTail +#COMPOSE_PROJECT_NAME= # Optional: only use when running multiple deployments on the same infrastructure. + +# Service Configuration +# Service name (e.g., adguard). Used as hostname in Tailscale and for container naming (app-${SERVICE}). +SERVICE=umami +# Docker image URL from container registry (e.g., adguard/adguard-home). +IMAGE_URL=ghcr.io/umami-software/umami:latest + +# Network Configuration +# Port to expose to local network. Uncomment the "ports:" section in compose.yaml to enable. +SERVICEPORT=3000 +DNS_SERVER=9.9.9.9 # Preferred DNS server for Tailscale. Uncomment the "dns:" section in compose.yaml to enable. + +# Tailscale Configuration +# Auth key from https://tailscale.com/admin/authkeys. See: https://tailscale.com/kb/1085/auth-keys#generate-an-auth-key for instructions. +TS_AUTHKEY= + +# Optional Service variables +# PUID=1000 + +# Time Zone setting for containers +TZ=Europe/Amsterdam # See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + +# Any Container environment variables are declared below. See https://docs.docker.com/compose/how-tos/environment-variables/ + +# Required: a random value. Generate with: openssl rand -hex 32 +APP_SECRET= + +# Database Configuration +DB_IMAGE_URL=postgres:15-alpine +DB_NAME=umami +DB_USER=umami +# Required: password for the Umami PostgreSQL user. Use letters and digits only, because it is part of DATABASE_URL. +# PostgreSQL applies it only when the database is first created. +DB_PASSWORD= + +# The Serve configuration to use. +# 'private' keeps all access to Umami within your Tailnet. +# 'public' exposes the tracking script and the data collection endpoint through Funnel, +# so that websites with visitors outside your Tailnet can report analytics. +SERVE_CONFIG=private diff --git a/services/umami/README.md b/services/umami/README.md new file mode 100644 index 00000000..50e70135 --- /dev/null +++ b/services/umami/README.md @@ -0,0 +1,47 @@ +# Umami + +[Umami](https://umami.is/) is a web analytics platform that respects the privacy of your visitors. It uses no cookies and collects no personal data. + +This stack runs Umami with a Tailscale sidecar, as described in [the standard setup](../../documentation/standard-setup.md). + +## At a glance + +| Item | Value | +| ------------- | ------------------------------------------ | +| Web interface | `https://umami..ts.net` | +| Service port | `3000` | +| Images | `ghcr.io/umami-software/umami:latest` | +| | `postgres:15-alpine` | +| Data | `./umami-data/db` (PostgreSQL database) | + +## Before you start + +Set these values in `.env`. Compose stops with an error if one of them is empty. + +- **`APP_SECRET`.** A random value. Generate one with `openssl rand -hex 32`. +- **`DB_PASSWORD`.** The password of the database. Use letters and digits only, because the value is part of the database address. PostgreSQL applies it only when it first creates the database. + +## Deviations from the standard setup + +- **Extra container.** The stack runs `database` (PostgreSQL). It uses the default Compose network, and Umami reaches it by its service name through Docker's DNS. Keep `TS_ACCEPT_DNS` disabled, because MagicDNS cannot resolve this name. +- **Two Serve configurations.** `compose.yaml` defines `ts-serve-private` and `ts-serve-public`. `SERVE_CONFIG` in `.env` selects one of them. The default is `private`, which matches the standard setup. +- **Funnel in public mode.** With `SERVE_CONFIG=public`, Tailscale Funnel publishes `/script.js` and `/api/send` on port 443 to the public internet. The web interface moves to `https://umami..ts.net:8443` and stays Tailnet-only. + +## First run + +Open the web interface and log in with the username `admin` and the password `umami`. Change this password right after the first login. + +Then add a website in Umami and embed its tracking script in your site, as described in the [Umami documentation](https://umami.is/docs/collect-data): + +```html + +``` + +## Configuration + +- **`SERVE_CONFIG`.** Use `private` when all visitors of your websites are on your Tailnet. Use `public` to collect data from websites with visitors outside your Tailnet. Public mode needs [Funnel](https://tailscale.com/kb/1223/funnel) allowed for the device in your Tailnet policy. Recreate the stack after you change the value. + +## Links + +- [Umami documentation](https://umami.is/docs) +- [Umami source code](https://github.com/umami-software/umami) diff --git a/services/umami/compose.yaml b/services/umami/compose.yaml new file mode 100644 index 00000000..802f04a9 --- /dev/null +++ b/services/umami/compose.yaml @@ -0,0 +1,101 @@ +configs: + ts-serve-private: + content: | + {"TCP":{"443":{"HTTPS":true}}, + "Web":{"$${TS_CERT_DOMAIN}:443": + {"Handlers":{"/": + {"Proxy":"http://127.0.0.1:3000"}}}}, + "AllowFunnel":{"$${TS_CERT_DOMAIN}:443":false}} + ts-serve-public: + content: | + {"TCP": {"443": {"HTTPS": true}, "8443": {"HTTPS": true}}, + "Web": {"$${TS_CERT_DOMAIN}:443": + {"Handlers": {"/script.js": + {"Proxy": "http://127.0.0.1:3000/script.js"}, + "/api/send": + {"Proxy": "http://127.0.0.1:3000/api/send"}}}, + "$${TS_CERT_DOMAIN}:8443": { + "Handlers": {"/": + {"Proxy": "http://127.0.0.1:3000/"}}}}, + "AllowFunnel": {"$${TS_CERT_DOMAIN}:443": true, "$${TS_CERT_DOMAIN}:8443": false}} + +services: +# Make sure you have updated/checked the .env file with the correct variables. +# Every variable used in this file must be defined there. + # Tailscale Sidecar Configuration + tailscale: + image: tailscale/tailscale:latest # Image to be used + container_name: tailscale-${SERVICE} # Name for local container management + hostname: ${SERVICE} # Name used within your Tailscale environment + environment: + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_SERVE_CONFIG=/config/serve.json # Tailscale Serve configuration to expose the web interface on your local Tailnet - remove this line if not required + - TS_USERSPACE=false + - TS_ENABLE_HEALTH_CHECK=true # Enable healthcheck endpoint: "/healthz" + - TS_LOCAL_ADDR_PORT=127.0.0.1:41234 # The : for the healthz endpoint + #- TS_ACCEPT_DNS=true # Uncomment only if the service must resolve MagicDNS names - this replaces Docker DNS, so Compose service names no longer resolve + - TS_AUTH_ONCE=true + configs: + - source: ts-serve-${SERVE_CONFIG} + target: /config/serve.json + volumes: + - ./config:/config # Config folder used to store Tailscale files - you may need to change the path + - ./ts/state:/var/lib/tailscale # Tailscale requirement - you may need to change the path + devices: + - /dev/net/tun:/dev/net/tun # Network configuration for Tailscale to work + cap_add: + - net_admin # Tailscale requirement + #ports: + # - 0.0.0.0:${SERVICEPORT}:${SERVICEPORT} # Binding the service port to the local network - may be removed if only exposure to your Tailnet is required + # If any DNS issues arise, use your preferred DNS provider by uncommenting the config below + #dns: + # - ${DNS_SERVER} + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"] # Check Tailscale has a Tailnet IP and is operational + interval: 1m # How often to perform the check + timeout: 10s # Time to wait for the check to succeed + retries: 3 # Number of retries before marking as unhealthy + start_period: 10s # Time to wait before starting health checks + restart: always + + # Umami + application: + image: ${IMAGE_URL} # Image to be used + network_mode: service:tailscale # Sidecar configuration to route the service through Tailscale + container_name: app-${SERVICE} # Name for local container management + environment: # Variables are declared in .env file. + - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD:?Set DB_PASSWORD in .env}@database:5432/${DB_NAME} + - APP_SECRET=${APP_SECRET:?Set APP_SECRET in .env} + depends_on: + tailscale: + condition: service_healthy + database: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/api/heartbeat"] # Check if the service is responding + interval: 1m # How often to perform the check + timeout: 10s # Time to wait for the check to succeed + retries: 3 # Number of retries before marking as unhealthy + start_period: 30s # Time to wait before starting health checks + restart: always + + # PostgreSQL database + database: + image: ${DB_IMAGE_URL} + container_name: db-${SERVICE} + environment: # Variables are declared in .env file. + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} + # Point PGDATA to a subfolder inside the volume mount + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - ./${SERVICE}-data/db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] # Check if PostgreSQL accepts connections + interval: 5s # How often to perform the check + timeout: 3s # Time to wait for the check to succeed + retries: 10 # Number of retries before marking as unhealthy + start_period: 10s # Time to wait before starting health checks + restart: always