225 lines
9.7 KiB
YAML
225 lines
9.7 KiB
YAML
# TRM platform — deployment stack
|
||
#
|
||
# Deployed via Portainer Repository Stack:
|
||
# Repository: git.dev.microservices.al/trm/deploy
|
||
# Compose path: compose.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.
|
||
#
|
||
# Before first deploy on the host: `docker login git.dev.microservices.al`
|
||
# (Portainer can store registry credentials in its UI; configure once.)
|
||
#
|
||
# Environment variables are populated from Portainer's stack environment
|
||
# config (or a `.env` file alongside this compose for non-Portainer hosts).
|
||
# Defaults are provided via `${VAR:-default}` so the stack starts with no
|
||
# explicit configuration on a fresh deploy.
|
||
|
||
name: trm
|
||
|
||
services:
|
||
# -------------------------------------------------------------------
|
||
# Redis — telemetry queue + (future) connection registry for Phase 2.
|
||
# Internal-only; no host port mapping.
|
||
# -------------------------------------------------------------------
|
||
redis:
|
||
image: redis:7-alpine
|
||
expose:
|
||
- '6379'
|
||
volumes:
|
||
- redis-data:/data
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ['CMD', 'redis-cli', 'ping']
|
||
interval: 10s
|
||
timeout: 3s
|
||
retries: 5
|
||
|
||
# -------------------------------------------------------------------
|
||
# tcp-ingestion — Teltonika telemetry TCP server.
|
||
# Built by git.dev.microservices.al/trm/tcp-ingestion's Gitea workflow.
|
||
# -------------------------------------------------------------------
|
||
tcp-ingestion:
|
||
image: git.dev.microservices.al/trm/tcp-ingestion:${TCP_INGESTION_TAG:-main}
|
||
depends_on:
|
||
redis:
|
||
condition: service_healthy
|
||
ports:
|
||
# Devices connect to this port. Use `${HOST_BIND_IP:-0.0.0.0}:5027:5027`
|
||
# if you want to restrict which host interface accepts connections.
|
||
- '${TCP_INGESTION_PORT:-5027}:5027'
|
||
environment:
|
||
NODE_ENV: production
|
||
INSTANCE_ID: ${TCP_INGESTION_INSTANCE_ID:-stage-1}
|
||
REDIS_URL: redis://redis:6379
|
||
# Pinned explicitly here to keep tcp-ingestion and processor in sync;
|
||
# neither service's compiled default is authoritative — the deploy
|
||
# stack is the single source of truth for the stream name.
|
||
REDIS_TELEMETRY_STREAM: ${REDIS_TELEMETRY_STREAM:-telemetry:teltonika}
|
||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||
restart: unless-stopped
|
||
|
||
# -------------------------------------------------------------------
|
||
# postgres — PostgreSQL 16 with TimescaleDB + PostGIS extensions
|
||
# (and others) bundled. Image: timescale/timescaledb-ha, `-all` suffix
|
||
# = all extensions present and ready for `CREATE EXTENSION`.
|
||
#
|
||
# Schema is owned by Directus (when it lands); the `positions`
|
||
# hypertable is owned by the processor's migration runner.
|
||
# Internal-only; no host port mapping.
|
||
#
|
||
# The image tag should be reviewed every 3–6 months. Pick the latest
|
||
# stable `pg16-ts*-all` build from Docker Hub:
|
||
# https://hub.docker.com/r/timescale/timescaledb-ha/tags
|
||
# Pin to a specific tag (not rolling `pg16`) so deploys are reproducible.
|
||
# -------------------------------------------------------------------
|
||
postgres:
|
||
image: timescale/timescaledb-ha:pg16.6-ts2.17.2-all
|
||
expose:
|
||
- '5432'
|
||
volumes:
|
||
- postgres-data:/home/postgres/pgdata/data
|
||
environment:
|
||
POSTGRES_USER: ${POSTGRES_USER:-trm}
|
||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-trm-pilot-change-me}
|
||
POSTGRES_DB: ${POSTGRES_DB:-trm}
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-trm} -d ${POSTGRES_DB:-trm}']
|
||
interval: 10s
|
||
timeout: 3s
|
||
retries: 5
|
||
|
||
# -------------------------------------------------------------------
|
||
# processor — consumes telemetry from Redis, writes to Postgres.
|
||
# Built by git.dev.microservices.al/trm/processor's Gitea workflow.
|
||
# -------------------------------------------------------------------
|
||
processor:
|
||
image: git.dev.microservices.al/trm/processor:${PROCESSOR_TAG:-main}
|
||
depends_on:
|
||
redis:
|
||
condition: service_healthy
|
||
postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
NODE_ENV: production
|
||
INSTANCE_ID: ${PROCESSOR_INSTANCE_ID:-processor-1}
|
||
REDIS_URL: redis://redis:6379
|
||
# Must match tcp-ingestion's REDIS_TELEMETRY_STREAM — both pinned
|
||
# 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}
|
||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||
restart: unless-stopped
|
||
|
||
# -------------------------------------------------------------------
|
||
# directus — business-plane API, admin UI, and schema authority.
|
||
# Built by git.dev.microservices.al/trm/directus's Gitea workflow.
|
||
#
|
||
# Boot pipeline (5 steps; see trm/directus/entrypoint.sh):
|
||
# 1. db-init pre-schema → positions hypertable + faulty column
|
||
# 2. directus bootstrap → installs Directus system tables
|
||
# 3. directus schema apply → applies snapshots/schema.yaml
|
||
# 4. db-init post-schema → composite UNIQUE constraints
|
||
# 5. pm2-runtime start → server up at :8055
|
||
#
|
||
# First-boot on a fresh DB takes ~60–90 s (Directus runs its own
|
||
# internal migrations during step 2). Subsequent boots are ~5 s as
|
||
# all steps no-op against the warm DB.
|
||
#
|
||
# Schema-as-code: collections + fields + relations live in the image
|
||
# (snapshots/schema.yaml + db-init/*.sql baked in at build time).
|
||
# Schema changes flow through the trm/directus repo + its CI dry-run
|
||
# gate, NOT through manual edits on this stage instance. Editing
|
||
# 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.
|
||
# -------------------------------------------------------------------
|
||
directus:
|
||
image: git.dev.microservices.al/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.
|
||
#
|
||
# Why not host-publish 8055 directly: the admin UI is a privileged
|
||
# surface (full CRUD + permission policies + Flow execution). Direct
|
||
# exposure leaks an attack surface and forces TLS into a service that
|
||
# shouldn't care about it. tcp-ingestion is different (GPS devices
|
||
# connect directly so it must publish to the host); Directus is HTTP
|
||
# and belongs behind a proxy in any non-throwaway environment.
|
||
- '8055'
|
||
environment:
|
||
# ----- Database connection -----
|
||
DB_CLIENT: pg
|
||
DB_HOST: postgres
|
||
DB_PORT: 5432
|
||
DB_DATABASE: ${POSTGRES_DB:-trm}
|
||
DB_USER: ${POSTGRES_USER:-trm}
|
||
DB_PASSWORD: ${POSTGRES_PASSWORD:-trm-pilot-change-me}
|
||
|
||
# ----- 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.
|
||
KEY: ${DIRECTUS_KEY:-REPLACE-ME-WITH-A-UUID}
|
||
SECRET: ${DIRECTUS_SECRET:-REPLACE-ME-WITH-A-LONG-RANDOM-STRING}
|
||
|
||
# ----- Admin bootstrap — only used on first init.
|
||
# If directus_users is empty at first boot, an admin user is created
|
||
# from these. Subsequent boots ignore them. Change the password via
|
||
# the admin UI after first login.
|
||
ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL:-admin@example.com}
|
||
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://<your-domain>; default localhost is just
|
||
# for first-deploy smoke testing.
|
||
PUBLIC_URL: ${DIRECTUS_PUBLIC_URL:-http://localhost:8055}
|
||
|
||
# ----- Logging -----
|
||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||
LOG_STYLE: ${LOG_STYLE:-json}
|
||
|
||
# ----- WebSockets — required for the live channel architecture
|
||
# (Directus's WS subs cover business-plane events; processor's WS
|
||
# carries the telemetry firehose). See live-channel-architecture
|
||
# in the wiki.
|
||
WEBSOCKETS_ENABLED: 'true'
|
||
|
||
# ----- Cache / CORS — defaults disabled; enable per environment.
|
||
CACHE_ENABLED: ${DIRECTUS_CACHE_ENABLED:-false}
|
||
CORS_ENABLED: ${DIRECTUS_CORS_ENABLED:-false}
|
||
CORS_ORIGIN: ${DIRECTUS_CORS_ORIGIN:-false}
|
||
volumes:
|
||
# Persist admin-uploaded files across container restarts.
|
||
# snapshots/ + db-init/ are baked into the image, NOT mounted —
|
||
# that's the schema-as-code split.
|
||
- directus-uploads:/directus/uploads
|
||
restart: unless-stopped
|
||
healthcheck:
|
||
test: ['CMD-SHELL', 'wget -qO- http://localhost:8055/server/health || exit 1']
|
||
interval: 30s
|
||
timeout: 10s
|
||
# First boot includes Directus's internal migrations (~30–45 s on
|
||
# fresh DB). 120 s gives margin; warm boots become healthy in ~10 s.
|
||
start_period: 120s
|
||
retries: 3
|
||
|
||
# -------------------------------------------------------------------
|
||
# Future services land here:
|
||
# - react-spa: front-end (static, served via nginx or Caddy)
|
||
# See ../docs/wiki/ for the platform architecture.
|
||
# -------------------------------------------------------------------
|
||
|
||
volumes:
|
||
redis-data:
|
||
postgres-data:
|
||
directus-uploads:
|