Repository navigation
Architecture 07 Deployment View
The same container images run at home and in the cloud. Only configuration and instance counts differ.
| Self-hosted (owner start) | Cloud | |
|---|---|---|
| Start | one docker compose up
|
Helm chart on Kubernetes |
| Instances | one per service | per service by load, autoscaled |
| Database | one PostgreSQL with one database per service | PostgreSQL per service or managed cluster |
| Event bus | NATS single container | NATS cluster |
| Billing | off | on or off |
| Registration | off | open or invitation only |
| External access | Tailscale or reverse proxy | gateway with public domain |
| Hardware | mini PC, NAS or Raspberry Pi 5 with 4 GB | any |
flowchart LR
internet["Clients (Tailscale / LAN)"] --> traefik["traefik :80/:443"]
subgraph net["Docker network mowdeck"]
traefik
nats["nats :4222 / :8222"]
pg["postgres :5432"]
migrate["openfga-migrate (one-shot)"]
fga["openfga :8080 / :8081"]
future["identity, fleet, device-gateway, telemetry, realtime"]
web["web :8080 (nginx, the PWA)"]
end
traefik -.->|"/api/v1/*"| future
traefik -.->|"/ (priority 1)"| web
future --> nats
future --> pg
migrate --> pg
fga --> pg
migrate -->|"completed successfully"| fga
The web service serves the static web app (an unprivileged, read-only nginx) at / with router priority 1, so every /api/v1/* route and /.well-known/jwks.json take precedence; the app and the API share one origin, so no CORS is needed and the refresh cookie (SameSite=Strict) works unchanged (ADR 0020). Traefik waits for a healthy web like for the API services.
See Operating the Compose stack for ports and commands.
The chart infra/helm/mowdeck (ADR 0029, Helm operations) deploys the same images with the same settings (values.yaml uses the names of the Compose .env.example).
flowchart LR
clients["Clients (public domain / Tailscale)"] --> ing["Ingress controller (Traefik, ...)"]
subgraph ns["Namespace mowdeck (one release)"]
ing -.->|"/api/v1/*, /.well-known/jwks.json"| svc["identity, fleet, telemetry, realtime, notifications, scheduler (Deployments)"]
ing -.->|"/api/v1/device-gateway"| gw["device-gateway (StatefulSet, one shard per pod)"]
ing -.->|"/"| web["web (nginx)"]
jobs["migration Jobs (hooks or per revision)"] --> pg
boot["authz-bootstrap Job"] --> fga
svc --> nats["NATS StatefulSet (JetStream PVC, subchart)"]
gw --> nats
svc --> pg["PostgreSQL: CloudNativePG Cluster, one Database per service (or external)"]
svc --> fga["OpenFGA (subchart)"]
fga --> pg
end
| Homelab overlay | Cloud overlay | |
|---|---|---|
| Replicas | one per service | two per stateless service, CPU autoscaling, PodDisruptionBudgets, spread over nodes |
| PostgreSQL | CloudNativePG, one instance | CloudNativePG with three instances, or a managed PostgreSQL (postgresql.external) |
| NATS | one server with JetStream | three node cluster with JetStream |
| OpenFGA | one replica | three replicas |
| Ingress | k3s Traefik or the Tailscale operator | Ingress controller with cert-manager TLS and a rate limit on the login paths |
Migrations run as pre-install/pre-upgrade hooks with an external database and as Jobs per release revision with the in-cluster one; every database service waits in an init container until its schema is current, every OpenFGA user until the store carries its release's model. Releases (.github/workflows/release.yml, Releases) publish multi-arch images to ghcr.io/<owner>/mowdeck/<image> and the chart to oci://ghcr.io/<owner>/charts/mowdeck.
- Built for amd64 and arm64 (release workflow, QEMU), running as non-root with a read-only file system.
- Configuration and secrets only through environment variables or secret files, never in the image.
- Database migrations run as a separate one-shot step before the service starts (Compose:
<service>-migrate, Kubernetes: migration Jobs plus thewait-for-schemainit container). - Remote access at home goes through Tailscale or an HTTPS reverse proxy. Installing the PWA reliably and Web Push need HTTPS (for example Tailscale's HTTPS certificates); behind HTTPS set
IDENTITY_REFRESH_COOKIE_SECURE=trueandREALTIME_ALLOWED_ORIGINSto the HTTPS origin.
Storage is exchangeable: PostgreSQL for time series, local volume or S3-compatible storage (such as MinIO) for files. Moving from home to cloud is a database export, not a rebuild.
Mirrored from docs/architecture/07-deployment-view.md at commit a46c22cf085b9be11366cd38887abd045c842630. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit a46c22cf085b9be11366cd38887abd045c842630. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- Development
- Operations
- Spikes
- Guides
- Original plan (German)