e7b8d4df17
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>
188 lines
8.0 KiB
Django/Jinja
188 lines
8.0 KiB
Django/Jinja
# {{ ansible_managed }}
|
|
# Dawarich — self-hosted location history. Four containers + watchtower:
|
|
# dawarich_db postgis/postgis:16-3.4-alpine (PostGIS-enabled Postgres)
|
|
# dawarich_redis redis:7-alpine (cache + Sidekiq queue)
|
|
# dawarich_app freikin/dawarich (Rails web tier, port 3000)
|
|
# dawarich_sidekiq freikin/dawarich (background jobs)
|
|
# DB lives here (not the central DB VM) because the central image is plain
|
|
# postgres:16-alpine without the postgis extension. See memory project_dawarich.
|
|
|
|
services:
|
|
dawarich_db:
|
|
image: "{{ postgis_image }}"
|
|
container_name: dawarich_db
|
|
restart: unless-stopped
|
|
shm_size: 1g
|
|
environment:
|
|
POSTGRES_USER: "{{ dawarich_db_user }}"
|
|
POSTGRES_PASSWORD: "{{ dawarich_db_password }}"
|
|
POSTGRES_DB: "{{ dawarich_db_name }}"
|
|
volumes:
|
|
- /opt/dawarich/postgres:/var/lib/postgresql/data
|
|
# No published ports — only the app + sidekiq reach it over the compose net.
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U {{ dawarich_db_user }} -d {{ dawarich_db_name }}"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
dawarich_redis:
|
|
image: "{{ redis_image }}"
|
|
container_name: dawarich_redis
|
|
restart: unless-stopped
|
|
volumes:
|
|
- /opt/dawarich/redis:/data
|
|
# No published ports — internal-only.
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 15s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
dawarich_app:
|
|
image: "{{ dawarich_image }}"
|
|
container_name: dawarich_app
|
|
restart: unless-stopped
|
|
depends_on:
|
|
dawarich_db:
|
|
condition: service_healthy
|
|
dawarich_redis:
|
|
condition: service_healthy
|
|
ports:
|
|
- "{{ dawarich_port }}:3000"
|
|
volumes:
|
|
- /opt/dawarich/public:/var/app/public
|
|
- /opt/dawarich/watched:/var/app/tmp/imports/watched
|
|
- /opt/dawarich/storage:/var/app/storage
|
|
# Solid_cache + solid_queue + solid_cable SQLite files. Without this
|
|
# bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost
|
|
# on container recreate. Path matches the entrypoint's expectation.
|
|
- /opt/dawarich/db_data:/dawarich_db_data
|
|
# The image ships a `web-entrypoint.sh` that runs `db:prepare` (creates
|
|
# the solid_cache + solid_queue SQLite files in /var/app/storage, runs
|
|
# Postgres migrations) before exec'ing the command. We MUST use it —
|
|
# without it, Rails 8 boots fail with "No database file specified" on
|
|
# solid_cache's first lookup. The image default entrypoint is `bundle exec`
|
|
# alone, which skips the prep.
|
|
entrypoint: ["/usr/local/bin/web-entrypoint.sh"]
|
|
command: ["bin/rails", "server", "-p", "3000", "-b", "::"]
|
|
environment:
|
|
RAILS_ENV: production
|
|
SELF_HOSTED: "true"
|
|
# localhost + ::1 needed for Docker healthcheck (Rails 8 host
|
|
# authorization rejects any host not in this list, including loopback).
|
|
APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1"
|
|
APPLICATION_PROTOCOL: "https"
|
|
DOMAIN: "{{ dawarich_domain }}"
|
|
TIME_ZONE: "{{ timezone }}"
|
|
DISTANCE_UNIT: "km"
|
|
# Database
|
|
DATABASE_HOST: dawarich_db
|
|
DATABASE_PORT: "5432"
|
|
DATABASE_USERNAME: "{{ dawarich_db_user }}"
|
|
DATABASE_PASSWORD: "{{ dawarich_db_password }}"
|
|
DATABASE_NAME: "{{ dawarich_db_name }}"
|
|
RAILS_MAX_THREADS: "5"
|
|
# Redis
|
|
REDIS_URL: "redis://dawarich_redis:6379"
|
|
# Secrets
|
|
SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}"
|
|
OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}"
|
|
OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}"
|
|
OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}"
|
|
# Registration policy — OIDC-only when enabled. Anything else and a
|
|
# local user form silently appears on /users/sign_up.
|
|
ALLOW_EMAIL_PASSWORD_REGISTRATION: "{{ dawarich_allow_email_password_registration | string | lower }}"
|
|
{% if dawarich_oidc_enabled %}
|
|
# ---- Auth: Authentik OIDC ----
|
|
OIDC_CLIENT_ID: "{{ dawarich_oidc_client_id }}"
|
|
OIDC_CLIENT_SECRET: "{{ dawarich_oidc_client_secret }}"
|
|
OIDC_ISSUER: "{{ dawarich_oidc_issuer }}"
|
|
OIDC_REDIRECT_URI: "{{ dawarich_oidc_redirect_uri }}"
|
|
OIDC_PROVIDER_NAME: "{{ dawarich_oidc_provider_name }}"
|
|
OIDC_AUTO_REGISTER: "{{ dawarich_oidc_auto_register | string | lower }}"
|
|
OIDC_PKCE_ENABLED: "true"
|
|
{% endif %}
|
|
# TCP socket check, not HTTP. Rails 8 force_ssl (APPLICATION_PROTOCOL=https)
|
|
# redirects every plain-HTTP path to https — curl/wget can't satisfy a 2xx
|
|
# without TLS, and Puma is plain-HTTP (Caddy terminates TLS upstream). The
|
|
# socket check confirms Puma is listening, which is what we actually care
|
|
# about for orchestration.
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "ruby -rsocket -e 'TCPSocket.new(\"localhost\",3000).close' || exit 1"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 120s
|
|
|
|
dawarich_sidekiq:
|
|
image: "{{ dawarich_image }}"
|
|
container_name: dawarich_sidekiq
|
|
restart: unless-stopped
|
|
depends_on:
|
|
dawarich_db:
|
|
condition: service_healthy
|
|
dawarich_redis:
|
|
condition: service_healthy
|
|
dawarich_app:
|
|
condition: service_started
|
|
volumes:
|
|
- /opt/dawarich/public:/var/app/public
|
|
- /opt/dawarich/watched:/var/app/tmp/imports/watched
|
|
- /opt/dawarich/storage:/var/app/storage
|
|
# Solid_cache + solid_queue + solid_cable SQLite files. Without this
|
|
# bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost
|
|
# on container recreate. Path matches the entrypoint's expectation.
|
|
- /opt/dawarich/db_data:/dawarich_db_data
|
|
# Same entrypoint pattern as the web tier — sidekiq's script ensures the
|
|
# DB is migrated before workers start consuming jobs.
|
|
entrypoint: ["/usr/local/bin/sidekiq-entrypoint.sh"]
|
|
command: ["sidekiq"]
|
|
environment:
|
|
RAILS_ENV: production
|
|
SELF_HOSTED: "true"
|
|
# localhost + ::1 needed for Docker healthcheck (Rails 8 host
|
|
# authorization rejects any host not in this list, including loopback).
|
|
APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1"
|
|
APPLICATION_PROTOCOL: "https"
|
|
DOMAIN: "{{ dawarich_domain }}"
|
|
TIME_ZONE: "{{ timezone }}"
|
|
DATABASE_HOST: dawarich_db
|
|
DATABASE_PORT: "5432"
|
|
DATABASE_USERNAME: "{{ dawarich_db_user }}"
|
|
DATABASE_PASSWORD: "{{ dawarich_db_password }}"
|
|
DATABASE_NAME: "{{ dawarich_db_name }}"
|
|
REDIS_URL: "redis://dawarich_redis:6379"
|
|
SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}"
|
|
OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}"
|
|
OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}"
|
|
OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}"
|
|
BACKGROUND_PROCESSING_CONCURRENCY: "{{ dawarich_sidekiq_concurrency }}"
|
|
|
|
watchtower:
|
|
container_name: dawarich_watchtower
|
|
image: containrrr/watchtower:latest
|
|
restart: unless-stopped
|
|
dns:
|
|
- 192.168.1.1
|
|
ports:
|
|
- "8088:8080"
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
environment:
|
|
- DOCKER_API_VERSION=1.40
|
|
- WATCHTOWER_HTTP_API_UPDATE=true
|
|
- WATCHTOWER_HTTP_API_TOKEN={{ watchtower_api_token }}
|
|
- TZ={{ timezone }}
|
|
- WATCHTOWER_CLEANUP=true
|
|
- WATCHTOWER_INCLUDE_RESTARTING=true
|
|
- WATCHTOWER_NO_STARTUP_MESSAGE=true
|
|
- WATCHTOWER_NOTIFICATION_REPORT=true
|
|
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
|
- WATCHTOWER_NOTIFICATION_URL={{ watchtower_gotify_url }}
|
|
- WATCHTOWER_NOTIFICATIONS_HOSTNAME=DAWARICH
|
|
- WATCHTOWER_NOTIFICATION_TITLE_TAG=DAWARICH
|
|
# Render empty when nothing was updated/failed — see feedback_watchtower_pushover.
|
|
- 'WATCHTOWER_NOTIFICATION_TEMPLATE={% raw %}{{- if .Report -}}{{- with .Report -}}{{- if or .Updated .Failed -}}{{ len .Updated }} updated, {{ len .Failed }} failed{{- range .Updated}} | updated {{.Name}}{{- end -}}{{- range .Failed}} | FAILED {{.Name}}{{- end -}}{{- end -}}{{- end -}}{{- end -}}{% endraw %}'
|