Architecture: Rails App (Shared Pattern)

arch-rails-app Architecture

architecture convention rails

Architecture: Rails App (Shared Pattern)

Common deployment architecture for Rails applications on the pal-e platform. Currently implemented by landscaping-assistant (production) and palinks (in development). New Rails apps SHOULD follow this pattern unless there is a documented reason to diverge.

Diagram

Components

Component Purpose Notes
Rails Pod Application server Rails 8.1, Hotwire (Turbo + Stimulus). Monolithic -- no separate frontend, Rails serves HTML directly. Importmap for JS, no build pipeline.
CNPG PostgreSQL 17 Primary database CloudNativePG operator on k3s. Shared cluster, per-app databases. Handles failover, backups, WAL archiving.
Keycloak OIDC Authentication (opt-in) Authorization Code + PKCE via OmniAuth. OAuth 2.1 compliant. NOT all apps have this -- landscaping-assistant: yes, palinks: planned.
MinIO S3 Object storage (opt-in) ActiveStorage with S3-compatible backend. Local disk in dev. Only for apps with file/image uploads -- landscaping-assistant: yes, palinks: no.
Woodpecker CI CI pipeline Triggered by Forgejo webhook. Stages: test (rspec + postgres service), build (multi-stage Docker), push to Harbor.
Harbor Registry Container image store harbor.tail5b443a.ts.net. Images tagged with full commit SHA. Trivy vulnerability scanning.
ArgoCD + Image Updater Continuous deployment Image Updater detects new SHA tags, writes to kustomize overlay in pal-e-deployments. ArgoCD syncs to cluster.
ServiceMonitor Metrics scraping Prometheus scrapes /metrics endpoint exposed by yabeda-rails + yabeda-prometheus.
Grafana + Loki Observability Grafana dashboards for metrics. Loki + Promtail for log aggregation. Structured JSON logs via lograge.
Caddy (Hetzner edge) Public reverse proxy + TLS 178.156.129.142, CPX11 Ashburn. Auto-TLS for custom domains. Connected to home lab via Tailscale mesh (tail5b443a.ts.net).
GoDaddy DNS Domain management A records pointing custom domains to Hetzner edge IP. Managed by godaddy-tofu provider via pal-e-platform/terraform/.
Kustomize overlays Deployment manifests Live in pal-e-deployments repo. Follow convention-kustomize-overlay pattern. Image tags updated by ArgoCD Image Updater.

Deployment Pipeline

  • Push to Forgejo -- triggers Woodpecker CI webhook
  • CI: Test -- bundle exec rspec against PostgreSQL service container (version 17, matching prod)
  • CI: Build -- multi-stage Docker build
  • CI: Push -- push image to Harbor (harbor.tail5b443a.ts.net/project/app-name:<commit-sha>)
  • ArgoCD Image Updater -- detects new tag (regexp ^[0-9a-f]{7,40}$), writes SHA to kustomize overlay
  • ArgoCD Sync -- applies updated kustomization to k3s cluster
  • Auto-migration -- rails db:migrate runs on deploy

Public Domain Routing

Three steps to a public domain: (1) godaddy-tofu provider creates A record pointing to 178.156.129.142, (2) Caddy on Hetzner edge terminates TLS and reverse-proxies via Tailscale, (3) k3s ClusterIP service routes to Rails pod.

Variations by App

Concern landscaping-assistant palinks
Auth Keycloak (Auth Code + PKCE, OmniAuth). Five-role model. Session-based. Planned, not yet integrated.
Object storage ActiveStorage + MinIO for photo uploads. Not needed (link app, no file uploads).
Mobile Turbo Native iOS build. Dev funnel at landscaping-dev.tail5b443a.ts.net. Web-only.
Observability depth Full Grafana ecosystem: 26 dashboards, golden signals, blackbox probes, DORA metrics. ServiceMonitor deployed, metrics parity achieved.
Domain-specific services Nominatim geocoding. None.
Test suite 230+ RSpec specs (model, request, system). Capybara + Cuprite. In development.
Custom domain landscaping-assistant.app (when configured). palinks.app (DNS pending godaddy-tofu integration).

Key Decisions

  • Monolithic Rails over microservices -- single deployable unit per app. No API gateway complexity. Frontend and backend in one repo, one process.
  • Hotwire over SPA -- server-rendered HTML with progressive enhancement via Turbo Frames/Streams. No JS build pipeline (importmap). Turbo Native wraps the same app for mobile.
  • CNPG shared cluster -- one CloudNativePG operator instance, separate databases per app. Operator manages failover, backups, WAL archiving. CI postgres version pinned to match prod (17).
  • Keycloak opt-in, not mandatory -- apps that need auth use Authorization Code + PKCE (OAuth 2.1 compliant). ROPC/Direct Access Grants rejected as deprecated (RFC 9700). Apps without auth needs skip Keycloak entirely.
  • MinIO opt-in -- only apps with file/image storage use ActiveStorage + MinIO. S3-compatible API provides portability. Dev uses local disk.
  • Hetzner edge for public ingress -- Caddy handles TLS termination on a public IP. Tailscale mesh avoids exposing the home lab directly. Single edge node at 178.156.129.142 serves all custom domains.
  • Commit-SHA image tags -- immutable, traceable. ArgoCD Image Updater automates tag detection and kustomize overlay updates.
  • RSpec + Capybara + Cuprite for testing -- system specs run headless Chromium via CDP. No Selenium dependency.
  • Structured logging via lograge -- JSON output collected by Promtail into Loki. No unstructured Rails default logs in production.
  • project-palinks -- palinks project page
  • project-landscaping-assistant -- landscaping-assistant project page
  • arch-godaddy-tofu -- DNS provider architecture (manages A records for custom domains)
  • convention-kustomize-overlay -- deployment overlay pattern used by pal-e-deployments
  • convention-pipeline-stages -- CI pipeline stage convention
  • convention-frontend-css -- CSS conventions for frontend work