diff --git a/compose.yaml b/compose.dev.yaml similarity index 60% rename from compose.yaml rename to compose.dev.yaml index acc5c7d..5f72601 100644 --- a/compose.yaml +++ b/compose.dev.yaml @@ -1,20 +1,30 @@ -# TRM platform — deployment stack +# TRM platform — deployment stack (dev environment) # -# Deployed via Portainer Repository Stack: -# Repository: git.dev.microservices.al/trm/deploy -# Compose path: compose.yaml +# Deployed via Komodo Stack-from-Git: +# Repository: git.dev.trmtracking.org/trm/deploy +# Compose path: compose.dev.yaml # Branch: main # -# Images are built and pushed by each service's own Gitea workflow. -# This file references them by tag and runs them as a coordinated stack. +# Images are built and pushed by each service's own Gitea Actions workflow +# (see each repo's .gitea/workflows/build.yml). This file references the +# resulting images by tag and runs them as a coordinated stack. # -# Before first deploy on the host: `docker login git.dev.microservices.al` -# (Portainer can store registry credentials in its UI; configure once.) +# Before first deploy on the host: `docker login git.dev.trmtracking.org` +# (or configure registry credentials in Komodo's Image Registry Account). # -# Environment variables are populated from Portainer's stack environment -# config (or a `.env` file alongside this compose for non-Portainer hosts). +# Environment variables are populated from Komodo's per-stack secrets +# config (or a `.env` file alongside this compose for non-Komodo hosts). # Defaults are provided via `${VAR:-default}` so the stack starts with no # explicit configuration on a fresh deploy. +# +# Routing — fronted by Traefik on the host (single origin model required by +# the SPA's session cookie + Processor WebSocket upgrade; see +# react-spa.md and processor-ws-contract.md): +# app.dev.trmtracking.org/ → spa (catch-all SPA bundle) +# app.dev.trmtracking.org/api/* → directus (REST/GraphQL, /api stripped) +# app.dev.trmtracking.org/ws-business → directus (Directus's WS, served at this path natively) +# app.dev.trmtracking.org/ws-live → processor (live telemetry WS, /ws-live stripped) +# api.dev.trmtracking.org/ → directus (admin UI on its own subdomain) name: trm @@ -38,10 +48,10 @@ services: # ------------------------------------------------------------------- # tcp-ingestion — Teltonika telemetry TCP server. - # Built by git.dev.microservices.al/trm/tcp-ingestion's Gitea workflow. + # Built by git.dev.trmtracking.org/trm/tcp-ingestion's Gitea workflow. # ------------------------------------------------------------------- tcp-ingestion: - image: git.dev.microservices.al/trm/tcp-ingestion:${TCP_INGESTION_TAG:-main} + image: git.dev.trmtracking.org/trm/tcp-ingestion:${TCP_INGESTION_TAG:-main} depends_on: redis: condition: service_healthy @@ -92,16 +102,23 @@ services: retries: 5 # ------------------------------------------------------------------- - # processor — consumes telemetry from Redis, writes to Postgres. - # Built by git.dev.microservices.al/trm/processor's Gitea workflow. + # processor — consumes telemetry from Redis, writes to Postgres, and + # (post Phase 1.5) broadcasts live positions over its own WebSocket. + # Built by git.dev.trmtracking.org/trm/processor's Gitea workflow. + # + # Routed by Traefik at app.dev.trmtracking.org/ws-live (the host strips + # the prefix; processor hosts its WS server at root). The route is + # ready in advance of the live-broadcast feature landing. # ------------------------------------------------------------------- processor: - image: git.dev.microservices.al/trm/processor:${PROCESSOR_TAG:-main} + image: git.dev.trmtracking.org/trm/processor:${PROCESSOR_TAG:-main} depends_on: redis: condition: service_healthy postgres: condition: service_healthy + expose: + - '8081' environment: NODE_ENV: production INSTANCE_ID: ${PROCESSOR_INSTANCE_ID:-processor-1} @@ -110,12 +127,25 @@ services: # to the same shared variable so they cannot drift. REDIS_TELEMETRY_STREAM: ${REDIS_TELEMETRY_STREAM:-telemetry:teltonika} POSTGRES_URL: postgres://${POSTGRES_USER:-trm}:${POSTGRES_PASSWORD:-trm-pilot-change-me}@postgres:5432/${POSTGRES_DB:-trm} + LIVE_WS_PORT: ${LIVE_WS_PORT:-8081} LOG_LEVEL: ${LOG_LEVEL:-info} + networks: + - default + - proxy restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.trm-processor-live.rule=Host(`app.dev.trmtracking.org`) && PathPrefix(`/ws-live`)" + - "traefik.http.routers.trm-processor-live.entrypoints=websecure" + - "traefik.http.routers.trm-processor-live.tls.certresolver=le" + - "traefik.http.routers.trm-processor-live.middlewares=trm-strip-ws-live" + - "traefik.http.middlewares.trm-strip-ws-live.stripprefix.prefixes=/ws-live" + - "traefik.http.services.trm-processor-live.loadbalancer.server.port=8081" # ------------------------------------------------------------------- # directus — business-plane API, admin UI, and schema authority. - # Built by git.dev.microservices.al/trm/directus's Gitea workflow. + # Built by git.dev.trmtracking.org/trm/directus's Gitea workflow. # # Boot pipeline (5 steps; see trm/directus/entrypoint.sh): # 1. db-init pre-schema → positions hypertable + faulty column @@ -135,18 +165,23 @@ services: # collections via the admin UI here will be DROPPED on the next image # rebuild — schema-apply enforces the committed snapshot. See # docs/wiki/entities/directus.md "destructive-apply hazard" callout. + # + # Routing: two Traefik routers point at the same backend. + # api.dev.trmtracking.org/* → admin UI + direct API access + # app.dev.trmtracking.org/api/* → SPA's REST/GraphQL surface (prefix stripped) + # app.dev.trmtracking.org/ws-business → SPA's business-plane WebSocket + # The /ws-business path is served by Directus natively via + # WEBSOCKETS_REST_PATH below; no Traefik rewrite is needed. # ------------------------------------------------------------------- directus: - image: git.dev.microservices.al/trm/directus:${DIRECTUS_TAG:-main} + image: git.dev.trmtracking.org/trm/directus:${DIRECTUS_TAG:-main} depends_on: postgres: condition: service_healthy expose: # Internal-only. The admin UI + API are reachable from other services - # in the stack via service-name DNS (`http://directus:8055`). A reverse - # proxy (Traefik / Caddy / nginx) running on the host or attached to - # the `trm_default` network terminates TLS, applies its own auth / - # rate-limit / WAF rules, and forwards to this expose port. + # in the stack via service-name DNS (`http://directus:8055`). Traefik + # on the host fronts both public hostnames and forwards to this port. # # Why not host-publish 8055 directly: the admin UI is a privileged # surface (full CRUD + permission policies + Flow execution). Direct @@ -167,7 +202,7 @@ services: # ----- Instance security — REQUIRED, must be unique per environment. # KEY: any UUID. SECRET: long random string, e.g. `openssl rand -hex 64`. # Two instances sharing the same KEY/SECRET produce colliding JWTs. - # Defaults below are placeholders — REPLACE in the Portainer stack env. + # Defaults below are placeholders — REPLACE in Komodo's stack secrets. KEY: ${DIRECTUS_KEY:-REPLACE-ME-WITH-A-UUID} SECRET: ${DIRECTUS_SECRET:-REPLACE-ME-WITH-A-LONG-RANDOM-STRING} @@ -179,9 +214,9 @@ services: ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD:-CHANGE-ON-FIRST-LOGIN} # ----- Public-facing URL (used in emails, OAuth redirects, asset URLs). - # In real prod set to https://; default localhost is just - # for first-deploy smoke testing. - PUBLIC_URL: ${DIRECTUS_PUBLIC_URL:-http://localhost:8055} + # Default = the admin canonical hostname so password-reset links land + # in the admin UI, not the SPA. + PUBLIC_URL: ${DIRECTUS_PUBLIC_URL:-https://api.dev.trmtracking.org} # ----- Logging ----- LOG_LEVEL: ${LOG_LEVEL:-info} @@ -192,6 +227,9 @@ services: # carries the telemetry firehose). See live-channel-architecture # in the wiki. WEBSOCKETS_ENABLED: 'true' + # Serve Directus's WS at the path the SPA expects under app.dev, + # avoiding a Traefik rewrite (cookies + upgrade headers stay clean). + WEBSOCKETS_REST_PATH: /ws-business # ----- Cache / CORS — defaults disabled; enable per environment. CACHE_ENABLED: ${DIRECTUS_CACHE_ENABLED:-false} @@ -202,7 +240,33 @@ services: # snapshots/ + db-init/ are baked into the image, NOT mounted — # that's the schema-as-code split. - directus-uploads:/directus/uploads + networks: + - default + - proxy restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + # Admin UI on its own subdomain (full surface, root path). + - "traefik.http.routers.trm-directus-admin.rule=Host(`api.dev.trmtracking.org`)" + - "traefik.http.routers.trm-directus-admin.entrypoints=websecure" + - "traefik.http.routers.trm-directus-admin.tls.certresolver=le" + - "traefik.http.routers.trm-directus-admin.service=trm-directus" + # SPA-side REST/GraphQL surface — strip /api prefix. + - "traefik.http.routers.trm-directus-api.rule=Host(`app.dev.trmtracking.org`) && PathPrefix(`/api`)" + - "traefik.http.routers.trm-directus-api.entrypoints=websecure" + - "traefik.http.routers.trm-directus-api.tls.certresolver=le" + - "traefik.http.routers.trm-directus-api.service=trm-directus" + - "traefik.http.routers.trm-directus-api.middlewares=trm-strip-api" + - "traefik.http.middlewares.trm-strip-api.stripprefix.prefixes=/api" + # SPA-side business-plane WebSocket — Directus serves at /ws-business + # natively (WEBSOCKETS_REST_PATH above), so no rewrite middleware. + - "traefik.http.routers.trm-directus-wsbiz.rule=Host(`app.dev.trmtracking.org`) && PathPrefix(`/ws-business`)" + - "traefik.http.routers.trm-directus-wsbiz.entrypoints=websecure" + - "traefik.http.routers.trm-directus-wsbiz.tls.certresolver=le" + - "traefik.http.routers.trm-directus-wsbiz.service=trm-directus" + # Single backend service shared by all three routers above. + - "traefik.http.services.trm-directus.loadbalancer.server.port=8055" healthcheck: test: ['CMD-SHELL', 'wget -qO- http://localhost:8055/server/health || exit 1'] interval: 30s @@ -214,21 +278,23 @@ services: # ------------------------------------------------------------------- # spa — React + TypeScript single-page app for end-user operators. - # Built by git.dev.microservices.al/trm/spa's Gitea workflow. + # Built by git.dev.trmtracking.org/trm/spa's Gitea workflow. # # The image is nginx serving a static bundle. The bundle's runtime # config (Directus URL, Processor WS URL, optional Google Maps key, # env label) is read at first paint from /config.json — overridable # via the volume mount below without rebuilding the image. # - # Internal-only on the stack (expose: 80). The reverse proxy that - # fronts directus also routes the SPA at the public domain root. + # Routed as the catch-all on app.dev.trmtracking.org. The /api, + # /ws-business, and /ws-live PathPrefix routers above bind first; + # everything else falls through to this router. + # # Same-origin is non-negotiable: the SPA's auth cookie + the # processor's WebSocket upgrade both rely on browser-attached cookies # that only flow within one origin. # ------------------------------------------------------------------- spa: - image: git.dev.microservices.al/trm/spa:${SPA_TAG:-main} + image: git.dev.trmtracking.org/trm/spa:${SPA_TAG:-main} expose: - '80' volumes: @@ -238,7 +304,16 @@ services: # edit the URLs / env label / optional Google Maps key. Read-only # mount — the container can't accidentally write to its own config. - ${SPA_CONFIG_FILE:-./spa-config.json}:/usr/share/nginx/html/config.json:ro + networks: + - proxy restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy" + - "traefik.http.routers.trm-spa.rule=Host(`app.dev.trmtracking.org`)" + - "traefik.http.routers.trm-spa.entrypoints=websecure" + - "traefik.http.routers.trm-spa.tls.certresolver=le" + - "traefik.http.services.trm-spa.loadbalancer.server.port=80" healthcheck: test: ['CMD-SHELL', 'wget -qO- http://localhost/ || exit 1'] interval: 30s @@ -250,3 +325,8 @@ volumes: redis-data: postgres-data: directus-uploads: + +networks: + default: {} + proxy: + external: true