initial commit: Dawarich LXC role (CT 459 on pve02, .159)

Self-hosted location history. 4-container compose: Rails 8 app + Sidekiq
+ PostGIS 16-3.4 + Redis 7, plus watchtower. Authentik OIDC end-to-end.
Image pinned at freikin/dawarich:1.7.11 (OIDC support requires >= 1.7.8).

PostGIS DB lives in this LXC, not on the central DB VM (.172) — central
image is postgres:16-alpine without postgis, swapping it carries broader
blast radius than colocating here. Convention exception captured in
homelab-docs project_dawarich memory.

Roles:
  - dawarich: system + Docker + compose + weekly prune timer
  - alloy:    logs+journald → Loki, node metrics → Prometheus

Bring-up sequence proven 2026-06-01. README documents the 5-trap build
chain (image version, entrypoint scripts, solid_cache SQLite bind mount,
APPLICATION_HOSTS+localhost, force_ssl+healthcheck).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-06-01 21:24:09 -04:00
commit e7b8d4df17
20 changed files with 1126 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
// Alloy — Docker container logs + journald → Loki. River syntax.
// Per-LXC config; only the `host:` label changes via alloy_host_label var.
// ============================================================
// Docker — discover + scrape container logs
// ============================================================
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "15s"
}
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.+)"
target_label = "container"
}
rule {
source_labels = ["__meta_docker_container_log_stream"]
target_label = "stream"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "service"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
target_label = "compose_project"
}
rule {
target_label = "job"
replacement = "docker"
}
rule {
target_label = "host"
replacement = "{{ alloy_host_label }}"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.relabel.containers.output
forward_to = [loki.process.docker.receiver]
}
loki.process "docker" {
// Replay protection on first boot — Loki rejects > 7d.
stage.drop {
older_than = "24h"
drop_counter_reason = "older_than_24h"
}
// pulse-agent broken-pipe noise on hosts running pulse-agent --enable-host.
stage.drop {
expression = ".*broken pipe.*"
drop_counter_reason = "broken_pipe_noise"
}
forward_to = [loki.write.default.receiver]
}
// ============================================================
// Journald — LXC systemd units
// ============================================================
loki.source.journal "host" {
path = "/var/log/journal"
max_age = "1m"
forward_to = [loki.process.journal.receiver]
relabel_rules = loki.relabel.journal.rules
labels = {
job = "journald",
host = "{{ alloy_host_label }}",
}
}
loki.relabel "journal" {
forward_to = []
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
rule {
source_labels = ["__journal__hostname"]
target_label = "instance"
}
rule {
source_labels = ["__journal_priority_keyword"]
target_label = "severity"
}
}
loki.process "journal" {
forward_to = [loki.write.default.receiver]
}
// ============================================================
// Loki — push
// ============================================================
loki.write "default" {
endpoint {
url = "{{ alloy_loki_url }}"
}
}
// ============================================================
// Prometheus — embedded node_exporter (replaces standalone)
// ============================================================
prometheus.exporter.unix "node" {
// Container view of host filesystems — bind-mounted in compose.
rootfs_path = "/host/rootfs"
procfs_path = "/host/proc"
sysfs_path = "/host/sys"
// Default collectors match upstream node_exporter set.
}
prometheus.scrape "node" {
targets = prometheus.exporter.unix.node.targets
forward_to = [prometheus.relabel.node.receiver]
scrape_interval = "15s"
job_name = "{{ alloy_prom_job | default('node_lxc') }}"
}
// Rewrite the instance + job labels to match the historical
// node_exporter scrape, so existing dashboards continue to slice by
// the same instance string. Alloy's exporter component injects
// job="integrations/unix" on its targets — we have to overwrite it
// here; `job_name` in prometheus.scrape only names the scrape pool,
// it doesn't relabel the metrics.
prometheus.relabel "node" {
forward_to = [prometheus.remote_write.observe.receiver]
rule {
target_label = "instance"
replacement = "{{ alloy_prom_instance | default(ansible_default_ipv4.address + ':9100') }}"
}
rule {
target_label = "job"
replacement = "{{ alloy_prom_job | default('node_lxc') }}"
}
// Match the static labels Prom previously injected at scrape time
// (see homelab-ansible-vm-observability/roles/configs/files/prometheus.yml).
rule {
target_label = "group"
replacement = "{{ alloy_prom_group | default('lxc') }}"
}
rule {
target_label = "hostname"
replacement = "{{ alloy_prom_hostname | default(alloy_host_label) }}"
}
}
prometheus.remote_write "observe" {
endpoint {
url = "{{ alloy_prom_remote_write | default('http://observe.lan.balders.ca:9090/api/v1/write') }}"
}
}