3218320b1e
Daily digest now runs as the n8n "Umami daily report" workflow (Schedule -> Code -> Send Email) instead of the host cron. Code node logic in scripts/n8n-report-core.js; reads stats via a dedicated umami 'reporter' admin user (login API). Gmail SMTP on 587/STARTTLS (465 blocked). Host cron retired (disabled, script kept as fallback). Inventory updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
458 lines
27 KiB
Markdown
458 lines
27 KiB
Markdown
# Dokploy inventory
|
|
|
|
*Last verified: 2026-06-04*
|
|
|
|
What's deployed on the Dokploy instance, with API IDs for scripting.
|
|
|
|
## Projects
|
|
|
|
### httpbin
|
|
|
|
> A simple HTTP request & response service.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `pr7LbZckEamcTDxZTDVd_` |
|
|
| Environment | `production` (`9WcQGl8yxLSq7OAYtTQ6I`) |
|
|
|
|
#### Application: httpbin
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://httpbin.kennethreitz.org |
|
|
| Application ID | `q_TboJiWsBtn6PRD3BfIO` |
|
|
| App name (Swarm service) | `httpbin-lmxabw` |
|
|
| Source | Docker image `kennethreitz/httpbin:latest` (Docker Hub) |
|
|
| Container port | 80 |
|
|
| Domain | `httpbin.kennethreitz.org` → A → `5.161.122.181` (DNSimple) |
|
|
| TLS | Let's Encrypt via Traefik (`certificateType: letsencrypt`) |
|
|
| Deployed | 2026-06-04 |
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"q_TboJiWsBtn6PRD3BfIO"}'`
|
|
|
|
### poemsbysarah
|
|
|
|
> Poems by Sarah — static site (sarah-poems repo)
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `6DVGWaAc221gBD0fww1vY` |
|
|
| Environment | `production` (`2LmxnWq6E4QGzaipaHRza`) |
|
|
|
|
#### Application: poemsbysarah
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://poemsbysarah.com (+ www, poemsbysarah.kennethreitz.org) |
|
|
| Application ID | `-JVyFxpxGQHsIJUBsNAT3` |
|
|
| App name (Swarm service) | `poemsbysarah-jc5l83` |
|
|
| Source | Custom git `git@github.com:kennethreitz/sarah-poems.git`, branch `main` |
|
|
| Auth | Dokploy SSH key `github-sarah-poems` (`PO4Ik1oy95XZzt_NjnrEd`), added as read-only GitHub deploy key "dokploy@mercury" |
|
|
| Build | Dockerfile (multi-stage: Python/uv builds static site → nginx:alpine serves) |
|
|
| Container port | 8080 |
|
|
| Domains | `poemsbysarah.com` (`qBT5H0GsBSLhLEqhN6nip`), `www.poemsbysarah.com` (`ic5BF71JRiV1FuhjslO6L`), `poemsbysarah.kennethreitz.org` (`Vgud4uu3054EE2Ru5dIIm`) |
|
|
| TLS | Let's Encrypt via Traefik |
|
|
| Auto-deploy | GitHub webhook (id `636431970`) on push → `$DOKPLOY_URL/api/deploy/<refreshToken>`; replaced the repo's old Fly Deploy action |
|
|
| Deployed | 2026-06-05 (migrated from Fly.io app `poemsbysarah`) |
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"-JVyFxpxGQHsIJUBsNAT3"}'`
|
|
|
|
### kjvstudy
|
|
|
|
> KJV Study — kjvstudy.org Bible study site
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `A649aIoEmJOdR00t3Pjbv` |
|
|
| Environment | `production` (`aaP2KaUXwe_YNP1L-P7Nj`) |
|
|
|
|
#### Application: kjvstudy
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://kjvstudy.org (+ www, kjvstudy.kennethreitz.org) |
|
|
| Application ID | `8Grj0KrNvoEQTubzgERAY` |
|
|
| App name (Swarm service) | `kjvstudy-bt2mgm` |
|
|
| Source | Custom git `git@github.com:kennethreitz/kjvstudy.org.git`, branch `main` |
|
|
| Auth | Dokploy SSH key `github-kjvstudy` (`p4OF0zEphdM124OgCjnNW`), read-only GitHub deploy key "dokploy@mercury" |
|
|
| Build | Dockerfile (multi-stage: uv deps → python:3.13-slim + granian ASGI, search index baked at build) |
|
|
| Container port | 8000 (`/health` endpoint) |
|
|
| Env | `PRELOAD_INTERLINEAR=false`, `WORKERS=2`, `RUNTIME_THREADS=2` |
|
|
| Domains | `kjvstudy.org` (`n5d7dr6glGHt0GZUcHY4Z`), `www.kjvstudy.org` (`_9BBfXI5RKJm6vCh7Srkd`), `kjvstudy.kennethreitz.org` (`Uz0GYQOZakCpX1080MxMA`) |
|
|
| TLS | Let's Encrypt via Traefik |
|
|
| Auto-deploy | Test-gated: GitHub Actions `deploy.yml` (push to main → pytest → curl `DOKPLOY_DEPLOY_URL` secret). No plain webhook — repo's deploys were test-gated on Fly, behavior preserved. ⚠️ The deploy endpoint silently no-ops (still 200) without a push payload: must send `x-github-event: push` header + `{"ref":"refs/heads/main"}` body. |
|
|
| Deployed | 2026-06-05 (migrated from Fly.io app `kjvstudy`) |
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"8Grj0KrNvoEQTubzgERAY"}'`
|
|
|
|
### kennethreitz.org
|
|
|
|
> Personal site — tuftecms/granian
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `Uy9ecVxT_JCLUhpIaJei6` |
|
|
| Environment | `production` (`vyHsgusrcY2YiP-b2SeMv`) |
|
|
|
|
#### Application: kennethreitz.org
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://kennethreitz.org |
|
|
| Application ID | `WOM59R7Q99Bk6Uhu1blJ0` |
|
|
| App name (Swarm service) | `kennethreitz-org` |
|
|
| Source | Custom git `git@github.com:kennethreitz/kennethreitz.org.git`, branch `main` |
|
|
| Auth | Dokploy SSH key `github-kennethreitz-org` (`1UnKE3iZu797vpUYaA5Gb`), read-only GitHub deploy key "dokploy@mercury" |
|
|
| Build | Dockerfile (astral/uv python3.14 + WeasyPrint deps, granian ASGI) |
|
|
| Container port | 8000 (`/health` endpoint) |
|
|
| Domains | `kennethreitz.org` (`OwJn1ashpFz7kUHFNNcvj`). `www` is a Squarespace CNAME — intentionally not on Dokploy. |
|
|
| TLS | Let's Encrypt via Traefik |
|
|
| Auto-deploy | GitHub webhook (id `636436048`) on push (old Fly workflow had no test gate) |
|
|
| Deployed | 2026-06-05 (migrated from Fly.io app `kr-website-icy-sea-7069`) |
|
|
|
|
Note: the Fly app had `AWS_*`/`BUCKET_NAME` (Tigris) secrets, but nothing in the
|
|
code references them — treated as leftovers and not carried over.
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"WOM59R7Q99Bk6Uhu1blJ0"}'`
|
|
|
|
### interpretations
|
|
|
|
> Interpretations — album site (nginx static)
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `xba7jeE0wgN1T8vwWfjVb` |
|
|
| Environment | `production` (`afyewWiiRd5wtJqe9EqVN`) |
|
|
|
|
#### Application: interpretations
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://interpretations.kennethreitz.org |
|
|
| Application ID | `arptPVNJZU0SIsk7z8G-E` |
|
|
| App name (Swarm service) | `interpretations` (suffix assigned by Dokploy) |
|
|
| Source | Custom git `git@github.com:kennethreitz/interpretations.git`, branch `main` |
|
|
| Auth | Dokploy SSH key `github-interpretations` (`TlDSsKKVz7O0Gpk_jYFVB`), read-only GitHub deploy key "dokploy@mercury" |
|
|
| Build | Dockerfile (nginx:alpine serving `site/`, zips full album at build) |
|
|
| Container port | 80 |
|
|
| Domains | `interpretations.kennethreitz.org` (`URm1R9zAWG4mugI77XBVi`) — DNS currently CNAME to Fly, needs flip to `5.161.122.181` |
|
|
| TLS | Let's Encrypt via Traefik |
|
|
| Auto-deploy | GitHub webhook (id `636437610`) on push (no tests in repo) |
|
|
| Deployed | 2026-06-05 (migrated from Fly.io app `interpretations`) |
|
|
|
|
Note: repo is heavy (~1.4 GB .git, mp3s tracked directly) — clones/builds are slow
|
|
but the site rarely changes.
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"arptPVNJZU0SIsk7z8G-E"}'`
|
|
|
|
### photos
|
|
|
|
> photos.kennethreitz.org (ExifTree) — Django photo portfolio
|
|
|
|
| | |
|
|
|---|---|
|
|
| Project ID | `ZTrEB4hHFibiOjqGUaEvF` |
|
|
| Environment | `production` (`p0XGhTIy3NvO3Flwr7bVU`) |
|
|
|
|
#### Compose: photos (web + worker)
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://photos.kennethreitz.org |
|
|
| Compose ID | `WICLbVwy5JEbHz2SPb4tR` (appName `exiftree-1fms6x`) |
|
|
| Source | Custom git `git@github.com:kennethreitz/photos.kennethreitz.org.git`, branch `main`, `compose.prod.yml` |
|
|
| Auth | Dokploy SSH key `github-photos` (`a1BX7DYoCglQFXAwTEmy0`), read-only deploy key |
|
|
| Services | `web` (runbolt :8000, Traefik routes here) + `worker` (Celery, concurrency 2) |
|
|
| Domain | `photos.kennethreitz.org` (`x9rE8KS2nDGXlQjpUorfm`, serviceName `web`) — covered by wildcard `*.kennethreitz.org` A record |
|
|
| Env | DEBUG/SECRET_KEY/ALLOWED_HOSTS/DATABASE_URL/CELERY_*/AWS_* in Dokploy compose env. Celery brokers through Postgres via `sqla+postgresql+psycopg://` (explicit env override — repo only ships psycopg v3, default `sqla+postgresql://` wants psycopg2). `OPENAI_API_KEY` empty for now (AI describe disabled). |
|
|
| Storage | MinIO bucket `exiftree-media` (self-hosted, see minio compose below) — public URLs via `img.kennethreitz.org/exiftree-media`, path-style. Migrated from Tigris 2026-06-05: 166 GiB / 100,632 objects, `mc diff` verified clean. Tigris bucket retained as cold fallback. |
|
|
| Auto-deploy | GitHub webhook (id `636448741`) → `/api/deploy/compose/<refreshToken>` |
|
|
| Deployed | 2026-06-05 (migrated from Fly.io app `exiftree`; old `deploy.yml` + `daily-restart.yml` workflows removed) |
|
|
|
|
#### Database: exiftree-db
|
|
|
|
| | |
|
|
|---|---|
|
|
| Postgres ID | `BgAzfQ5lAXvd6jORAz2bs` (appName/host `exiftree-db-fsgd0w`) |
|
|
| Image | `postgres:17` (matches the Fly source version — pg16 cannot read pg17 dumps) |
|
|
| DB/user | `exiftree` / `exiftree` (password in Dokploy) |
|
|
| Data | Restored 2026-06-05 from Fly Postgres `exiftree-db` dump — 20,301 images, 168 MB. The Fly DB app still exists as a fallback snapshot. |
|
|
|
|
Redeploy: `scripts/api.sh compose.deploy '{"composeId":"WICLbVwy5JEbHz2SPb4tR"}'`
|
|
|
|
#### Compose: minio (photos media store)
|
|
|
|
> MinIO for photo media. Lives in the photos project. A *separate* MinIO in the
|
|
> infra project handles backups — see below.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `UK8pWczw8d9GSmyLjZJiP` (appName `minio-q3xgqx`, sourceType raw, env `production` of photos) |
|
|
| S3 API | https://img.kennethreitz.org (container port 9000) — ⚠️ `media.` was first choice but it's an existing CNAME to the Synology NAS; check the zone before naming! |
|
|
| Console | https://media-minio.kennethreitz.org (container port 9001) |
|
|
| Data dir | `/mnt/objects/minio` — bind mount on the 250 GB `mercury-objects` Hetzner volume |
|
|
| Root user | `exiftree-admin` (password in Dokploy compose env) |
|
|
| Buckets | `exiftree-media` (anonymous download enabled) |
|
|
| Service account | access key in Dokploy (photos compose env) |
|
|
|
|
⚠️ Gotcha learned the hard way: Dokploy compose domains are applied as **container
|
|
labels at deploy time** — `domain.create`/`delete` alone changes nothing until the
|
|
compose is redeployed. Stale labels keep routing the old hostname.
|
|
|
|
### infra
|
|
|
|
> Infrastructure services (own project)
|
|
|
|
#### Compose: minio-backups
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `o5LlW9tAugh9K3nf5CTh5` (appName `minio-backups-o5fjyf`, raw) |
|
|
| S3 API | https://s3.kennethreitz.org (port 9000) |
|
|
| Console | https://minio.kennethreitz.org (port 9001) |
|
|
| Data dir | `/mnt/objects/minio-infra` (on the volume) |
|
|
| Root user | `infra-admin` (password in Dokploy compose env) |
|
|
| Buckets | `dokploy-backups` (private) |
|
|
| Service account | access key in the Dokploy destination config |
|
|
|
|
#### Compose: umami
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `hDl_SBuWUPhYesgIZYYRU` (appName `umami`, raw) |
|
|
| URL | https://analytics.kennethreitz.org (container port 3000) |
|
|
| Stack | umami (postgresql flavor) + dedicated postgres:17 (named volume `umami-db-data`) |
|
|
| Login | default `admin`/`umami` — **change on first login** |
|
|
| Notes | Nine sites registered + instrumented (incl. rhymepad, pytheory-playground 2026-06-12, and `pytheory-docs` = pytheory.org 2026-06-16 → website `abaa0013-d524-4583-b70e-6d0ff5b7360c`, snippet in the Sphinx `kennethreitz/pytheory` `docs/_templates/layout.html`, replacing a dead Gauges tracker). Public share dashboards enabled per site, all 9 linked from Glance's Analytics bookmark group. **Sharing is stored in the `share` table** (not a `website.share_id` column in this version): one row per site with `entity_id`=website_id, `share_type`=1, `parameters`='{"events": true, "overview": true}', and a random `slug` that is the public URL id (`/share/<slug>/<domain>`). New sites can be registered with a SQL insert into the `website` table, and shared with an insert into `share` (umami-db container `umami-avvc5k-umami-db-1`, user_id `41e2b680-648e-4b09-bcd7-3e2b10c06264`) — see pytheory-playground/pytheory-docs entries. |
|
|
| Daily email report | **n8n workflow** "Umami daily report" (id `zotWITv1CWUiIOEP`): Schedule (`15 12 * * *`, daily 12:15 UTC / ~08:15 ET) → Code node → Send Email. The Code node logic lives in `scripts/n8n-report-core.js` (login → fetch yesterday + prior day stats + top pages for all 9 sites by hard-coded website-id → styled HTML, day boundaries America/New_York). Emails a digest (totals + per-site pageviews/visitors/visits with day-over-day deltas + top 10 pages) to thepythonist@gmail.com. **Auth:** a dedicated umami user `reporter` (role `admin` — `view-only` sees no sites in v3) logs in via `/api/auth/login`; admin can read any website's stats by id even without owning it. **SMTP:** Gmail via n8n credential "Gmail SMTP 587 (reports)" — **port 587/STARTTLS, NOT 465** (465 is blocked outbound on this host). Gotcha: updating a workflow's schedule via the public API doesn't reload the live trigger — must POST `/deactivate` then `/activate` to re-register. Top pages need umami metrics `type=path` (not `url`). The earlier cron version (`scripts/umami-daily-report.py`, `/usr/local/sbin/`, queried Postgres directly) is **retired** — cron moved to `/root/umami-daily-report.cron.disabled`, script kept as a fallback. |
|
|
|
|
#### Compose: glance
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `CKS3H3zDpAB71FF0A-1cR` (appName `glance`, raw) |
|
|
| URL | https://home.kennethreitz.org (container port 8080) |
|
|
| Config | `/var/lib/glance/glance.yml` on the host. **Home**: clock, weather (Winchester VA, imperial), server-stats, Services monitor (15 sites — each links to its homepage via `url`, health-probed separately via `check-url`; minio links to the console but checks the S3 health endpoint), essays RSS, bookmarks (Admin + Analytics groups; Analytics has 9 Umami share-dashboard links), Hacker News. **Server**: docker-containers (ro socket mount), **"Dokploy Apps"** custom-api widget (hits `project.all`, renders 8 apps + 11 compose stacks with status dots), gitea activity RSS, stack release tracker. **Auth enabled** (user `kenneth`; auth secret-key must be exactly 64 bytes base64). Edit + restart container to change. Timestamped config backups on the host: `glance.yml.bak*-20260616`. |
|
|
| Secret in config | The "Dokploy Apps" widget stores the **`DOKPLOY_API_KEY` in plaintext** in `glance.yml` (`headers.x-api-key`). Host-only, **not** in git, but it is a full-access Dokploy API key. Harden later by moving it to a glance-container env var (`${VAR}`, needs a Dokploy redeploy) or a scoped/read-only key. |
|
|
|
|
#### Compose: icloud-drive (filebrowser)
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `-fIZL8clnGBaFjAJY84j9` (appName `icloud-drive`, raw) |
|
|
| URL | https://drive.kennethreitz.org (filebrowser, container port 80) |
|
|
| Login | `kenneth` (filebrowser requires ≥12-char passwords) |
|
|
| Data | `/mnt/objects/icloud-drive` (synced mirror under `current/`, deletions kept in dated `archive/`), filebrowser DB at `/mnt/objects/filebrowser` |
|
|
| Sync | systemd `icloud-sync.timer` hourly → `rclone sync icloud: …` with `--exclude "*.partial" --exclude ".*.icloud"` (those are iCloud's in-flight upload placeholders; without excluding them rclone exits non-zero and the unit falsely reports failure). Initial seed 290 GB done 2026-06-05. Log: `/var/log/icloud-sync.log`. ⚠️ Apple 2FA trust token expires ~monthly — `rclone config reconnect icloud:` on the server to refresh. |
|
|
|
|
#### Compose: gitea
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `PV7bUFe4wV-2G1WD8H57e` (appName `gitea-qdogok`, raw) |
|
|
| URL | https://git.kennethreitz.org (container port 3000) |
|
|
| Git SSH | `git.kennethreitz.org:2222` (host port 2222 → container 22) |
|
|
| Data dir | `/var/lib/gitea` (root disk; sqlite DB) |
|
|
| Admin | `kennethreitz` (password noted at setup; registration disabled) |
|
|
| Homepage | Custom landing page (`/var/lib/gitea/gitea/templates/home.tmpl` on the host) with a "Featured projects" card grid — edit the file and `docker restart` the gitea container to change picks |
|
|
| Mirrors | Pull-mirrors (8h refresh) of all repos from github.com/**kennethreitz** (100, incl. rhymepad.org), **not-kennethreitz** (38), and **kennethreitz-archive** (154) — all three owners public on Gitea — created via `/api/v1/repos/migrate` with a GitHub token |
|
|
| robots.txt | Custom at `/var/lib/gitea/gitea/robots.txt` (= `$GITEA_CUSTOM/robots.txt`, served live, no restart). Allows indexing of repo/code/release/wiki pages; disallows the infinite, CPU-expensive endpoints (`compare`, `blame`, `commit`/`commits`, `src/commit`, `raw`, `archive`) and faceted `?q=`/`?sort=` listings. Points crawlers at `/sitemap.xml` (gitea auto-generates it; 14 sub-sitemaps). Default gitea ships no robots.txt = everything crawlable, which let scrapers burn crawl budget + CPU on the trap URLs. See server.md "Gitea crawler storm". |
|
|
| Anti-scraper | Abusive crawler IPs dropped at the host `DOCKER-USER` iptables chain (NOT ufw — ufw is inactive and Docker bypasses it). Currently blocked: `216.73.216.4` (anon AWS, was 46% of traffic) and `57.141.0.0/24` (Meta crawler swarm — note: blocking Meta breaks FB/IG/WhatsApp link previews). Persisted by `block-scrapers.service`; edit IP list in `/usr/local/sbin/block-scrapers.sh` then `systemctl restart block-scrapers`. |
|
|
|
|
### Backups
|
|
|
|
| | |
|
|
|---|---|
|
|
| Destination | `minio` (`gZBEIrDnfDmRcv_pQQO7q`) → bucket `dokploy-backups` via https://s3.kennethreitz.org |
|
|
| Schedule | exiftree-db (postgres): daily 06:00 UTC, keep 14 (`backupId Wa4DDnhr2alAK_f7o7C5I`) — verified working with a manual run |
|
|
|
|
### immich
|
|
|
|
> Immich — self-hosted photo backup (Google Photos replacement)
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `yct1XQb0qsCZWQwYq16DT` (appName `immich`, raw — adapted from the official release compose) |
|
|
| URL | https://immich.kennethreitz.org (container port 2283) |
|
|
| Services | immich-server, immich-machine-learning, valkey 9, postgres 14 + vectorchord (pinned digests from upstream) |
|
|
| Data | `/mnt/objects/immich/library` (media) and `/mnt/objects/immich/db` (postgres) on the volume (resized to 750 GB for a ~500 GB Apple Photos migration) |
|
|
| Env | `IMMICH_VERSION=release`, `DB_PASSWORD` in Dokploy compose env |
|
|
| Notes | First registered account becomes admin. iPhone app → server URL → background backup. Mac Photos library migration planned via osxphotos → immich CLI. |
|
|
|
|
|
|
|
|
### strainsdb
|
|
|
|
> strainsdb.org — Django cannabis strain database (7,024 strains)
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://strainsdb.org (+ www) |
|
|
| Application ID | `8FAkmUQwPFTKCiIc8iyyo` (Swarm app `strainsdb`) |
|
|
| Source | Custom git `git@github.com:kennethreitz/strainsdb.org.git`, branch `main` |
|
|
| Auth | Dokploy SSH key `github-strainsdb`, read-only deploy key |
|
|
| Build | Dockerfile (python 3.12 + pipenv, gunicorn :8000) |
|
|
| Auto-deploy | GitHub webhook (id `636625919`) on push |
|
|
| Deploys | start-first ordering (zero-downtime) |
|
|
| DNS | Both records flipped from dead Fly app `shakti` to A → 5.161.122.181 (2026-06-05, via DNSimple API) |
|
|
| Caveats | `DEBUG=True` hardcoded in settings.py; SQLite ships in the image (writes lost on redeploy — same as Fly-era behavior) |
|
|
| Revived | 2026-06-05 — the Fly app was already destroyed; site was fully down before this deploy |
|
|
|
|
### n8n
|
|
|
|
> n8n — workflow automation
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `gRwvbIWFwIGDxyDuCeYG3` (appName `n8n-pfgiqr`, raw) |
|
|
| URL | https://n8n.kennethreitz.org (container port 5678) |
|
|
| Data | `/var/lib/n8n` (root disk; sqlite + encryption key) — `N8N_ENCRYPTION_KEY` also in Dokploy compose env |
|
|
| Auth | n8n owner account — first visitor claims it (set up immediately after deploy) |
|
|
| Notes | `WEBHOOK_URL` set for inbound webhooks; task runners enabled; TZ America/New_York |
|
|
|
|
### opengist (infra project)
|
|
|
|
> Opengist — self-hosted gists
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `AYu2u7VdZynbTPzAoVwfY` (appName `opengist-zvxug6`, raw) |
|
|
| URL | https://gist.kennethreitz.org (container port 6157, `/healthcheck`) |
|
|
| Git SSH | port `2223` (host) → 2222 (container); firewall rule added |
|
|
| Data | `/var/lib/opengist` (root disk, sqlite) |
|
|
| Auth | Admin account `kennethreitz` (house password); signup disabled (`OG_DISABLE_SIGNUP=true`) |
|
|
| Contents | Empty — for manual/ad-hoc gists. A bulk import of GitHub gists was attempted 2026-06-05 but abandoned: this Opengist build (`:1`) rejects any gist content containing a literal `%` and its token-create API was flaky. Gists remain on GitHub. To retry, pin an older Opengist tag (the `%` bug is a latest-tag regression). |
|
|
|
|
### photos-backup (iCloud Photos → Hetzner Object Storage)
|
|
|
|
> Off-site backup of iCloud Photos (839 GB) for Apple-lockout insurance
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `5OPgRX49aU0S-L5Gpzswi` (appName `photos-backup-imap2e`, raw) |
|
|
| Image | `mercury-photos-backup:latest` — built on host from `photos-backup/` (icloudpd + rclone + FUSE) |
|
|
| How | Single container FUSE-mounts the Hetzner bucket and runs icloudpd writing straight to it (no local-staging ceiling, no re-download loop). Watch mode, 6h interval. |
|
|
| Backend | Hetzner Object Storage bucket `kr-icloud-photos`, endpoint `hel1.your-objectstorage.com`, region `hel1` (⚠️ region MUST be set or S3 ops fail with LocationConstraintConflict) |
|
|
| Apple auth | icloudpd session in `/var/lib/icloudpd/cookies` (user did interactive 2FA). Datacenter-IP logins trip Apple's fraud detection — a bad-username attempt once returned a scary "account locked" message that wasn't a real lock. Token refreshes ~monthly. |
|
|
| Creds | S3 key + Apple ID in Dokploy compose env |
|
|
|
|
### Immich external libraries
|
|
|
|
Immich indexes two read-only rclone mounts (host systemd units, bound into immich-server with `rslave` propagation):
|
|
- `icloud-photos-mount.service` → `hz:kr-icloud-photos/library` at `/mnt/objects/icloud-photos` → `/icloud-photos` → library "iCloud Photos" (`5616024c-…`)
|
|
- `minio-photos-mount.service` → `m:exiftree-media/originals` at `/mnt/objects/minio-photos` → `/minio-photos` → library "photos.kennethreitz.org" (`8e870747-…`)
|
|
|
|
⚠️ Indexing over the transatlantic FUSE mount is slow and reads originals back for thumbnails (one-time egress ~bucket size). Fragile if a mount drops. Acceptable for archive browsing.
|
|
|
|
### storagebox-archive
|
|
|
|
> Off-site cold archive of S3 buckets → Hetzner Storage Box (BX21, 5 TB)
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `I01acQdiU5giQo5WrclIs` (appName `storagebox-archive`, raw, infra project) |
|
|
| Target | Hetzner Storage Box `u609317.your-storagebox.de:23` (SFTP/SSH). Key auth: mercury + Kenneth's Mac keys installed in `.ssh/authorized_keys`. Password also in compose env. |
|
|
| Schedule | rclone container, 24h loop: `exiftree-media` → `sbox:archives/exiftree-media`, `dokploy-backups` → `sbox:archives/dokploy-backups` |
|
|
| Why | `exiftree-media` (166 GB photo-site library) previously had ZERO backups — single copy on the volume. This is its first real off-server copy. |
|
|
| Note | Storage Box keep "external reachability" ON (mercury is US Hetzner Cloud, box is EU; Mac restores come from home). Compose YAML saved at repo root `storagebox-archive.compose.yml`. |
|
|
|
|
### dozzle (log dashboard)
|
|
|
|
> Live container-log dashboard for the backup/archive jobs
|
|
|
|
| | |
|
|
|---|---|
|
|
| Compose ID | `P38wZWXlREYXBhG75QQLi` (appName `dozzle`, raw, infra project) |
|
|
| URL | https://logs.kennethreitz.org (Traefik basicAuth `dozzle-auth@file`, user `kenneth`) |
|
|
| Scope | All containers on the host (no filter) |
|
|
| Purpose | Watch new files stream into cold storage. archive rclone runs with `-v` so each transfer logs `Copied (new): ...`. |
|
|
|
|
### iCloud Drive → Storage Box (in progress 2026-06-06)
|
|
|
|
- `drive.kennethreitz.org` (Filebrowser) **repointed** to read-only host mount
|
|
`icloud-drive-sbox-mount.service` → `sbox:icloud-drive` (was the local volume mirror).
|
|
- Bulk copy local mirror → `sbox:icloud-drive` via **rsync over SSH** (~33 MiB/s; rclone-SFTP
|
|
was only 2.5 MiB/s — **use rsync for bulk transfers to the Storage Box**, small files).
|
|
- Still TODO once rsync completes: repoint the `icloud-sync` timer to write iCloud →
|
|
`sbox:icloud-drive` directly (Dokploy-managed), remove the 291 G local mirror, drop the
|
|
redundant `icloud-drive` line from storagebox-archive. Then the volume holds ~only Immich.
|
|
|
|
### rhymepad
|
|
|
|
> RhymePad — phoneme-aware rhyme analysis for poets & rappers
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://rhymepad.org (canonical) + https://rhymepad.kennethreitz.org; www.rhymepad.org 301→apex |
|
|
| Application ID | `vw6R6B_8uK07_U1Mm48d-` (Swarm app `rhymepad`) |
|
|
| Source | Custom git `git@github.com:kennethreitz/rhymepad.org.git`, branch `main` (repo created 2026-06-07) |
|
|
| Auth | Dokploy SSH key `github-rhymepad`, read-only deploy key |
|
|
| Build | Dockerfile (python 3.12 + uv, FastAPI/uvicorn :8000, NLTK data pre-warmed for g2p-en) |
|
|
| API | `/api/analyze` (POST), `/api/lookup` (GET); static UI at `/` |
|
|
| Auto-deploy | GitHub webhook (id `637508735`), start-first updates |
|
|
| Deployed | 2026-06-07 |
|
|
|
|
### pytheory-playground
|
|
|
|
> PyTheory Playground — interactive music theory toolbox (pytheory + responder)
|
|
|
|
| | |
|
|
|---|---|
|
|
| URL | https://playground.pytheory.org |
|
|
| Project ID | `FaXCUHVmOaJu6w8G2Mz7t` |
|
|
| Environment | `production` (`wtnERcBz2nWQi56Xpb7hs`) |
|
|
| Application ID | `C2oKPwJ9cNXWmmLg-K-nL` (Swarm app `pytheory-playground-ju4thg`) |
|
|
| Source | Custom git `https://github.com/kennethreitz/pytheory-playground.git` (public, no auth), branch `main` |
|
|
| Build | Dockerfile (uv python 3.14 + lilypond + ffmpeg; responder app on :5042) |
|
|
| Domain | `playground.pytheory.org` (`pAS0p44NhPWjXYhWM8toa`) → A → 5.161.122.181; LE via Traefik |
|
|
| Auto-deploy | GitHub webhook (id `640130465`) on push → `$DOKPLOY_URL/api/deploy/<refreshToken>` |
|
|
| Analytics | Umami website `a691eb37-8047-4529-9ea8-64ef1957f564` (registered via SQL insert into the umami db — admin password isn't the documented default) |
|
|
| Deployed | 2026-06-12 |
|
|
| Note | pytheory.org registered 2026-06-12 via DNSimple. The apex (ALIAS → pytheory.kennethreitz.org → kennethreitz.github.io) and www serve the GitHub Pages docs for repo kennethreitz/pytheory (docs/CNAME = `pytheory.org`); only `playground.` lives on mercury |
|
|
|
|
Redeploy: `scripts/api.sh application.deploy '{"applicationId":"C2oKPwJ9cNXWmmLg-K-nL"}'`
|
|
|
|
|
|
## iCloud Drive cutover COMPLETE 2026-06-07
|
|
|
|
- 207,038 files / 311.5 GB rsynced to `sbox:icloud-drive` (verified, 0 errors).
|
|
- `drive.kennethreitz.org` (Filebrowser) reads `sbox:icloud-drive` via mount.
|
|
- `icloud-sync.service` repointed: iCloud → `sbox:icloud-drive` directly, **daily** (SFTP
|
|
listing of 200k files is slow; backup cadence is fine daily). backup-dir on the box.
|
|
- storagebox-archive: icloud-drive line + local bind removed (now primary on the box).
|
|
- **Local 291 GB mirror removed** → volume 614G→323G, then cache reclaim → **65G used**.
|
|
|
|
## icloudpd cache: cleared the bulk bloat
|
|
The photos-backup rclone vfs-cache grew to 257G on the volume (the `--vfs-cache-max-size`
|
|
can't bound it during a bulk run — files pending the slow US→EU upload can't be evicted).
|
|
iCloud Photos download is now COMPLETE (bucket `kr-icloud-photos` = 437 GiB / 61k objects,
|
|
icloudpd idle). Cleared the stale cache; it now sits at ~34 MB and stays lean because
|
|
incrementals only fetch new photos. Volume usage: **65 GB** (Immich 59 + infra 6).
|
|
|
|
## Volume teardown readiness
|
|
Volume now holds only Immich (59G) + dokploy-backups MinIO (6G) + tiny. Ready for option A
|
|
(migrate to a ~250 GB volume, delete the 1 TB). Still gated on a Hetzner volume-quota bump
|
|
for a safe side-by-side migration.
|
|
|
|
## iCloud Photos backup — PARKED 2026-06-07
|
|
|
|
Abandoned the continuous icloudpd backup. Root cause: icloudpd downloads far faster
|
|
than the US→EU upload to Hetzner Object Storage drains, so the rclone vfs cache buffer
|
|
grows past any available disk (filled root twice + the volume once). Backpressure
|
|
(`--vfs-cache-min-free-space`) + an auto-stop watchdog were added and are committed, but
|
|
per owner decision we are NOT running it.
|
|
- photos-backup container: stopped, restart=no (won't revive).
|
|
- Partial backup retained: `kr-icloud-photos` bucket = ~498 GiB / 63k objects (of ~839 GiB).
|
|
Kept as a real off-site snapshot; costs a few € in object storage. Delete if not wanted.
|
|
- iCloud **Drive** backup is UNAFFECTED and stays (daily sync → Storage Box, drive.kennethreitz.org).
|
|
|
|
|
|
## Traefik custom config (hand-written, NOT Dokploy-managed)
|
|
|
|
`/etc/dokploy/traefik/dynamic/custom-rhymepad-www-redirect.yml` (copy in repo `traefik/`):
|
|
301 redirects `www.rhymepad.org` → `https://rhymepad.org` (path-preserving). Uses priority
|
|
1000 to beat Dokploy's auto-generated www routers (priority 24), so it survives app
|
|
redeploys. Pattern to reuse for any www→apex redirect: separate dynamic file + high-priority
|
|
routers (web + websecure) with a `redirectRegex` middleware. Note: write the file locally and
|
|
scp it — backtick `Host()` rules get mangled by heredoc escaping.
|