# HomeOS — dev stack (dev = prod parity, R8). All app-family services share one image. # mosquitto / mqtt-listener (Phase 3) and discovery sidecar (Phase 4) are added in their phases. x-app-image: &app-image image: homeos-app:dev build: context: . dockerfile: docker/app/Dockerfile args: HOST_UID: ${HOST_UID:-1000} HOST_GID: ${HOST_GID:-1000} # Laravel reads its own .env from the mounted volume (correct phpdotenv interpolation). # Compose still reads root .env for ${...} substitution in this file (ports, uid, db creds). volumes: - .:/var/www/html - ./docker/nginx/default.conf:/etc/nginx/sites-available/default:ro depends_on: db: condition: service_healthy redis: condition: service_started restart: unless-stopped services: app: <<: *app-image ports: - "${APP_PORT:-80}:80" - "${VITE_PORT:-5173}:5173" horizon: <<: *app-image command: ["php", "artisan", "horizon"] ports: [] scheduler: <<: *app-image command: ["php", "artisan", "schedule:work"] ports: [] reverb: <<: *app-image command: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"] ports: - "${REVERB_HOST_PORT:-6001}:8080" mqtt-listener: <<: *app-image command: ["php", "artisan", "mqtt:listen"] restart: always ports: [] db: image: timescale/timescaledb:latest-pg17 environment: POSTGRES_DB: ${DB_DATABASE:-homeos} POSTGRES_USER: ${DB_USERNAME:-homeos} POSTGRES_PASSWORD: ${DB_PASSWORD:-homeos} volumes: - db-data:/var/lib/postgresql/data ports: - "127.0.0.1:${DB_HOST_PORT:-5432}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-homeos} -d ${DB_DATABASE:-homeos}"] interval: 5s timeout: 5s retries: 10 restart: unless-stopped redis: image: redis:7-alpine command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data restart: unless-stopped mosquitto: image: eclipse-mosquitto:2 # Wrapper runs mosquitto + reloads it (SIGHUP) when a device credential is provisioned. command: ["/bin/sh", "/mosquitto/config/entrypoint.sh"] volumes: - ./docker/mosquitto/config:/mosquitto/config - mosquitto-data:/mosquitto/data ports: # LAN-reachable so real devices can connect (auth + ACL enforced) - "${MQTT_HOST_PORT:-1883}:1883" restart: unless-stopped # Ring bridge — part of the stack so it starts automatically (no manual command). Idle and # cheap until you log in. Ring has no local API (handoff §12): the account login + 2FA is done # in ring-mqtt's own web UI (http://:${RING_UI_PORT:-55123}), which mints the refresh # token; the bridge then connects to our broker as the least-privileged `ring` user and # publishes device state under ring/# for HomeOS to ingest. We do not build a custom Ring client. ring-mqtt: image: tsightler/ring-mqtt depends_on: - mosquitto environment: RUNMODE: docker volumes: - ring-data:/data # Generated by gen-passwd.sh (holds the broker mqtt_url); the token/state lives in ring-data. - ./docker/ring-mqtt/config.json:/data/config.json:ro ports: # ring-mqtt setup/token web UI. Port may differ by ring-mqtt version — adjust if needed. - "${RING_UI_PORT:-55123}:55123" restart: unless-stopped # Discovery sidecar — host network so mDNS/SSDP multicast reaches the smart-home VLAN. discovery: build: context: ./sidecar dockerfile: Dockerfile network_mode: host cap_add: [NET_RAW] environment: MQTT_HOST: 127.0.0.1 MQTT_PORT: ${MQTT_HOST_PORT:-1883} MQTT_USERNAME: sidecar MQTT_PASSWORD: ${MQTT_SIDECAR_PASSWORD} restart: unless-stopped volumes: db-data: redis-data: mosquitto-data: ring-data: