Files
kennethreitz 3218320b1e umami report: migrate from host cron to n8n workflow
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>
2026-06-16 09:02:35 -04:00

27 KiB

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 timedomain.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/umamichange 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 adminview-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.servicehz:kr-icloud-photos/library at /mnt/objects/icloud-photos/icloud-photos → library "iCloud Photos" (5616024c-…)
  • minio-photos-mount.servicem: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-mediasbox:archives/exiftree-media, dokploy-backupssbox: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.servicesbox: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.orghttps://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.