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:
@@ -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`.
|
||||
@@ -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 }}"
|
||||
@@ -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}}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Daily LubeLogger Postgres backup to PBS
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:30:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user