Add LubeLogger deployment (docker-node01)

Ansible-managed (inline-compose, file-provider router, host :8099), idempotent
(changed=0). PBS-backed pg_dump (daily, keep 14) with verified round-trip restore;
ntfy 'garage' reminder probe (.190); Diun repo-watch for new tags; Homepage
tile+widget. Secrets via ansible-vault / /etc env files / HOMEPAGE_VAR — none in repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tommy
2026-07-02 15:14:20 -05:00
parent 9e6afe27c5
commit c9aac20e73
10 changed files with 278 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# LubeLogger — deployment source of truth
Vehicle & small-engine maintenance tracker. **Host:** docker-node01 (.186).
Postgres 18 backend (tables in the `app` schema). In-app auth ENABLED (root user).
## Routing (file provider)
Published on the host at `:8099`; Traefik on node01 routes `lubelogger.goattw.net`
`http://192.168.99.186:8099` via `dynamic_conf.yml` (see `traefik-router.dynamic_conf.yml`).
Edge chain: TLS (letsencrypt) → CrowdSec (global) → Authelia (`authelia-auth@file`,
`two_factor` off-LAN / bypass on-LAN). LubeLogger's own auth gates the LAN/`:8099` path.
## Deploy (Ansible, idempotent)
Playbook `deploy-lubelogger.yml` (runs from `~/ansible` on ansible-control):
```
ansible-playbook playbooks/deploy-lubelogger.yml
```
Inline-compose house style: `copy:` the compose + `community.docker.docker_compose_v2`.
DB password is sourced from ansible-vault (`vault_lubelogger_db_password`) — never inline.
Second converge is `changed=0`.
## Backup (PBS)
`lubelogger-backup.{sh,service,timer}` on node01 — daily 03:30, `pg_dump` → PBS
(`rust-usb`, host/lubelogger), prune `--keep-daily 14`. PBS creds live in
`/etc/lubelogger-backup.env` (root:600, NOT in git). Restore verified via full
PBS round-trip into a throwaway postgres:18 (live↔restored row-count parity).
NOTE: the `lubelogger-data`/`lubelogger-keys` volumes (uploads, config, the ROOT USER
credential, ASP.NET keyring) are a SEPARATE layer — covered by docker-volume-backup
(`stop-during-backup` labels already set). pg_dump captures Postgres data only.
## Reminders → ntfy
`lubelogger-reminders-probe.{sh}` + units on ansible-control (.190) — daily 07:30.
Reads `/api/vehicle/info` (Basic auth, creds in `/etc/lubelogger-reminders.env`,
root:600, NOT in git), posts a digest to ntfy topic **garage** only when something is
due/overdue. Subscribe to the `garage` topic in your ntfy app to receive it.
## Diun
App carries `diun.watch_repo=true` + semver `include_tags` so a new release tag pings
the existing Diun→HA-webhook pipeline. DB (`postgres:18`) is watched by default (digest).
## Homepage
Tile + `lubelogger` widget under "AI & Productivity" in
`observability/homepage/services.yaml` (live) — widget password via
`HOMEPAGE_VAR_LUBELOGGER_PASSWORD` (observability/.env, not a git repo). See
`homepage-tile.yaml`.
## Secrets (none in this repo)
- DB password: ansible-vault (`~/ansible`), and Postgres volume (already initialized).
- PBS token: `/etc/lubelogger-backup.env` (reuses vaultwarden token — a dedicated
`lubelogger-backup` PBS token would be cleaner; needs PBS root to create).
- Admin/basic-auth password: `/etc/lubelogger-reminders.env` + `observability/.env`.
+104
View File
@@ -0,0 +1,104 @@
---
- name: Deploy LubeLogger on docker-node01
hosts: docker-node01
become: true
vars:
app_dir: /home/tommy/lubelogger
app_port: 8099
tasks:
- name: Create lubelogger directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: tommy
group: tommy
mode: "0755"
- name: Write docker-compose.yml
ansible.builtin.copy:
dest: "{{ app_dir }}/docker-compose.yml"
owner: tommy
group: tommy
mode: "0644"
content: |
# lubelogger - Vehicle & small-engine maintenance tracker
# Host: docker-node01 (.186), Port: {{ app_port }}
# Traefik: lubelogger.goattw.net (Authelia protected, @file router -> :{{ app_port }})
# Backend: PostgreSQL 18 (tables live in the `app` schema).
# In-app auth is ENABLED (root user) - the published host port is safe.
# Do NOT lose the lubelogger-keys volume (ASP.NET DataProtection keyring).
services:
lubelogger:
image: ghcr.io/hargata/lubelogger:v1.6.8
container_name: lubelogger
restart: unless-stopped
depends_on:
- lubelogger-db
ports:
- "{{ app_port }}:8080"
environment:
TZ: America/Chicago
LUBELOGGER_LOCALE_OVERRIDE: "en_US"
POSTGRES_CONNECTION: "Host=lubelogger-db:5432;Username=lubelogger;Password={{ vault_lubelogger_db_password }};Database=lubelogger;"
volumes:
- lubelogger-data:/App/data
- lubelogger-keys:/root/.aspnet/DataProtection-Keys
networks:
- lubelogger-int
labels:
- "docker-volume-backup.stop-during-backup=true"
# Diun watches by default (digest); watch_repo + semver filter
# so a NEW release tag (e.g. v1.6.9) pings the existing Diun->HA pipeline.
- "diun.enable=true"
- "diun.watch_repo=true"
- 'diun.include_tags=^v[0-9]+\.[0-9]+\.[0-9]+$'
lubelogger-db:
image: postgres:18
container_name: lubelogger-db
restart: unless-stopped
environment:
POSTGRES_USER: lubelogger
POSTGRES_PASSWORD: "{{ vault_lubelogger_db_password }}"
POSTGRES_DB: lubelogger
TZ: America/Chicago
volumes:
- lubelogger-db:/var/lib/postgresql
- /etc/localtime:/etc/localtime:ro
networks:
- lubelogger-int
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lubelogger -d lubelogger"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
labels:
- "docker-volume-backup.stop-during-backup=true"
networks:
lubelogger-int:
driver: bridge
volumes:
lubelogger-data:
lubelogger-keys:
lubelogger-db:
- name: Remove stale .env (secret now sourced from ansible-vault)
ansible.builtin.file:
path: "{{ app_dir }}/.env"
state: absent
- name: Deploy lubelogger with docker compose
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
pull: missing
register: app_result
- name: Show deployment result
ansible.builtin.debug:
msg: "lubelogger is {{ 'running (changed)' if app_result.changed else 'already up (no-op)' }} on port {{ app_port }}"
+10
View File
@@ -0,0 +1,10 @@
- LubeLogger:
icon: lubelogger.png
href: https://lubelogger.goattw.net
description: Vehicle & small-engine maintenance
siteMonitor: https://lubelogger.goattw.net
widget:
type: lubelogger
url: http://192.168.99.186:8099
username: tommy
password: {{HOMEPAGE_VAR_LUBELOGGER_PASSWORD}}
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=LubeLogger Postgres dump -> PBS
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/lubelogger-backup.sh
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# LubeLogger Postgres logical backup -> Proxmox Backup Server.
# Installed at /usr/local/bin/lubelogger-backup.sh ; driven by lubelogger-backup.timer.
set -euo pipefail
source /etc/lubelogger-backup.env
export PBS_PASSWORD PBS_FINGERPRINT
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# Consistent logical dump (includes the `app` schema + data). App stays online.
docker exec lubelogger-db pg_dump -U lubelogger lubelogger > "$TMPDIR/lubelogger.sql"
# Push to PBS
proxmox-backup-client backup \
"lubelogger-db.pxar:$TMPDIR" \
--repository "$PBS_REPOSITORY" \
--backup-type host \
--backup-id lubelogger
# Retain ~14 daily snapshots
proxmox-backup-client prune "host/lubelogger" \
--repository "$PBS_REPOSITORY" \
--keep-daily 14
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Daily LubeLogger Postgres backup to PBS
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
# Daily LubeLogger reminder digest -> ntfy 'garage' topic.
# Reads per-vehicle reminder urgency counts from LubeLogger's API and posts a
# summary ONLY when something is due/overdue (PastDue + VeryUrgent + Urgent).
# Runs on ansible-control (.190) via lubelogger-reminders.timer.
# Shape mirrors the gluetun vpn-tunnel-probe (oneshot + timer, set -euo pipefail).
source /etc/lubelogger-reminders.env # LUBELOGGER_URL LUBELOGGER_USER LUBELOGGER_PASS NTFY_URL
if ! resp=$(curl -sf --max-time 20 -u "${LUBELOGGER_USER}:${LUBELOGGER_PASS}" "${LUBELOGGER_URL}/api/vehicle/info"); then
logger -t lubelogger-reminders "ERROR: LubeLogger API query failed"
curl -sf --max-time 15 -H "Priority: high" -H "Tags: warning" \
-H "Title: LubeLogger reminder probe failed" \
-d "Could not query ${LUBELOGGER_URL}/api/vehicle/info" "$NTFY_URL" || true
exit 1
fi
total=$(printf '%s' "$resp" | jq '[.[] | (.PastDueReminderCount + .VeryUrgentReminderCount + .UrgentReminderCount)] | add // 0')
if [ "${total:-0}" -gt 0 ]; then
body=$(printf '%s' "$resp" | jq -r '
.[]
| (.PastDueReminderCount + .VeryUrgentReminderCount + .UrgentReminderCount) as $due
| select($due > 0)
| "• \(.VehicleData.Year) \(.VehicleData.Make) \(.VehicleData.Model): "
+ "\(.PastDueReminderCount) past-due, \(.VeryUrgentReminderCount) very-urgent, \(.UrgentReminderCount) urgent"')
curl -sf --max-time 15 \
-H "Title: Vehicle maintenance due (${total})" \
-H "Tags: wrench" -H "Priority: default" \
-d "$body" "$NTFY_URL" || true
logger -t lubelogger-reminders "posted ${total} due reminder(s) to ntfy garage"
else
logger -t lubelogger-reminders "no due/overdue reminders"
fi
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=LubeLogger reminder digest -> ntfy garage
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/home/tommy/ansible/scripts/lubelogger-reminders-probe.sh
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Daily LubeLogger reminder digest
[Timer]
OnCalendar=*-*-* 07:30:00
Persistent=true
[Install]
WantedBy=timers.target
@@ -0,0 +1,21 @@
# File-provider router for LubeLogger — merged into node01
# /home/tommy/traefik/dynamic_conf.yml (live) under http.routers and http.services.
# Traefik watch=true hot-reloads on change. (Reference copy; the live file is authoritative.)
# --- under http.routers: ---
lubelogger:
rule: "Host(`lubelogger.goattw.net`)"
entryPoints:
- websecure
middlewares:
- authelia-auth@file # crowdsec-bouncer@file applies globally at the websecure entrypoint
service: lubelogger-service
tls:
certResolver: letsencrypt
# --- under http.services: ---
lubelogger-service:
loadBalancer:
servers:
- url: "http://192.168.99.186:8099"
serversTransport: default-transport