Created: 2026-04-05
Location: /opt/doorcounty-wi/
Domain: doorcounty-wi.com
Home Lab Host: 192.168.11.18
A photo display platform that pulls curated, AI-analyzed photos of Door County, Wisconsin, and displays them on HDMI screens at business locations throughout the region.
System consists of:
[Photo Sources] → [Ingestion] → [MinIO Staging] → [YOLOv8 Analyzer] → [Approval UI] → [MinIO Production] → [Pi Display Devices]
Path 1: File-based sources (iCloud, SMB, NFS)
[/mnt/icloud files]
↓
[ingestion-service container]
→ iCloud plugin reads files
→ writes to MinIO staging bucket
↓
[photo-analyzer processes from staging]
↓
[Results written to PostgreSQL]
↓
[Manual approval via web UI]
↓
[Approved photos → MinIO production bucket]
Path 2: Web upload
[User uploads via browser]
↓
[FastAPI POST /upload endpoint receives file]
→ writes DIRECTLY to MinIO staging bucket
↓
[photo-analyzer processes from staging]
↓
[Results written to PostgreSQL]
↓
[Manual approval via web UI]
↓
[Approved photos → MinIO production bucket]
Key insight: Both paths converge at "MinIO staging bucket." After that, the pipeline is identical.
All services run as Docker containers on 192.168.11.18 in the doorcounty-wi network.
minio - S3-compatible object storage
staging (pending analysis), production (approved)postgres - Metadata database
doorcountyphotos, devices (see schema in Project_Overview doc)fastapi-app - Main web application
nginx - Reverse proxy
wireguard-provisioning - Provisioning VPN
wireguard-production - Production VPN
photo-analyzer - YOLOv8 image analysis
/mnt/icloud (read-only access to source photos)./analyzer:/app (application code)ingestion-service - Pluggable file-based ingestion
/mnt/icloud (read-only access to source photos)./ingestion:/app (application code)./ingestion/sources.yaml:/app/sources.yaml (plugin config)plugins/icloud.py (enabled in MVP)plugins/smb.py (future - disabled)plugins/google_photos.py (future - disabled)plugins/dropbox.py (future - disabled)/app/sources.yaml controls which plugins are enableddoorcounty-wiDNS (via Cloudflare):
doorcounty-wi.com → 66.188.146.166 (Proxied - orange cloud)vpn-provision.doorcounty-wi.com → 66.188.146.166 (DNS only - grey cloud)vpn-production.doorcounty-wi.com → 66.188.146.166 (DNS only - grey cloud)Nameservers:
coby.ns.cloudflare.comkeyla.ns.cloudflare.comPort forwarding required on router:
minio-data - All object storagepostgres-data - Database fileswireguard-provisioning-data - Provisioning VPN configswireguard-production-data - Production VPN configs/mnt/icloud - 3.5TB external SSD with 315GB of iCloud photos
/opt/doorcounty-wi/
├── docker-compose.yml # Main container orchestration
├── CREDENTIALS.txt # Service passwords (chmod 600)
├── ARCHITECTURE.md # This file
├── app/ # FastAPI application code
├── analyzer/ # YOLOv8 analyzer code
├── ingestion/ # Ingestion service code
│ ├── sources.yaml # Plugin configuration
│ └── plugins/ # Ingestion plugins
│ ├── icloud.py # iCloud ingestion (MVP)
│ ├── smb.py # SMB ingestion (future)
│ └── ... # Additional plugins
└── nginx/ # Nginx configs
├── nginx.conf
└── ssl/
/opt/doorcounty-wi/CREDENTIALS.txt (chmod 600)Two-VPN design:
Provisioning VPN (port 143)
Production VPN (port 587)
doorcounty-wi networksources.yamlControls which ingestion sources are enabled and their settings.
Current MVP configuration:
sources:
- name: icloud
enabled: true # ← Only this is enabled in MVP
plugin: plugins.icloud
config:
photos_path: /mnt/icloud
file_extensions: [.heic, .jpg, .jpeg, .png]
preserve_originals: true
- name: smb_share
enabled: false # ← Future expansion
plugin: plugins.smb
- name: google_photos
enabled: false # ← Future expansion
plugin: plugins.google_photos
plugins/smb.py)sources.yaml to enable the pluginPlugin interface (all plugins must implement):
class IngestionPlugin:
def scan(self) -> List[PhotoFile]:
"""Return list of photos to ingest"""
def upload_to_minio(self, photo: PhotoFile) -> bool:
"""Upload photo to MinIO staging bucket"""
def cleanup(self):
"""Cleanup temporary files if needed"""
Algorithm refined iteratively by correlating scores with human approval decisions.
Format: [PREFIX]-[NUMBER]
pulse-001, ep-042, sisbay-007Stage 1 - Generic Boot:
Stage 2 - Production Mode:
Problem: 500+ devices lose power simultaneously, all boot and check in at once.
Solution:
| Decision | Choice | Rationale |
|---|---|---|
| Object storage | MinIO | S3-compatible API - zero code change to migrate to AWS S3 |
| Database | PostgreSQL | Scalable, portable, queryable metadata |
| Web framework | FastAPI | Modern, async, scales well, strong AI-assisted coding |
| VPN | WireGuard | Lightweight, fast, low CPU on Pi Zero 2W |
| Provisioning VPN port | 143 TCP | Mimics IMAP, passes most firewalls |
| Production VPN port | 587 TCP | Mimics SMTP submission, passes most firewalls |
| Photo analyzer | YOLOv8 | Best balance of speed, accuracy, community support |
| Quality score range | 0-255 | Standard byte range, finer granularity than 0-100 |
| Pi platform | Pi Zero 2W | Full Linux, proven community, good form factor, ARM portable |
| Config format | JSON | Human-editable, lightweight, easy to parse on Pi |
| Poll interval | MAC hash → 58-65s | Deterministic jitter, no RNG, prevents thundering herd |
| Ingestion architecture | Plugin-based | Add new sources without reworking core |
| Web uploads | FastAPI endpoint | No separate container needed, direct to MinIO staging |
/mnt/icloud/mnt/project/Project_Overview/opt/doorcounty-wi/docker-compose.yml/opt/doorcounty-wi/CREDENTIALS.txt/opt/doorcounty-wi/ingestion/sources.yamlThe photo analyzer now extracts complete EXIF metadata from HEIC images:
Extracted Fields:
Contextual Metadata Derived:
Dependencies:
pillow-heif - Native HEIC support for EXIF extraction/opt/doorcounty-wi/analyzer/requirements.txtRe-analysis:
To re-run EXIF extraction on existing photos:
docker exec photo-analyzer python3 /app/analyze.py --reanalyze