# Production Infrastructure > **Maintenance rule**: Update this file when infrastructure changes (hosting, databases, > backups, monitoring, deployment pipeline, external services). Keep it factual and concise. **CRITICAL: under no circumstances you're allowed to interact directly with production infrastructure without asking the user first.** ## Overview Self-hosted Phoenix application running as a Docker container orchestrated by [Coolify](https://coolify.io). SQLite for all persistence, Litestream for continuous backup to S3-compatible object storage. Deployed automatically on every `main` branch push via GitHub Actions. **Domain**: `music-library.claudio-ortolina.org` --- ## Hosting | Component | Technology | |-----------|-----------| | Orchestration | Coolify (self-hosted) | | Container runtime | Docker | | SSL termination | Coolify reverse proxy | | HTTP redirect | HTTP 301 → HTTPS (enforced by app config) | The Docker image is a multi-stage build: 1. **Builder** — `hexpm/elixir:1.20.0-rc.4-erlang-28.5-debian-trixie-20260421-slim` with Node.js 25, compiles deps, builds assets (`mix assets.deploy`), generates an OTP release. 2. **Runner** — `debian:trixie-20260421-slim` with minimal runtime deps (`libstdc++6`, `openssl`, `libncurses6`, `ca-certificates`). Runs as unprivileged `nobody` user. Fluxon UI (licensed dependency) is fetched during build via Docker build secrets (`FLUXON_LICENSE_KEY`, `FLUXON_KEY_FINGERPRINT`). --- ## Databases Three separate SQLite databases, each managed by its own Ecto repo: | Repo | Purpose | Cache size | Pool size | |------|---------|------------|-----------| | `MusicLibrary.Repo` | Application data | 128 MB | `$POOL_SIZE` (default 5) | | `MusicLibrary.BackgroundRepo` | Oban job queue | 16 MB | `$POOL_SIZE` (default 5) | | `MusicLibrary.TelemetryRepo` | Persistent telemetry metrics | 4 MB | 2 | All databases use incremental auto-vacuum. Paths are configured via environment variables (`DATABASE_PATH`, `BACKGROUND_DATABASE_PATH`, `TELEMETRY_DATABASE_PATH`). --- ## Backups ### Litestream (continuous) Configured inline in `compose.yaml`. Runs as a separate Docker Compose service (`litestream/litestream:0.5.11-scratch`) sharing the database volume. | Setting | Value | |---------|-------| | S3 endpoint | `https://nbg1.your-objectstorage.com` | | Bucket | `ffmusiclibrary` | | Sync interval | 60 minutes | | Retention | 672 hours (28 days) | | Healthcheck | `litestream databases` every 30s (timeout 10s, 3 retries) | Credentials via environment: `LITESTREAM_ACCESS_KEY_ID`, `LITESTREAM_SECRET_ACCESS_KEY`. ### Manual backup Two manual options are available: - `mise run prod:backup` — takes an atomic snapshot of the live database on the production server using `sqlite3 .backup`, then rsyncs it locally as `data/music_library_prod_.db`. Safe under concurrent writes. - `mise run prod:litestream-backup` — restores the production database locally from the S3 Litestream replica. Requires `LITESTREAM_ACCESS_KEY_ID` and `LITESTREAM_SECRET_ACCESS_KEY`. Old backups can be cleaned with `mise run prod:prune-backups`. --- ## Deployment Pipeline ### CI/CD (GitHub Actions) Workflow: `.github/workflows/test_and_deploy.yml` Triggers: push to `main`/tags, pull requests, manual `workflow_dispatch`. Concurrency control cancels in-progress runs for the same ref. Uses `mise` (via `jdx/mise-action@v4`) for tool version management. ``` Push to main (or PR / manual dispatch) ├── Lint (format, gettext, credo, sobelow, mix_audit, shellcheck, docker image, asset build) ├── Test (mix test with partitioning, coverage ≥75%) └── Deploy (requires GitHub environment approval, main branch only) ├── Trigger deployment via Coolify API (hurl) ├── Wait for container health check └── Run post-deploy verification (test/prod.hurl) ``` A manual verification workflow (`.github/workflows/verify.yml`) can be triggered via `workflow_dispatch` to re-run production checks without deploying. ### Dependency management (Dependabot) Automated dependency updates via GitHub Dependabot (`.github/dependabot.yml`): - **Docker (Dockerfile)**: daily checks for builder/runner base image updates, max 5 open PRs - **Docker Compose**: daily checks for service image updates - **Elixir (mix)**: daily checks, max 10 open PRs - **NPM**: daily checks, max 10 open PRs, ignores path-based local deps - **GitHub Actions**: daily checks for action version updates Fluxon (private dependency) is configured with a dedicated registry entry. ### Post-deploy verification `test/prod.hurl` checks: - HTTP → HTTPS redirect - API authentication enforcement - Valid API responses with auth token - Dev routes require login - Bot scanner paths (`wp-admin`, `.env`, `xmlrpc`) return 404 ### Deployment credentials | Secret/Variable | Purpose | |-----------------|---------| | `COOLIFY_TOKEN` | API authentication (GitHub secret) | | `COOLIFY_HOST` | Coolify server address (GitHub variable) | | `COOLIFY_APP_UUID` | Application identifier (GitHub variable) | --- ## Environment Variables ### Required | Variable | Purpose | |----------|---------| | `DATABASE_PATH` | Absolute path to main SQLite database | | `BACKGROUND_DATABASE_PATH` | Absolute path to background jobs database | | `TELEMETRY_DATABASE_PATH` | Absolute path to telemetry database | | `SECRET_KEY_BASE` | Phoenix secret (`mix phx.gen.secret`) | | `CLOAK_ENCRYPTION_KEY` | Base64-encoded 32-byte AES key for encrypted secrets | | `LOGIN_PASSWORD` | Web login password | | `API_TOKEN` | Bearer token for API endpoints | | `MAILGUN_API_KEY` | Mailgun API key | ### Optional | Variable | Default | Purpose | |----------|---------|---------| | `SERVICE_FQDN_WEB` | `example.com` | Application domain | | `PORT` | `4000` | HTTP listen port | | `POOL_SIZE` | `5` | Database connection pool size | | `MAILGUN_DOMAIN` | `mailgun.fullyforged.com` | Mailgun sending domain | | `DEFAULT_TIMEZONE` | `Europe/London` | Application timezone | | `OPENAI_KEY` | — | OpenAI API (embeddings, chat) | | `DISCOGS_PERSONAL_ACCESS_TOKEN` | — | Discogs API | | `BRAVE_SEARCH_API_KEY` | — | Brave Search API | | `LAST_FM_API_KEY` | — | Last.fm API | | `LAST_FM_SHARED_SECRET` | — | Last.fm scrobbling auth | | `LAST_FM_USER` | — | Last.fm username | --- ## OTP Release Standard Mix release. Entry point: `rel/overlays/bin/server` (sets `PHX_SERVER=true`). Migrations are skipped during release boot (`skip_migrations?/0` returns `true` when `RELEASE_NAME` is set). Instead, Coolify is configured to run migrations after the Docker image is built and the container is started — this happens via the `rel/overlays/bin/migrate` script, executed as a post-deployment command in Coolify before the application begins serving traffic. A standalone `rel/overlays/bin/migrate` script is also available for manual use. ERL_FLAGS: `+JPperf true` (JIT performance monitoring). --- ## Monitoring & Observability ### Health check `GET /health` — queries the main database, returns 200 or 500. Used by Docker health checks and post-deploy verification. ### Error tracking `ErrorTracker` with email notifications via Mailgun: - Listens to `:error_tracker` telemetry events - Throttles repeated error notifications - Non-actionable errors (bot scanners, `NoRouteError`) filtered via `ErrorIgnorer` - Muted errors skip email notifications ### Telemetry SQLite-backed persistent metrics (`MusicLibraryWeb.Telemetry.Storage`) with 30-second polling interval. Events are buffered in GenServer state keyed by metric and flushed to SQLite every 5 seconds inside a single transaction; reads via `metrics_history/1` force-flush only the requested metric so the dashboard sees fresh data without waiting for the next tick. Per-metric retention is capped at 32 768 rows (`:retention_limit`), pruned after each flush. Flush failures are logged at `:warning` and the offending batch is dropped. Tracks: - Database query times (total, query, queue) - External API request latency (Finch) - Rate limiter throttle durations - Asset cache hit/miss - Oban job metrics (enqueue, execute, attempt, discard) - Scrobble rules processing (duration, exceptions) - Error tracker counters ### Dashboards (behind auth) - Phoenix LiveDashboard (`/dev/dashboard`) - Oban Web (`/dev/oban`) - ErrorTracker (`/dev/errors`) ### Pi coding agent tools Pi extensions provide additional tools for production observability without manual SSH or browser access. Each extension reads its own environment variables from the pi runtime environment (not server-side config). | Extension | Tools | Env vars | |-----------|-------|----------| | `prod-logs` | `fetch_production_logs` | `PI_COOLIFY_HOST`, `PI_COOLIFY_APP_UUID`, `PI_COOLIFY_TOKEN` | | `prod-errors` | `fetch_production_errors`, `fetch_production_error` | `PI_API_TOKEN`, `PI_SERVICE_FQDN_WEB` | **`prod-logs` env vars:** - `PI_COOLIFY_HOST` — Coolify server base URL (e.g., `https://coolify.example.com`) - `PI_COOLIFY_APP_UUID` — Application UUID in Coolify - `PI_COOLIFY_TOKEN` — Coolify API Bearer token **`prod-errors` env vars:** - `PI_API_TOKEN` — Must match the `API_TOKEN` env var on the production server (used for Bearer auth on `/api/v1/*`) - `PI_SERVICE_FQDN_WEB` — Production domain with protocol (e.g., `https://musiclibrary.claudio-ortolina.org`, no trailing slash) --- ## Email Mailgun via Swoosh (`Swoosh.Adapters.Mailgun`). | Setting | Value | |---------|-------| | From | `postmaster@mailgun.fullyforged.com` | | To | `claudio@fullyforged.com` | Used for error notifications and the daily "records on this day" digest email. --- ## Encryption Cloak vault (`MusicLibrary.Vault`) with AES.GCM cipher for at-rest encryption of secrets stored in the `secrets` table. Key configured via `CLOAK_ENCRYPTION_KEY` (base64-encoded 32-byte key). --- ## Tool Versions Defined in `mise.toml`.