Airflow & Database Backup¶
Airflow DAG development workflow, GitDagBundle, podcast pipeline, and Databasus backup management.
Source: extracted from AGENTS.md
Airflow DAG & Plugin Development Workflow¶
Architecture: GitDagBundle (auto-sync DAGs from Git)¶
Airflow 3.x GitDagBundle pulls DAGs from GitHub repo (NOT Gitea — Gitea repo is archived). Scheduler auto-refreshes every 5 minutes (refresh_interval: 300).
Bundle config (in compose.yaml env vars):
- Name:
dags-folder(must match Airflow default — task runner expects this name) - Repo:
https://github.com/minhluc-info/docker-compose.git - Subdir:
stacks/orchestration/airflow/dags - Tracking:
mainbranch - Token: Komodo Variable
GITHUB_TOKEN(scope:repo)
Server paths:
- Tracking repo:
/tmp/airflow/dag_bundles/dags-folder/tracking_repo/ - Versions:
/tmp/airflow/dag_bundles/dags-folder/versions/{commit_hash}/ - Bare:
/tmp/airflow/dag_bundles/dags-folder/bare/
Deploy methods¶
| What | Location | Deploy method | Restart? |
|---|---|---|---|
| DAGs | stacks/orchestration/airflow/dags/*.py |
Git push → scheduler auto-pulls within 5min | ❌ |
| Python packages | requirements.txt |
Rebuild image + push + redeploy | ✅ |
| Compose/TOML | compose.yaml / .toml |
km x deploy-stack airflow |
✅ |
Development workflows¶
1. DAG-only changes (fully automatic — just push):
# Edit DAG file in stacks/orchestration/airflow/dags/
git add -A && git commit -m "feat(airflow): new DAG" && git push origin main
# Done — scheduler auto-pulls within 5 minutes
# To force immediate: docker exec airflow-scheduler airflow dags reserialize
2. Normal deploy (compose/TOML changes):
git add -A && git commit -m "chore(airflow): ..." && git push origin main
echo "" | km x run-sync orchestration # sync TOML changes
echo "" | km x deploy-stack airflow # deploy containers
3. New Python package (rebuild image):
# Edit requirements.txt on development server
ssh development
cd /tmp/airflow-build
rsync -av /path/to/stacks/orchestration/airflow/{Dockerfile,requirements.txt} .
docker build -t 100.68.251.84:3005/lucndm/airflow:latest .
docker push 100.68.251.84:3005/lucndm/airflow:latest
exit
echo "" | km x deploy-stack airflow
4. Edit-on-server (fast iteration):
# Edit DAG directly on server for testing
ssh root@100.68.251.84
# Find the latest version dir
ls /tmp/airflow/dag_bundles/dags-folder/versions/
# Edit the DAG file
vi /tmp/airflow/dag_bundles/dags-folder/versions/{hash}/stacks/orchestration/airflow/dags/podcast_download.py
# Force re-parse
docker exec airflow-scheduler airflow dags reserialize
# Test and iterate, then commit to git when done
Podcast Download Pipeline¶
DAG: podcast_download — Daily automated podcast management pipeline.
Schedule: 0 18 * * * (18:00 UTC = 01:00 UTC+7)
Task graph: discover_and_download → trigger_library_scan → cleanup_podcasts
Architecture:
YouTube channels → yt-dlp (audio + VTT + thumbnail) → ffmpeg (opus 96kbps)
→ metadata.json → mc cp S3 OpenList + local copy → ABS library scan → cleanup
Channels: Tangocquy (23 eps), BVVN (197 eps), Spiderum (~150 eps after Shorts filter)
Config:
- Audio format: opus 96kbps (YouTube serves opus natively, ~40% smaller than m4a)
- Shorts filter:
duration > 120sat discover +--match-filterat download - Subtitles:
en,viVTT format (best effort — YouTube 429 rate limits subtitles) - S3 checkpoint:
mc statskips already-processed episodes on retry - tmpfs: 1GB
/tmpnamed volume for temp downloads (avoids Unraid FUSE overhead) - Local copy:
/podcasts/{channel}/{episode_name}/{episode_name}.opusfor ABS/Jellyfin - S3 path:
podcasts/{channel}/{year}/{episode}/(backup/remote access)
Key env vars (in compose.yaml):
S3_ENDPOINT,S3_ACCESS_KEY,S3_SECRET_KEY— OpenList S3 credentialsABS_URL,ABS_TOKEN— Audiobookshelf API for library scan
ABS library: "YouTube Podcasts" (ID 61e735a3-c237-4a3f-b6b7-c772b69a3d4a, type=podcast, path /podcasts)
Jellyfin library: "Podcasts" (ID 608e1ed1e97ee5c4556f3d0299ac9894, Music type, path /data/podcasts)
Key files¶
| File | Purpose |
|---|---|
stacks/orchestration/airflow/compose.yaml |
2 services (apiserver, scheduler), LocalExecutor, UID 99:100, iGPU mount, tmpfs |
stacks/orchestration/airflow/Dockerfile |
Custom image: ffmpeg, mc (MinIO client), yt-dlp, chmod o+rX for UID 99 |
stacks/orchestration/airflow/requirements.txt |
Python packages: yt-dlp>=2025.5.0, apache-airflow-providers-git |
stacks/orchestration/airflow/dags/podcast_download.py |
Main DAG: 3 tasks, S3 checkpoint, local copy, Shorts filter, cleanup |
komodo/stacks/orchestration/airflow.toml |
Komodo config: server unraid, S3/ABS env vars via Komodo Variables |
Docker image: 100.68.251.84:3005/lucndm/airflow:latest |
Custom image with podcast deps |
Database Backup (Databasus/Postgresus)¶
Databasus (container name: postgresus) — self-hosted backup management cho PostgreSQL, MySQL, MariaDB, MongoDB.
| Detail | Value |
|---|---|
| Web UI | http://100.68.251.84:4005 |
| Auth | admin / Admin@2026 (reset via DB: clear hashed_password, then /api/v1/users/admin/set-password) |
| Workspace | Homelab (e1541210-7770-42fc-9fd9-0082cf8918a6) |
| Storage | S3 via OpenList (http://100.68.251.84:5246/backups/databasus/) |
| Schedule | Daily at 18:00 UTC (01:00 UTC+7) |
| Retention | 3 months |
| Encryption | None |
| Notifier | ntfy via webhook (http://100.126.172.96:9282/databasus) — fires on scheduled backup completion/failure |
| Stack | stacks/database/postgresus/compose.yaml |
Database inventory (43 databases):
| Instance | Port | Type | Databases |
|---|---|---|---|
100.68.251.84 |
5432 | PostgreSQL 17 | airflow, alist, anythingllm, blinko, cliproxyapi, coder, devpidb, firefly, firefly_pico, freshrss, gitea, hyperdrive, inboxzero, jellyfin, litellm, mem0db, mempalace, n8n, nanobot_v2, nanobot_v2_test, opencode_metrics, paperless, postgres, quickwit, romm, sftpgo, supabase, wiki, _supabase |
100.68.251.84 |
5437 | PostgreSQL 14 | immich |
100.68.251.84 |
5438 | PostgreSQL 17 | affine, lightrag, litellm, mem0db, nanobot, nanobot_test, openwebui, postgres, release_insights |
100.68.251.84 |
3306 | MariaDB 11 | memos |
DB credentials: postgres / minhluc1 (PostgreSQL), root / minhluc1 (MariaDB) — stored in Komodo Variable DB_PASSWORD.
API auth flow:
POST /api/v1/users/signin→{"email":"admin","password":"..."}→ JWT token- All API calls:
Authorization: Bearer <token>+?workspace_id=<ws_id>
Password reset (if locked out):
-- Via internal PG (port 5437 inside container)
UPDATE users SET hashed_password=NULL, password_creation_time=NOW() WHERE email='admin';
-- Then: POST /api/v1/users/admin/set-password {"password":"<new>"}
Internal database: PostgreSQL 17 on port 5437 (inside container). Access: docker exec postgresus /usr/lib/postgresql/17/bin/psql -p 5437 -U postgres -d postgresus
Pitfalls:
storePeriodenum values:"WEEK","MONTH","3_MONTH","6_MONTH"(NOT"THREE_MONTH")storageIdnot saved properly via/backup-configs/saveAPI — must update directly inbackup_configstable- MARIADB type uses key
"mariadb"in create payload (NOT"mysql") - pgdata permissions:
chmod 700 /mnt/user/appdata/postgresus/data/pgdatarequired after volume creation