From 56d834ede9dc2eb6476bbbce47f9e814adfa5e2e Mon Sep 17 00:00:00 2001 From: ginnoir Date: Tue, 2 Jun 2026 22:12:07 -0500 Subject: [PATCH] chore: initial homelab stack config and sync tooling Mirror the three production files (docker-compose.yml, .env, Caddyfile) that live on valhalla, plus the push/pull PowerShell scripts, CLAUDE.md, .gitignore/.gitattributes, and .claude/skills for ssh/apply/sync. --- .claude/skills/homelab-apply/SKILL.md | 37 ++ .claude/skills/homelab-ssh/SKILL.md | 51 ++ .claude/skills/homelab-sync/SKILL.md | 38 ++ .env | 99 ++++ .gitattributes | 8 + .gitignore | 12 + CLAUDE.md | 95 ++++ Caddyfile | 225 ++++++++ apply-compose.ps1 | 55 ++ docker-compose.yml | 781 ++++++++++++++++++++++++++ sync-prod.ps1 | 25 + 11 files changed, 1426 insertions(+) create mode 100644 .claude/skills/homelab-apply/SKILL.md create mode 100644 .claude/skills/homelab-ssh/SKILL.md create mode 100644 .claude/skills/homelab-sync/SKILL.md create mode 100644 .env create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Caddyfile create mode 100644 apply-compose.ps1 create mode 100644 docker-compose.yml create mode 100644 sync-prod.ps1 diff --git a/.claude/skills/homelab-apply/SKILL.md b/.claude/skills/homelab-apply/SKILL.md new file mode 100644 index 0000000..a8c57cb --- /dev/null +++ b/.claude/skills/homelab-apply/SKILL.md @@ -0,0 +1,37 @@ +--- +name: homelab-apply +description: Push local config changes (docker-compose.yml, .env, Caddyfile) to the valhalla server and apply them to the live Docker stack. Use after editing homelab config to deploy it. This mutates the running production stack. +--- + +# homelab-apply + +Deploys local repo changes to the live stack on valhalla. **This mutates production** — sync and review your diff first (see the `homelab-sync` skill). + +## Run it + +```powershell +powershell -File apply-compose.ps1 # push .env + Caddyfile + compose, then apply +powershell -File apply-compose.ps1 -EnvFile # .env only +powershell -File apply-compose.ps1 -Caddy # Caddyfile only + hot-reload Caddy +powershell -File apply-compose.ps1 -Compose # docker-compose.yml only + dc pull + dc up -d +``` + +Flags combine (e.g. `-Caddy -Compose`); no flags pushes & applies all three. + +## What each step does + +- **`.env` / compose** → `scp` to `~/htpc-download-box/`, then `dc pull` (latest images) + `dc up -d` (recreates only containers whose resolved config changed). +- **Caddyfile** → `scp` to `/config/caddy/Caddyfile`, then `dc exec caddy caddy reload …` — a hot reload with no downtime. If the new Caddyfile is invalid the reload fails and the old config keeps running; read the output. + +## After applying, verify + +```powershell +ssh ginnoir@valhalla "bash -ic 'dc ps'" +ssh ginnoir@valhalla "bash -ic 'dc logs --tail=50 '" +``` + +## Notes + +- Compose is **v1.27.4** (`docker-compose`, hyphenated); `dc` runs over `bash -ic` to load the alias. +- Editing config without applying does nothing — the server only sees pushed files. +- Cosmetic/whitespace-only compose edits are no-ops to running containers; `dc up -d` won't recreate them. diff --git a/.claude/skills/homelab-ssh/SKILL.md b/.claude/skills/homelab-ssh/SKILL.md new file mode 100644 index 0000000..1142951 --- /dev/null +++ b/.claude/skills/homelab-ssh/SKILL.md @@ -0,0 +1,51 @@ +--- +name: homelab-ssh +description: Connect to and run commands on the valhalla homelab server (ginnoir@valhalla) over SSH, including docker-compose operations through the `dc` alias. Use when inspecting the live stack, tailing logs, restarting a service, reloading Caddy, or checking container/file state on the server. +--- + +# homelab-ssh + +The production homelab runs on a headless Ubuntu host reachable at `ssh ginnoir@valhalla` (key-based auth; resolves and works from this Windows host directly). + +## Connect (non-interactive / safe for automation) + +Always pass `-o BatchMode=yes` so a missing key or password prompt fails fast instead of hanging: + +```powershell +ssh -o BatchMode=yes -o ConnectTimeout=8 ginnoir@valhalla "" +``` + +The first `Bash` command of a session is gated by a GateGuard hook — state the user request + what the command does, then retry. + +## docker-compose via the `dc` alias + +The stack is Docker Compose **v1.27.4** (`docker-compose`, hyphenated — `docker compose` v2 is not installed). On the server, `dc` is aliased in `~/.bashrc` to: + +``` +docker-compose -f ~/htpc-download-box/docker-compose.yml --env-file ~/htpc-download-box/.env +``` + +It's a shell alias, so it only exists in an interactive shell. Invoke it with `bash -ic`: + +```powershell +ssh ginnoir@valhalla "bash -ic 'dc ps'" +ssh ginnoir@valhalla "bash -ic 'dc logs -f --tail=200 '" +ssh ginnoir@valhalla "bash -ic 'dc restart '" +ssh ginnoir@valhalla "bash -ic 'dc up -d'" +ssh ginnoir@valhalla "bash -ic 'dc exec caddy caddy reload --config /etc/caddy/Caddyfile'" +``` + +Use the compose **service name** (`sonarr`, `caddy`, `app`, …). Without `bash -ic`, `dc` is "command not found". + +## Key paths on the server + +- `~/htpc-download-box/docker-compose.yml`, `~/htpc-download-box/.env` — stack definition +- `/config/caddy/Caddyfile` — Caddy config (mounted into the `caddy` container at `/etc/caddy/Caddyfile`) +- `/config//` — per-service persisted config; `/storage1/` — media & data; plus named Docker volumes +- `~/htpc-download-box/` also holds unrelated legacy files (`*.bak`, old `.git`, `README.md`, `Vagrantfile`) — don't modify them + +## Notes + +- Read-only exploration of `/config`, `/storage1`, and container state is fine for answering questions. +- Avoid destructive commands against the live stack without explicit confirmation. +- To push config changes and apply them, use the `homelab-apply` skill; to pull prod config into the repo, use `homelab-sync`. diff --git a/.claude/skills/homelab-sync/SKILL.md b/.claude/skills/homelab-sync/SKILL.md new file mode 100644 index 0000000..692d08c --- /dev/null +++ b/.claude/skills/homelab-sync/SKILL.md @@ -0,0 +1,38 @@ +--- +name: homelab-sync +description: Pull the live production configs (docker-compose.yml, .env, Caddyfile) from the valhalla server into this repo so local matches what's deployed. Use at the start of any session that will edit homelab deployment config, or to check for drift between repo and production. +--- + +# homelab-sync + +Production (valhalla) is the source of truth. Pull before editing so you never edit a stale copy. + +## Run it + +```powershell +powershell -File sync-prod.ps1 +``` + +This `scp`s three files from valhalla, **overwriting** the local copies: + +| From valhalla | Into repo | +|---------------|-----------| +| `~/htpc-download-box/docker-compose.yml` | `docker-compose.yml` | +| `~/htpc-download-box/.env` | `.env` | +| `/config/caddy/Caddyfile` | `Caddyfile` | + +It overwrites local working copies — commit or stash anything you want to keep first. + +## Check drift without overwriting + +To see how local differs from prod before deciding direction (git is installed): + +```powershell +$tmp = Join-Path $env:TEMP 'valhalla-compose.yml' +scp -o BatchMode=yes "ginnoir@valhalla:~/htpc-download-box/docker-compose.yml" $tmp +git --no-pager diff --no-index -- $tmp .\docker-compose.yml +``` + +(`.env` and `Caddyfile` can be compared the same way.) + +After syncing, review `git status` / `git diff` to see what changed on the server since your last sync. To push local changes the other direction, use the `homelab-apply` skill. diff --git a/.env b/.env new file mode 100644 index 0000000..307177e --- /dev/null +++ b/.env @@ -0,0 +1,99 @@ +# ============================================================= +# BASE CONFIG +# ============================================================= +# Timezone: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +TZ=America/Chicago +# UNIX PUID and PGID — find with: id $USER +PUID=1000 +PGID=1000 +# Root path prefix for volume mounts +ROOT=/ + +# ============================================================= +# FOUNDRY VTT +# ============================================================= +FOUNDRY_USERNAME=ginnoir +FOUNDRY_PASSWORD=agt-UAU!uky_ykh3juy +FOUNDRY_ADMIN_KEY=fuckyouben +FOUNDRY_PATCH_URLS=https://gist.githubusercontent.com/ginnoir/898b152a09369f9089770daa71887262/raw/a354e4284f2b1feb616910f65bfe65a4ffdb5473/plutonium-12.x.sh + +# ============================================================= +# OWNCLOUD +# ============================================================= +OWNCLOUD_DOMAIN=files.ginnoir.com +OWNCLOUD_DB_USER=ginnoir +OWNCLOUD_DB_PASSWORD=d0fet0th3x +OWNCLOUD_ADMIN_USER=ginnoir +OWNCLOUD_ADMIN_PASSWORD=d0fet0th3x + +# ============================================================= +# MARIADB +# ============================================================= +MYSQL_ROOT_PASSWORD=d0fet0th3x + +# ============================================================= +# REACTIVE RESUME +# ============================================================= +POSTGRES_PASSWORD=postgres +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +CHROME_TOKEN=chrome_token +# IMPORTANT: Replace this before exposing resume.ginnoir.com publicly. +# Generate with: openssl rand -hex 64 +AUTH_SECRET=CHANGE_ME_USE_openssl_rand_hex_64 + +# ============================================================= +# FRESHRSS +# ============================================================= +FRESHRSS_API_PASSWORD=d0fet0th3x +FRESHRSS_PASSWORD=d0fet0th3x +FRESHRSS_EMAIL=3nigma.matt@gmail.com + +AUTH_SECRET=b4c032c8475cdd21e9138f2178c862f79cb6302ebf90e561cdea4cd6efcfe61e4ee03f5a4b3405c3c99239b734c8f387c6c3547d6c5b8f1c7d7db2b81c01ce0b + +# ── famapp ──────────────────────────────────────────────────────────────────── +FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:latest +FAMAPP_PULL_POLICY=always +AUTHENTIK_IMAGE_TAG=2024.12.3 +RUN_MIGRATIONS=true + +AUTH_URL=https://fam.ginnoir.com +FAMAPP_DB_USER=famapp +FAMAPP_DB_PASSWORD=a9a999d16dd5cdc521bb62eff331265d3c24 +FAMAPP_DB_NAME=famapp + +AUTH_SECRET=CJsICgBs78YxxSdJfS8KU9Zn9k+QytRoVui9kVtuHtI= + +# Fill in after Authentik bootstrap +AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/famapp/ +AUTH_OIDC_CLIENT_ID=6uOZ8yjTR9Z2m1RZU1cLHYXHV2Lv6zZtfuJcrsdq +AUTH_OIDC_CLIENT_SECRET=CPcNM8jXaJ2jHFKxRwGuuKyMKJsT502l3FztO1AOcFqDzWqlWu5x7Ox52Q6zyBa2v33YzoA9igrXW8FFjd1Gp7wrZV4PGTwvXmOd7pIMOY93gNotAgJaewT8WHW1F3YD + +VAPID_PUBLIC_KEY=BNED_UgfPhyoG6_JOabhpQpN3nr6QM5tO0PDiAadMGCcZROn8LM7tnsBrKaB1E5kfuDFb1rC_M86cT3uCvBqSBk +VAPID_PRIVATE_KEY=QyJiyCI2TJa_QWVXznfj4vjpWTBsyFfSCtn1brpE9aI +VAPID_SUBJECT=mailto:3nigma.matt@gmail.com + +NTFY_URL=https://ntfy.ginnoir.com +NTFY_TOPIC=famapp + +AUTHENTIK_DB_USER=authentik +AUTHENTIK_DB_PASSWORD=ebb7d34ecbc4fc8f95cd16ef491d2162cdf8 +AUTHENTIK_DB_NAME=authentik +AUTHENTIK_SECRET_KEY=C33S/oXOVOLrlLnWGMRYadgH9zfEC7Bm1vHtbczkFxHcTOj9j6io397c/OGb + +# MinIO object storage (used for plant/container image uploads) +famapp_MINIO_ENDPOINT=http://minio:9000 +famapp_MINIO_ROOT_USER=famapp +famapp_MINIO_ROOT_PASSWORD=d0fet0th3x +famapp_MINIO_BUCKET=garden + +famapp_OPENPLANTBOOK_CLIENT_ID=5JdQLab69RhVaF4er7b7D29BCrkf3fo3OEiDsOQa +famapp_OPENPLANTBOOK_CLIENT_SECRET=kxPTrdQPAVta7sjVuIRf5EGIJ1fKvlMlqRSJpQtFxRftJhg7oeGnXzwH7ls6tluQLFUYVDYRMwISiAuKZGZLvkuJzUwFGCwhZw7UtW8OvcTGy4717WeNUinvuCDSsNXP + +# ============================================================= +# CLOUDFLARE DNS (for Caddy DNS-01 TLS challenge) +# ============================================================= +# Create a scoped API token at: https://dash.cloudflare.com/profile/api-tokens +# Required permissions: Zone > DNS > Edit (for ginnoir.com zone only) +# This is NOT the Zone ID or Account ID — it's the API Token secret value. +CF_API_TOKEN=cfut_ijIT9HDjXWKgd0dtnyLqxv8L3deB7hSDivSqquwp905a6c51 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d941980 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# These files are deployed to a Linux host via scp (a byte-for-byte copy). +# Force LF so a Windows checkout never introduces CRLF -- stray \r in .env +# values or shell snippets would break on the server. +* text=auto eol=lf +*.ps1 text eol=lf +*.yml text eol=lf +.env text eol=lf +Caddyfile text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3ab51f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# NOTE: .env is intentionally TRACKED in this repo. ginnoir/homelabstack is a +# private repo and the stack's secrets are versioned with it. Do NOT add .env here. + +# OS / editor cruft +.DS_Store +Thumbs.db +*.swp +*~ + +# Local scratch +*.tmp +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b174355 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Deployment configuration for a **live, single-host Docker homelab** running on a headless Ubuntu server reachable at `ssh ginnoir@valhalla`. This repo is **not application code** — it is a version-controlled mirror of three files that live on the server. Editing here changes nothing until the files are pushed to valhalla and the stack is re-applied. + +Tracked files ↔ where they live on valhalla: + +| Repo file | On valhalla | Consumed by | +|-----------|-------------|-------------| +| `docker-compose.yml` | `~/htpc-download-box/docker-compose.yml` | the whole stack | +| `.env` | `~/htpc-download-box/.env` | compose variable substitution | +| `Caddyfile` | `/config/caddy/Caddyfile` | the `caddy` container, mounted at `/etc/caddy/Caddyfile` | + +`.env` is committed **intentionally** — the GitHub repo `ginnoir/homelabstack` is private and the stack's secrets are versioned with it. Do not scrub or gitignore it. + +The server's `~/htpc-download-box/` also holds unrelated legacy files (`*.bak`, a 2020 `.git`, an old 42 KB `README.md`, `Vagrantfile`, `apache/`, `proxy/`): this stack was grafted onto the original *htpc-download-box* project. Leave that cruft alone — only the three files above are managed from here. + +## The core workflow + +Production is the source of truth. **Sync before editing, apply after editing:** + +```powershell +powershell -File sync-prod.ps1 # pull prod -> repo (OVERWRITES local copies) +# ...edit docker-compose.yml / Caddyfile / .env... +powershell -File apply-compose.ps1 # push repo -> prod and apply (mutates the LIVE stack) +``` + +`apply-compose.ps1` flags (combine freely; no flags = all three): + +- `-EnvFile` — push `.env` only +- `-Caddy` — push `Caddyfile`, then hot-reload Caddy (no downtime) +- `-Compose` — push `docker-compose.yml`, then `dc pull` + `dc up -d` + +Caddy changes hot-reload; on `dc up -d` only containers whose resolved config changed are recreated. Cosmetic/whitespace-only compose edits are therefore no-ops to the running containers. + +## Running commands on valhalla + +SSH is key-based and works from this Windows host directly. In automation, pass `-o BatchMode=yes` so it fails fast instead of hanging on a prompt. + +All compose operations go through the **`dc` alias** in the server's `~/.bashrc`: + +``` +dc = docker-compose -f ~/htpc-download-box/docker-compose.yml --env-file ~/htpc-download-box/.env +``` + +It's a shell alias, so it only exists in an **interactive** shell — invoke it over SSH with `bash -ic`: + +```powershell +ssh ginnoir@valhalla "bash -ic 'dc ps'" +ssh ginnoir@valhalla "bash -ic 'dc logs -f --tail=100 sonarr'" +ssh ginnoir@valhalla "bash -ic 'dc restart caddy'" +ssh ginnoir@valhalla "bash -ic 'dc exec caddy caddy reload --config /etc/caddy/Caddyfile'" +``` + +Address services by their **compose service name** (`sonarr`, `caddy`, `app`, …) — this works regardless of how the container is named. + +## docker-compose.yml — read before editing + +- **Compose v1.27.4.** The binary is hyphenated `docker-compose`; `docker compose` (v2) is **not** installed. Project name is `htpc-download-box` (from the directory). +- **Container names:** services with an explicit `container_name:` (most of the media stack, `owncloud_*`, `ntfy`, `freshrss`, `hbbr`/`hbbs`) use that literal name. Services without one get the v1 pattern `htpc-download-box__1` (e.g. `caddy`, `foundry`, `app`, `postgres`, the `authentik-*`/`famapp-*` sets). Prefer `dc ` so you don't have to track which is which. +- **`${ROOT}`** (`.env`, default `/`) prefixes some media-stack volume mounts; other services hardcode `/config` and `/storage1`. Both forms resolve to the same real paths. +- **Networks segment the stack:** `foundry`, `media`, `media_external`, `owncloud`, `resume`, `famapp`, `authentik`. `caddy` is the only container on all of them — a new service that Caddy must proxy has to share a network with `caddy`. +- **Persistence lives on the server, not in this repo:** container state is in `/config/` bind mounts, under `/storage1`, or in named volumes (`postgres_data`, `minio_data`, `authentik_db_data`, `garden_uploads`, …). + +## Caddyfile + +- TLS uses the Cloudflare DNS-01 challenge (`acme_dns cloudflare {env.CF_API_TOKEN}`), so certs need no inbound ports. Most sites are `.ginnoir.com → reverse_proxy :`. +- The `(internal_only)` snippet 403s any client outside `192.168.1.0/24`. `import internal_only` is how admin UIs (sonarr, qbittorrent, …) stay LAN-only while public sites omit it. +- Reverse-proxy upstreams use the compose **service name + container-internal port** (e.g. `qbittorrent:3232`, `stash:6970`), not the host-published port. + +## Adding a service (the common change) + +1. Add the service to `docker-compose.yml`; put it on a network `caddy` is also on if it needs proxying; bind config to `/config/`. +2. Add any secrets/vars to `.env`. +3. Add a site block to `Caddyfile` (`reverse_proxy :`); add `import internal_only` for LAN-only access. +4. Run `apply-compose.ps1` (or `-Compose -Caddy`), then verify with `dc ps` / `dc logs `. + +## Known quirks / gotchas + +- A **GateGuard hook blocks the first use of `Bash`, and every `Write`/`Edit`, until you state the required facts** (the user request + what the operation does/affects). State them, then retry the same call. +- `Caddyfile` proxies `matrix.ginnoir.com → localhost:8008`, but there is **no Matrix/Synapse service in compose** — it's external/legacy. Likewise `dev.ginnoir.com → 192.168.1.74:3000` points at a different LAN host. +- Reactive Resume's `app` reads `${RESUME_AUTH_SECRET}`, which is **not defined in `.env`** (resolves empty). `.env` also defines `AUTH_SECRET` three times — last definition wins (famapp's). +- `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. +- Line endings: a `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially in `.env`) to the Linux host would append stray `\r` to values and break things. + +## Skills + +Project skills in `.claude/skills/` wrap the recurring operations: + +- `homelab-sync` — pull prod configs into the repo +- `homelab-apply` — push config + apply to the live stack +- `homelab-ssh` — run commands / inspect state on valhalla diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..950209f --- /dev/null +++ b/Caddyfile @@ -0,0 +1,225 @@ +# ============================================================= +# GLOBAL OPTIONS +# ============================================================= +{ + acme_dns cloudflare {env.CF_API_TOKEN} +} + +# ============================================================= +# SNIPPETS +# ============================================================= +# Reusable matcher — blocks anything not on the LAN. +# Usage: import internal_only inside any site block. +(internal_only) { + @blocked not remote_ip 192.168.1.0/24 + respond @blocked "Access denied" 403 +} + +# ============================================================= +# FOUNDRY VTT — public +# ============================================================= +foundry.ginnoir.com { + reverse_proxy foundry:30000 +} + +foundry2.ginnoir.com { + reverse_proxy foundry2:30000 +} + +# ============================================================= +# TABLETOP TOOLS — public +# ============================================================= +5etools.ginnoir.com { + reverse_proxy 5etools:80 +} + +pf2e.ginnoir.com { + root * /srv/aon + file_server +} + +# ============================================================= +# FILE STORAGE — public +# ============================================================= +files.ginnoir.com { + reverse_proxy owncloud:8080 +} + +# ============================================================= +# STATIC SITES — public +# ============================================================= +ffttsystems.ginnoir.com { + root * /srv/ffttsystems + file_server +} + +# ============================================================= +# MEDIA REQUESTS — public +# ============================================================= +requests.ginnoir.com { + reverse_proxy overseerr:5055 +} + +# ============================================================= +# MEDIA MANAGEMENT — internal only +# ============================================================= +sonarr.ginnoir.com { + import internal_only + reverse_proxy sonarr:8989 +} + +radarr.ginnoir.com { + import internal_only + reverse_proxy radarr:7878 +} + +bazarr.ginnoir.com { + import internal_only + reverse_proxy bazarr:6767 +} + +jackett.ginnoir.com { + import internal_only + reverse_proxy jackett:9117 +} + +prowlarr.ginnoir.com { + import internal_only + reverse_proxy prowlarr:9696 +} + +tautulli.ginnoir.com { + import internal_only + reverse_proxy tautulli:8181 +} + +hydra.ginnoir.com { + import internal_only + reverse_proxy nzbhydra2:5076 +} + +# ============================================================= +# DOWNLOAD CLIENTS — internal only +# ============================================================= +qbittorrent.ginnoir.com { + import internal_only + reverse_proxy qbittorrent:3232 +} + +deluge.ginnoir.com { + import internal_only + reverse_proxy deluge:8112 +} + +nzbget.ginnoir.com { + import internal_only + reverse_proxy nzbget:6789 +} + +whisparr.ginnoir.com { + import internal_only + reverse_proxy whisparr:6969 +} + +stash.ginnoir.com { + import internal_only + reverse_proxy stash:6970 +} + +# ============================================================= +# NOTIFICATIONS & RSS — public +# ============================================================= +ntfy.ginnoir.com, http://ntfy.ginnoir.com { + reverse_proxy ntfy:80 + @httpget { + protocol http + method GET + path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) + } + redir @httpget https://{host}{uri} +} + +freshrss.ginnoir.com { + reverse_proxy freshrss:80 + @httpget { + protocol http + method GET + path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) + } + redir @httpget https://{host}{uri} +} + +# ============================================================= +# RESUME / PORTFOLIO — public +# ============================================================= +resume.ginnoir.com, https://resume.ginnoir.com { + reverse_proxy app:3000 + @httpget { + protocol http + method GET + path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) + } + redir @httpget https://{host}{uri} +} + +j-costa.com, https://j-costa.com { + tls { + issuer acme { + disable_tlsalpn_challenge + } + } + redir * https://resume.ginnoir.com/ginnoir/resume permanent +} + +storage.j-costa.com, https://storage.j-costa.com { + tls { + issuer acme { + disable_tlsalpn_challenge + } + } + reverse_proxy resume-minio:9000 + @httpget { + protocol http + method GET + path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) + } + redir @httpget https://{host}{uri} +} + +# ============================================================= +# MINIO CONSOLE — internal only +# ============================================================= +minio.ginnoir.com { + import internal_only + reverse_proxy resume-minio:9001 +} + +# ============================================================= +# MATRIX — public +# ============================================================= +ginnoir.com { + header /.well-known/matrix/* Content-Type application/json + header /.well-known/matrix/* Access-Control-Allow-Origin * + respond /.well-known/matrix/server `{"m.server": "matrix.ginnoir.com:443"}` + respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://matrix.ginnoir.com"},"m.identity_server":{"base_url":"https://identity.ginnoir.com"}}` +} + +matrix.ginnoir.com { + reverse_proxy /_matrix/* localhost:8008 + reverse_proxy /_synapse/client/* localhost:8008 +} + +# ============================================================= +# FAMAPP & AUTH — public +# ============================================================= +fam.ginnoir.com { + reverse_proxy famapp:3000 +} + +auth.ginnoir.com { + reverse_proxy authentik-server:9000 +} + +dev.ginnoir.com { + reverse_proxy 192.168.1.74:3000 +} diff --git a/apply-compose.ps1 b/apply-compose.ps1 new file mode 100644 index 0000000..41ab8f2 --- /dev/null +++ b/apply-compose.ps1 @@ -0,0 +1,55 @@ +# Push .env, Caddyfile, and docker-compose.yml to valhalla, then apply them +# to the live Docker stack. +# +# On valhalla, `dc` is aliased (in ~/.bashrc) to: +# docker-compose -f ~/htpc-download-box/docker-compose.yml --env-file ~/htpc-download-box/.env +# It's a shell alias, so we invoke it over SSH with `bash -ic` to load it. +# +# Usage (flags combine; no flags = push & apply all three): +# powershell -File apply-compose.ps1 # .env + Caddyfile + compose +# powershell -File apply-compose.ps1 -EnvFile # .env only +# powershell -File apply-compose.ps1 -Caddy # Caddyfile only + hot-reload Caddy +# powershell -File apply-compose.ps1 -Compose # docker-compose.yml only + pull + up -d + +param( + [switch]$Compose, + [switch]$Caddy, + [switch]$EnvFile +) + +$ErrorActionPreference = "Stop" +# Make a non-zero exit from scp/ssh abort the script instead of silently continuing. +$PSNativeCommandUseErrorActionPreference = $true + +$server = "ginnoir@valhalla" +$composeLocal = Join-Path $PSScriptRoot "docker-compose.yml" +$caddyLocal = Join-Path $PSScriptRoot "Caddyfile" +$envLocal = Join-Path $PSScriptRoot ".env" + +# Default (no flags): push everything. +if (-not $Compose -and -not $Caddy -and -not $EnvFile) { + $EnvFile = $true; $Caddy = $true; $Compose = $true +} + +if ($EnvFile) { + Write-Host "Pushing .env ..." + scp $envLocal "${server}:~/htpc-download-box/.env" +} + +if ($Caddy) { + Write-Host "Pushing Caddyfile ..." + scp $caddyLocal "${server}:/config/caddy/Caddyfile" + Write-Host "Reloading Caddy ..." + ssh $server "bash -ic 'dc exec caddy caddy reload --config /etc/caddy/Caddyfile'" +} + +if ($Compose) { + Write-Host "Pushing docker-compose.yml ..." + scp $composeLocal "${server}:~/htpc-download-box/docker-compose.yml" + Write-Host "Pulling latest images ..." + ssh $server "bash -ic 'dc pull'" + Write-Host "Applying compose changes (recreates only changed containers) ..." + ssh $server "bash -ic 'dc up -d'" +} + +Write-Host "Done." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8802a04 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,781 @@ +services: + # ============================================================ + # FOUNDRY VTT + # ============================================================ + foundry: + image: felddy/foundryvtt:latest + hostname: valhalla-primary + init: true + restart: unless-stopped + networks: + - foundry + volumes: + - /storage1/foundry:/data + - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash + - /storage1/Books/Tabletop:/data/Data/storage + environment: + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} + - CONTAINER_VERBOSE=true + - FOUNDRY_HOSTNAME=foundry.ginnoir.com + - FOUNDRY_PROXY_PORT=443 + - FOUNDRY_PROXY_SSL=true + - CONTAINER_PRESERVE_OWNER=/data/Data/storage + - FOUNDRY_UID=1000 + - FOUNDRY_GID=1000 + - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} + ports: + - "30000:30000" + + foundry2: + image: felddy/foundryvtt:latest + hostname: valhalla-secondary + init: true + restart: unless-stopped + networks: + - foundry + volumes: + - /storage1/foundry2:/data + - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash + - /storage1/Books/Tabletop:/data/Data/storage + environment: + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} + - CONTAINER_VERBOSE=true + - FOUNDRY_HOSTNAME=foundry2.ginnoir.com + - FOUNDRY_PROXY_PORT=443 + - FOUNDRY_PROXY_SSL=true + - CONTAINER_PRESERVE_OWNER=/data/Data/storage + - FOUNDRY_UID=1000 + - FOUNDRY_GID=1000 + - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} + ports: + - "30001:30000" + + # ============================================================ + # CADDY (reverse proxy) + # serfriz/caddy-cloudflare-ddns bundles: + # - caddy-dns/cloudflare (DNS-01 ACME challenge) + # - caddy-cloudflare-ip (trust Cloudflare proxy IPs for real client IPs) + # - caddy-dynamicdns (keeps DNS records current if WAN IP changes) + # ============================================================ + caddy: + image: serfriz/caddy-cloudflare-ddns:latest + restart: unless-stopped + networks: + - foundry + - owncloud + - resume + - media_external + - media + - famapp + - authentik + ports: + - "80:80" + - "443:443" + environment: + - CF_API_TOKEN=${CF_API_TOKEN} + volumes: + - /config/caddy/Caddyfile:/etc/caddy/Caddyfile + - /config/caddy/site:/srv + - /config/caddy/data:/data + - /config/caddy/config:/config + - /storage1/Books:/srv/Books + + # ============================================================ + # MEDIA STACK + # ============================================================ + deluge: + container_name: deluge + image: lscr.io/linuxserver/deluge:latest + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/storage1/torrents:/downloads + - ${ROOT}/config/deluge:/config + ports: + - "8112:8112" + + nzbget: + container_name: nzbget + image: lscr.io/linuxserver/nzbget:latest + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/storage1/complete:/downloads + - ${ROOT}/config/nzbget:/config + ports: + - "6789:6789" + + sonarr: + container_name: sonarr + image: lscr.io/linuxserver/sonarr:latest + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - /etc/localtime:/etc/localtime:ro + - ${ROOT}/config/sonarr:/config + - ${ROOT}/storage1/Media/TV:/tv + - ${ROOT}/storage1/Media/Anime:/anime + - ${ROOT}/storage1/complete:/downloads + ports: + - "8989:8989" + + radarr: + container_name: radarr + image: lscr.io/linuxserver/radarr:latest + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - /etc/localtime:/etc/localtime:ro + - ${ROOT}/config/radarr:/config + - ${ROOT}/storage1/Media/Movies:/movies + - ${ROOT}/storage1/complete:/downloads + ports: + - "7878:7878" + + bazarr: + container_name: bazarr + image: lscr.io/linuxserver/bazarr:latest + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - UMASK_SET=022 + volumes: + - ${ROOT}/config/bazarr:/config + - ${ROOT}/storage1/Media/Movies:/movies + - ${ROOT}/storage1/Media/TV:/tv + - ${ROOT}/storage1/Media/Anime:/anime + ports: + - "6767:6767" + + jackett: + container_name: jackett + image: ghcr.io/hotio/jackett:latest + restart: unless-stopped + networks: + - media + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + volumes: + - /config/jackett:/config + ports: + - "9117:9117" + + prowlarr: + container_name: prowlarr + image: ghcr.io/hotio/prowlarr:latest + restart: unless-stopped + networks: + - media + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + volumes: + - /config/prowlarr:/config + ports: + - "9696:9696" + + overseerr: + container_name: overseerr + image: sctx/overseerr:latest + networks: + - media_external + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + volumes: + - /config/overseerr:/app/config + ports: + - "5055:5055" + + # ============================================================ + # MEDIA UTILITIES + # ============================================================ + tautulli: + image: ghcr.io/linuxserver/tautulli:latest + container_name: tautulli + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/config/tautulli:/config + - ${ROOT}/storage1/PMS:/logs + ports: + - "8181:8181" + + nzbhydra2: + image: ghcr.io/linuxserver/nzbhydra2:latest + container_name: nzbhydra2 + restart: "no" + networks: + - media + environment: + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - ${ROOT}/config/nzbhydra:/config + - ${ROOT}/storage1/complete:/downloads + ports: + - "5076:5076" + + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + restart: unless-stopped + networks: + - media + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - WEBUI_PORT=3232 + - TORRENTING_PORT=6881 + volumes: + - /config/qbittorrent:/config + - /storage1/complete:/downloads + ports: + - "3232:3232" + - "6881:6881" + - "6881:6881/udp" + + whisparr: + container_name: whisparr + image: ghcr.io/hotio/whisparr:latest + restart: unless-stopped + networks: + - media + environment: + - PUID=1000 + - PGID=1000 + - UMASK=002 + - TZ=${TZ} + - WEBUI_PORTS=6969/tcp + volumes: + - /config/whisparr:/config + - /storage1:/data + ports: + - "6969:6969" + + stash: + image: stashapp/stash:latest + container_name: stash + restart: unless-stopped + networks: + - media + logging: + driver: json-file + options: + max-file: "10" + max-size: "2m" + environment: + - PUID=1000 + - PGID=1000 + - STASH_STASH=/data/ + - STASH_GENERATED=/generated/ + - STASH_METADATA=/metadata/ + - STASH_CACHE=/cache/ + - STASH_PORT=6970 + volumes: + - /etc/localtime:/etc/localtime:ro + - /config/stash:/root/.stash + - /storage1/LinuxISOs:/data + - /storage1/stash/metadata:/metadata + - /storage1/stash/cache:/cache + - /storage1/stash/blobs:/blobs + - /storage1/stash/generated:/generated + ports: + - "6971:6970" + + # ============================================================ + # OWNCLOUD STACK + # ============================================================ + owncloud: + image: owncloud/server:latest + container_name: owncloud_server + restart: always + networks: + - owncloud + ports: + - "8080:8080" + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + environment: + - OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN} + - OWNCLOUD_DB_TYPE=mysql + - OWNCLOUD_DB_NAME=owncloud + - OWNCLOUD_DB_USERNAME=${OWNCLOUD_DB_USER} + - OWNCLOUD_DB_PASSWORD=${OWNCLOUD_DB_PASSWORD} + - OWNCLOUD_DB_HOST=mariadb + - OWNCLOUD_ADMIN_USERNAME=${OWNCLOUD_ADMIN_USER} + - OWNCLOUD_ADMIN_PASSWORD=${OWNCLOUD_ADMIN_PASSWORD} + - OWNCLOUD_MYSQL_UTF8MB4=true + - OWNCLOUD_REDIS_ENABLED=true + - OWNCLOUD_REDIS_HOST=redis + healthcheck: + test: ["CMD", "/usr/bin/healthcheck"] + interval: 30s + timeout: 10s + retries: 5 + volumes: + - /config/owncloud:/mnt/data + - /storage1/Books:/mnt/books + - /mnt/dropbox:/mnt/data/files/dropbox + - /storage1/ROMs:/mnt/roms + + mariadb: + image: mariadb:latest + container_name: owncloud_mariadb + restart: always + networks: + - owncloud + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_USER=${OWNCLOUD_DB_USER} + - MYSQL_PASSWORD=${OWNCLOUD_DB_PASSWORD} + - MYSQL_DATABASE=owncloud + command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - /config/mysql:/var/lib/mysql + + redis: + image: redis:latest + container_name: owncloud_redis + restart: always + networks: + - owncloud + command: ["--databases", "1"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - /config/redis:/data + + # ============================================================ + # REACTIVE RESUME STACK + # ============================================================ + postgres: + image: postgres:16-alpine + restart: unless-stopped + networks: + - resume + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + + resume-minio: + image: minio/minio:latest + restart: unless-stopped + networks: + - resume + command: server /data --console-address :9001 + ports: + - "9000:9000" + - "9004:9001" + volumes: + - minio_data:/data + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 30s + + chrome: + image: ghcr.io/browserless/chromium:latest + restart: unless-stopped + networks: + - resume + environment: + - TIMEOUT=10000 + - CONCURRENT=10 + - TOKEN=${CHROME_TOKEN} + - EXIT_ON_HEALTH_FAILURE=true + - PRE_REQUEST_HEALTH_CHECK=true + + app: + image: amruthpillai/reactive-resume:latest + restart: unless-stopped + networks: + - resume + ports: + - "3000:3000" + depends_on: + postgres: + condition: service_healthy + resume-minio: + condition: service_healthy + chrome: + condition: service_started + environment: + - PORT=3000 + - NODE_ENV=production + - APP_URL=https://resume.ginnoir.com + - STORAGE_URL=https://storage.j-costa.com/default + - PRINTER_ENDPOINT=http://chrome:3000 + - PRINTER_TOKEN=${CHROME_TOKEN} + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres + - AUTH_SECRET=${RESUME_AUTH_SECRET} + - MAIL_FROM=noreply@localhost + - STORAGE_ENDPOINT=resume-minio + - STORAGE_PORT=9000 + - STORAGE_REGION=us-east-1 + - STORAGE_BUCKET=default + - STORAGE_ACCESS_KEY=${MINIO_ROOT_USER} + - STORAGE_SECRET_KEY=${MINIO_ROOT_PASSWORD} + - STORAGE_USE_SSL=false + - STORAGE_SKIP_BUCKET_CHECK=false + + # ============================================================ + # NOTIFICATIONS & RSS + # ============================================================ + ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + command: + - serve + restart: unless-stopped + environment: + - TZ=${TZ} + - NTFY_DEFAULT_HOST=https://ntfy.ginnoir.com + - NTFY_BASE_URL=https://ntfy.ginnoir.com + - NTFY_CACHE_FILE=/var/lib/ntfy/cache.db + - NTFY_AUTH_FILE=/var/lib/ntfy/auth.db + - NTFY_AUTH_DEFAULT_ACCESS=read-write + - NTFY_BEHIND_PROXY=true + - NTFY_ATTACHMENT_CACHE_DIR=/var/lib/ntfy/attachments + - NTFY_ENABLE_LOGIN=true + user: 1000:1000 + networks: + - famapp + volumes: + - /config/ntfy/var/cache/ntfy:/var/cache/ntfy + - /config/ntfy/etc/ntfy:/etc/ntfy + - /config/ntfy/var/lib/ntfy:/var/lib/ntfy + ports: + - "8000:80" + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1", + ] + interval: 60s + timeout: 10s + retries: 3 + start_period: 40s + + freshrss: + image: freshrss/freshrss:latest + container_name: freshrss + hostname: freshrss + restart: unless-stopped + networks: + - famapp + logging: + options: + max-size: 10m + volumes: + - /config/freshrss/data:/var/www/FreshRSS/data + - /config/freshrss/extensions:/var/www/FreshRSS/extensions + ports: + - "9999:80" + environment: + - CRON_MIN=3,33 + - TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16 + - FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com + - FRESHRSS_USER=--api-password ${FRESHRSS_API_PASSWORD} --email ${FRESHRSS_EMAIL} --password ${FRESHRSS_PASSWORD} --user ginnoir + + vigilant: + image: ghcr.io/verifiedjoseph/vigilant:latest + restart: unless-stopped + environment: + - VIGILANT_NOTIFICATION_SERVICE=ntfy + - VIGILANT_NOTIFICATION_NTFY_URL=https://ntfy.ginnoir.com/ + - VIGILANT_NOTIFICATION_NTFY_TOPIC=RSS + volumes: + - /config/vigilant/feeds.yaml:/app/feeds.yaml + - cache:/app/cache + security_opt: + - no-new-privileges:true + + # ============================================================ + # REMOTE ACCESS + # ============================================================ + hbbr: + container_name: hbbr + image: rustdesk/rustdesk-server:latest + command: hbbr + restart: unless-stopped + ports: + - "21117:21117" + volumes: + - /config/rustdesk/hbbr/data:/root + + hbbs: + container_name: hbbs + image: rustdesk/rustdesk-server:latest + command: hbbs + restart: unless-stopped + ports: + - "21115:21115" + - "21116:21116" + - "21116:21116/udp" + volumes: + - /config/rustdesk/hbbs/data:/root + depends_on: + - hbbr + + # ============================================================ + # UTILITIES + # ============================================================ + 5etools: + image: ghcr.io/5etools-mirror-3/5etools-src:latest + restart: unless-stopped + networks: + - foundry + ports: + - "9009:80" + + watchtower: + image: containrrr/watchtower:latest + restart: unless-stopped + environment: + - WATCHTOWER_NOTIFICATION_SKIP_TITLE=true + - WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.ginnoir.com/watchtower?title=WatchtowerUpdates + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + # ============================================================ + # FAMAPP + # ============================================================ + famapp: + image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} + pull_policy: ${FAMAPP_PULL_POLICY:-always} + restart: unless-stopped + networks: + - famapp + environment: + NODE_ENV: production + AUTH_URL: ${AUTH_URL} + DATABASE_URL: postgres://${FAMAPP_DB_USER}:${FAMAPP_DB_PASSWORD}@famapp-db:5432/${FAMAPP_DB_NAME} + AUTH_SECRET: ${AUTH_SECRET} + AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER} + AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID} + AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET} + VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY} + VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY} + VAPID_SUBJECT: ${VAPID_SUBJECT} + NTFY_URL: ${NTFY_URL:-} + NTFY_TOPIC: ${NTFY_TOPIC:-} + LOG_LEVEL: ${LOG_LEVEL:-info} + RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true} + MINIO_ENDPOINT: http://famapp-minio:9000 + MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} + MINIO_BUCKET: ${famapp_MINIO_BUCKET} + OPENPLANTBOOK_CLIENT_ID: ${famapp_OPENPLANTBOOK_CLIENT_ID} + OPENPLANTBOOK_CLIENT_SECRET: ${famapp_OPENPLANTBOOK_CLIENT_SECRET} + ports: + - "3010:3000" + depends_on: + famapp-db: + condition: service_healthy + famapp-minio: + condition: service_healthy + + famapp-db: + image: postgres:16-alpine + restart: unless-stopped + networks: + - famapp + environment: + POSTGRES_USER: ${FAMAPP_DB_USER} + POSTGRES_PASSWORD: ${FAMAPP_DB_PASSWORD} + POSTGRES_DB: ${FAMAPP_DB_NAME} + volumes: + - famapp_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${FAMAPP_DB_USER} -d ${FAMAPP_DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + + famapp-minio: + image: minio/minio:latest + command: server /data --console-address ":9003" + restart: unless-stopped + environment: + MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} + volumes: + - garden_uploads:/data + ports: + - "9002:9000" + - "9003:9003" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 30s + networks: + - famapp + + # ============================================================ + # AUTHENTIK + # ============================================================ + authentik-server: + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: server + networks: + - authentik + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + ports: + - "9200:9000" + depends_on: + authentik-db: + condition: service_healthy + authentik-redis: + condition: service_healthy + + authentik-worker: + image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} + restart: unless-stopped + command: worker + networks: + - authentik + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-db + AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + depends_on: + authentik-db: + condition: service_healthy + authentik-redis: + condition: service_healthy + + authentik-db: + image: postgres:16-alpine + restart: unless-stopped + networks: + - authentik + environment: + POSTGRES_USER: ${AUTHENTIK_DB_USER:-authentik} + POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD} + POSTGRES_DB: ${AUTHENTIK_DB_NAME:-authentik} + volumes: + - authentik_db_data:/var/lib/postgresql/data + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${AUTHENTIK_DB_USER:-authentik} -d ${AUTHENTIK_DB_NAME:-authentik}", + ] + interval: 10s + timeout: 5s + retries: 5 + + authentik-redis: + image: redis:7-alpine + restart: unless-stopped + networks: + - authentik + command: --save 60 1 --loglevel warning + volumes: + - authentik_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + foundry: + owncloud: + resume: + media_external: + media: + famapp: + authentik: + +volumes: + minio_data: + postgres_data: + cache: + famapp_db_data: + authentik_db_data: + authentik_redis_data: + garden_uploads: diff --git a/sync-prod.ps1 b/sync-prod.ps1 new file mode 100644 index 0000000..f725c46 --- /dev/null +++ b/sync-prod.ps1 @@ -0,0 +1,25 @@ +# Pull the live production configs from valhalla into this repo, so local +# matches what's actually deployed. Run at the start of any session where +# you'll edit deployment config. +# +# Production is the source of truth. This OVERWRITES the local copies of +# docker-compose.yml, .env, and Caddyfile -- commit or stash first. +# +# Usage: powershell -File sync-prod.ps1 + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true + +$server = "ginnoir@valhalla" +$repoDir = $PSScriptRoot + +Write-Host "Syncing production configs from $server ..." + +scp "${server}:~/htpc-download-box/docker-compose.yml" "$repoDir\docker-compose.yml" +scp "${server}:~/htpc-download-box/.env" "$repoDir\.env" +scp "${server}:/config/caddy/Caddyfile" "$repoDir\Caddyfile" + +Write-Host "Done. Files synced:" +Get-ChildItem $repoDir -File | + Where-Object { $_.Name -in 'docker-compose.yml', '.env', 'Caddyfile' } | + Select-Object Name, Length, LastWriteTime