Repository navigation
ADR 0080 Codespaces Dev Container
Accepted (2026-11-07).
A contributor needs Python 3.13, uv, Node 24, pnpm, Docker with Compose v2 and the docs toolchain, plus a stack that is running before they can try a change. Getting that on a fresh machine takes an afternoon. GitHub Codespaces can provide it in a browser tab, if the Compose stack runs inside the Codespace and its web app is reached through GitHub's port forwarding.
The forwarder changes what the stack sees. A Codespace's port 80 is published as https://$CODESPACE_NAME-80.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN. TLS ends at the forwarder; Traefik receives plain HTTP, with a forwarder address as the TCP peer. Several settings depend on the public origin: the refresh cookie's Secure flag, the realtime WebSocket's origin check, the passkey host name, links in notifications, and HSTS.
One configuration, .devcontainer/, no variants:
-
The base image
mcr.microsoft.com/devcontainers/python:1-3.13-bookworm, pinned by digest in.devcontainer/Dockerfile, which also removes the image's Yarn apt repository (its signing key is no longer accepted, which made everyapt-get updateand so every feature install fail; nothing here needs Yarn), with the Dev Container features for Node (pinned to.node-version), Docker-in-Docker and the GitHub CLI. uv is copied into the image fromUV_IMAGEofinfra/compose/.env.example(pinned by digest, kept equal bytools/repo-checks/tests/test_devcontainer.py), pnpm through Corepack at the version ofpackage.json(packageManager), so the Codespace and the stack's images agree.COREPACK_ENABLE_DOWNLOAD_PROMPT=0(containerEnv) lets Corepack fetch that pnpm without asking: a prebuild has no terminal to answer its download prompt and waited on it. -
hostRequirementsof 4 CPUs, 16 GB of memory and 64 GB of storage, because the stack builds about a dozen images from the checkout. -
The lifecycle commands are split so that a Codespaces prebuild can do the heavy part:
onCreateCommand(on-create.sh) installs uv and pnpm,updateContentCommand(update-content.sh) runsuv sync,pnpm installand installs the pre-commit hook; both run when the container is built, also inside a prebuild, andupdateContentCommandruns again when a Codespace opens from a prebuild older than the branch.postCreateCommand(generate-env.sh) writesinfra/compose/.envonce per Codespace, never in a prebuild, because the secrets and the public origin belong to one Codespace.postStartCommand(post-start.sh) runsdocker compose up -d --build --waiton every start, which is idempotent and quick when the Docker cache is warm; the image builds stay out of the prebuild because the Docker daemon is not reliably available there. -
generate-env.shwrites the.envfrom.env.exampleand never overwrites an existing one. Every secret (the PostgreSQL passwords, the OpenFGA key, the NATS passwords throughnats-passwords.sh, the identity, fleet and vision keys) is random and per Codespace. Nothing secret is committed;.envis git-ignored. -
The values derived from the public origin
https://$CODESPACE_NAME-80.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:Key Value Why IDENTITY_REFRESH_COOKIE_SECUREtrueThe browser talks HTTPS to the forwarder, so the refresh cookie gets the Secureflag as in any HTTPS installation.REALTIME_ALLOWED_ORIGINSthe origin and http://localhostThe browser sends the forwarded origin on the WebSocket; the default http://localhostwould refuse it.IDENTITY_PUBLIC_URLthe origin Passkeys are bound to this host name and need HTTPS. NOTIFICATIONS_APP_URLthe origin Links in notifications. WEB_HSTSfalseThe host name disappears with the Codespace, so the browser should not pin it for a year. -
GATEWAY_TRUSTED_PROXIESandGATEWAY_CLIENT_IP_DEPTHstay empty. Traefik then replacesX-Forwarded-*with its own view of the connection. The forwarder's address is not a documented, stable range, so naming it would be a guess, and trusting a wide range would let a client fake its address. The only consumer ofX-Forwarded-Protohere is HSTS (off), plus the disabled billing service. The cost: every request appears to come from one address, so Identity's per-IP login throttling is one shared bucket. That is acceptable for a development environment with one user. -
The Codespace's ports are private by default (GitHub authenticates every request to the forwarded URL); the setup does not make them public.
Options considered: Docker Compose through the repository's own services only (no Compose stack), which would leave the web app without its backend; a Compose-based dev container with the stack as sibling containers, which needs the Docker socket of the host and is not available in Codespaces; a separate configuration for a lightweight Codespace without the stack, which doubles the maintenance for a machine size that is not enough for the web build anyway.
- "Open in Codespaces" gives a contributor a working checkout with the stack running and the web app open; the first start takes several minutes (toolchains, then the image builds), later starts are fast.
- It needs a machine type with 4 CPUs and 16 GB; the free tier's smaller machines are refused by
hostRequirements. - What a Codespace cannot do: the Bluetooth bridge, the Matter server (host networking and mDNS) and the other host-network profiles are not available; a mower is simulated or linked through its cloud.
- Nothing else depends on the public origin: the web app and the APIs use relative URLs and the Host header they receive, and no service has a setting for the forwarder's address beyond the ones above.
- Verification of the forwarded flow (registration, login, WebSocket) can only be done in a real Codespace; the CI of this repository checks the files, not a Codespace.
- Follow-up: if the forwarder turns out to send a reliable
X-Forwarded-For, trusting it withGATEWAY_CLIENT_IP_DEPTH=1would give per-client throttling. - Dependabot keeps the dev container current (
.github/dependabot.yml): thedevcontainersecosystem bumps the features indevcontainer.jsonanddevcontainer-lock.jsonin one grouped pull request, thedockerecosystem on/.devcontainerbumps the digest of the base image in theDockerfile. Both arrive asci:commits and release nothing.
Mirrored from docs/adr/0080-codespaces-dev-container.md at commit 7e8a8734a7896892dc1bf582ae9b6d6715180c4e. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 7e8a8734a7896892dc1bf582ae9b6d6715180c4e. 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
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- ADR 0065: Security center and new-login alerts
- ADR 0066: Cloud provider plugins and adapters for more vendors
- ADR 0067: Error code lexicon, provider independent
- ADR 0068: Public demo mode
- ADR 0069: Provider conformance kit and provider package template
- ADR 0070: Virtual zones for mowers without a vendor map
- ADR 0071: Capability matrix generated from the adapters
- ADR 0072: API explorer in the web app, for the public demo
- ADR 0073: Guided tour with coach marks
- ADR 0074: NATS authentication and per-service subject permissions
- ADR 0075: Chat control as an optional assistant add-on
- ADR 0076: Demo sandboxes, one account per visitor
- ADR 0077: Liveness, readiness and routing
- ADR 0078: Batched state ingestion
- ADR 0079: Fast demo sandbox claim and release
- ADR 0080: GitHub Codespaces dev container with the full Compose stack
-
Development
- Getting started
- Service template
- Shared libraries
- Cloud providers: adding a vendor
- Adding a provider: template and conformance kit
- Adding an error lexicon for a vendor
- Design: identity and authorization
- Design: fleet, telemetry, realtime
- Design: web app
- Web app
- Home Assistant integration
- Camera source bridge protocol
- Security assurance case
- Fuzzing and property-based tests
- CI and pull request scope
- Reproducible builds
- GitHub wiki mirror
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Internal TLS
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Vendor library updates
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Matter server
- Chat control (assistant add-on)
- Source code offer (AGPL-3.0 section 13)
- Public demo
- Load testing
- Spikes
-
Guides
- Guides
- ESP32 bridge: purchase and setup
- Signing in with a passkey
- Signing in with GitHub
- Error codes in plain words
- Aerial photo under the map
- Signing in with Apple
- Confirming critical actions
- Your security page
- iPhone widgets and the mowing Live Activity
- Apple Watch
- The setup tour
- Apple Home and Google Home (Matter)
- Chat control
- Worx Landroid, Kress and LandXcape mowers
- Scripting MowDeck
- Capability matrix
- API reference
- Original plan (German)