Files
deploy/compose.yaml
T

225 lines
9.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 36 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 ~6090 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 (~3045 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: