services: timescaledb: image: timescale/timescaledb:latest-pg16 restart: unless-stopped command: ["postgres", "-c", "max_connections=300"] environment: POSTGRES_DB: ${POSTGRES_DB:-meshcore} POSTGRES_USER: ${POSTGRES_USER:-meshcore} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required} volumes: - timescaledb_data:/var/lib/postgresql/data - ./backend/src/db/schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-meshcore} -d ${POSTGRES_DB:-meshcore}"] interval: 10s timeout: 5s retries: 10 start_period: 30s mosquitto: image: eclipse-mosquitto:2 restart: unless-stopped volumes: - ./mosquitto:/mosquitto/config - mosquitto_data:/mosquitto/data - mosquitto_log:/mosquitto/log redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 backend: build: context: . dockerfile: Dockerfile.backend restart: unless-stopped environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} MQTT_BROKER_URL: ${MQTT_BROKER_URL:-ws://mosquitto:9001} MQTT_USERNAME: ${MQTT_USERNAME:-backend} MQTT_PASSWORD: ${MQTT_PASSWORD} REDIS_URL: ${REDIS_URL:-redis://redis:6379} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required} OPENTOPODATA_API: ${OPENTOPODATA_API:-https://api.opentopodata.org} PORT: ${PORT:-3000} MESHCORE_CHANNEL_SECRETS: ${MESHCORE_CHANNEL_SECRETS:-} OWNER_DATABASE_URL: ${OWNER_DATABASE_URL:-postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${OWNER_POSTGRES_DB:-meshcore_owner_auth}} OWNER_MQTT_USERNAME_MAP: ${OWNER_MQTT_USERNAME_MAP:-} OWNER_COOKIE_SECRET: ${OWNER_COOKIE_SECRET:-} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3001,http://localhost:3002,https://ukmesh.com,https://www.ukmesh.com,https://app.ukmesh.com,https://teessidemesh.com,https://www.teessidemesh.com,https://app.teessidemesh.com,https://test.ukmesh.com} RADIO_BOT_URL: ${RADIO_BOT_URL:-} COVERAGE_MODEL_VERSION: ${COVERAGE_MODEL_VERSION:-5} NODE_ENV: production depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy mosquitto: condition: service_started ports: - "3000:3000" volumes: - mosquitto_log:/mosquitto/log:ro - ./mosquitto:/mosquitto/config - /var/run/docker.sock:/var/run/docker.sock acl-watcher: build: context: . dockerfile: Dockerfile.backend restart: unless-stopped command: ["node", "dist/workers/acl-watcher.js"] environment: MOSQUITTO_ACL_PATH: /mosquitto/config/acl MOSQUITTO_CONTAINER_NAME: mosquitto NODE_ENV: production depends_on: - mosquitto volumes: - ./mosquitto:/mosquitto/config - /var/run/docker.sock:/var/run/docker.sock path-learning-worker: build: context: . dockerfile: Dockerfile.backend restart: unless-stopped command: ["node", "dist/workers/path-learning.js"] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} REDIS_URL: ${REDIS_URL:-redis://redis:6379} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required} NODE_ENV: production depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy path-history-worker: build: context: . dockerfile: Dockerfile.backend restart: unless-stopped command: ["node", "dist/workers/path-history.js"] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} REDIS_URL: ${REDIS_URL:-redis://redis:6379} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required} NODE_ENV: production DATABASE_STATEMENT_TIMEOUT_MS: 300000 depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy health-worker: build: context: . dockerfile: Dockerfile.backend restart: unless-stopped command: ["node", "dist/workers/health.js"] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} REDIS_URL: ${REDIS_URL:-redis://redis:6379} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required} NODE_ENV: production depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy link-backfill-worker: build: context: . dockerfile: Dockerfile.backend restart: "no" command: ["node", "dist/workers/link-backfill.js"] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} MQTT_BROKER_URL: ${MQTT_BROKER_URL:-ws://mosquitto:9001} REDIS_URL: ${REDIS_URL:-redis://redis:6379} MESHCORE_CHANNEL_SECRETS: ${MESHCORE_CHANNEL_SECRETS:-} NODE_ENV: production depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy backend: condition: service_started viewshed-worker: build: context: ./viewshed-worker dockerfile: Dockerfile restart: unless-stopped environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} REDIS_URL: ${REDIS_URL:-redis://redis:6379} SRTM_DIR: /data/srtm WORKER_MODE: viewshed NUM_WORKERS: '1' COVERAGE_MODEL: ${COVERAGE_MODEL:-rf_radial_100m} COVERAGE_MODEL_VERSION: ${COVERAGE_MODEL_VERSION:-5} volumes: - srtm_data:/data/srtm deploy: resources: limits: cpus: '1.5' memory: '5g' depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy link-worker: build: context: ./viewshed-worker dockerfile: Dockerfile restart: unless-stopped cpus: 2.0 environment: DATABASE_URL: postgresql://${POSTGRES_USER:-meshcore}:${POSTGRES_PASSWORD}@timescaledb:5432/${POSTGRES_DB:-meshcore} REDIS_URL: ${REDIS_URL:-redis://redis:6379} RADIO_BOT_URL: ${RADIO_BOT_URL:-http://meshcore-radio-bot:3011} SRTM_DIR: /data/srtm WORKER_MODE: link NUM_WORKERS: '1' volumes: - srtm_data:/data/srtm depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy website-teesside: build: context: . dockerfile: Dockerfile.website args: VITE_APP_HOSTNAME: app.teessidemesh.com VITE_SITE: teesside restart: unless-stopped ports: - "3002:80" depends_on: - backend app-ukmesh: build: context: . dockerfile: Dockerfile.app args: VITE_APP_HOSTNAME: app.ukmesh.com VITE_NETWORK: ukmesh VITE_SITE: ukmesh restart: unless-stopped depends_on: - backend ports: - "3003:80" website-ukmesh: build: context: . dockerfile: Dockerfile.website args: VITE_APP_HOSTNAME: app.ukmesh.com VITE_SITE: ukmesh restart: unless-stopped ports: - "3004:80" depends_on: - backend app-dev: build: context: . dockerfile: Dockerfile.app args: VITE_APP_HOSTNAME: ${VITE_DEV_APP_HOSTNAME:-} VITE_NETWORK: test VITE_SITE: dev VITE_OBSERVER_ID: ${VITE_DEV_OBSERVER_ID:-} VITE_SITE_DISPLAY_NAME: ${VITE_DEV_SITE_DISPLAY_NAME:-UK Mesh Test} VITE_SITE_FOOTER_NAME: ${VITE_DEV_SITE_FOOTER_NAME:-UK Mesh Test} VITE_SITE_APP_URL: ${VITE_DEV_SITE_APP_URL:-https://test.ukmesh.com} VITE_SITE_HOME_URL: ${VITE_DEV_SITE_HOME_URL:-https://test.ukmesh.com} restart: unless-stopped depends_on: - backend ports: - "3005:80" website-dev: build: context: . dockerfile: Dockerfile.website args: VITE_APP_HOSTNAME: ${VITE_DEV_APP_HOSTNAME:-app-dev.ukmesh.com} VITE_NETWORK: test VITE_SITE: dev VITE_OBSERVER_ID: ${VITE_DEV_OBSERVER_ID:-} VITE_SITE_DISPLAY_NAME: ${VITE_DEV_SITE_DISPLAY_NAME:-UK Mesh Test} VITE_SITE_FOOTER_NAME: ${VITE_DEV_SITE_FOOTER_NAME:-UK Mesh Test} VITE_SITE_APP_URL: ${VITE_DEV_SITE_APP_URL:-https://test.ukmesh.com} VITE_SITE_HOME_URL: ${VITE_DEV_SITE_HOME_URL:-https://test.ukmesh.com} restart: unless-stopped ports: - "3006:80" depends_on: - backend cloudflared: image: cloudflare/cloudflared:latest restart: unless-stopped command: tunnel run profiles: [tunnel] environment: TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN} depends_on: - backend - app-ukmesh - website-ukmesh - website-teesside - app-dev - website-dev - mosquitto loki: image: grafana/loki:3.4 restart: unless-stopped command: -config.file=/etc/loki/loki.yaml volumes: - ./logging/loki.yaml:/etc/loki/loki.yaml:ro - loki_data:/loki promtail: image: grafana/promtail:3.4 restart: unless-stopped command: -config.file=/etc/promtail/promtail.yaml volumes: - ./logging/promtail.yaml:/etc/promtail/promtail.yaml:ro - /var/run/docker.sock:/var/run/docker.sock:ro depends_on: - loki grafana: image: grafana/grafana:11.5.0 restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD required} GF_USERS_ALLOW_SIGN_UP: "false" GF_AUTH_ANONYMOUS_ENABLED: "false" volumes: - ./logging/grafana/provisioning:/etc/grafana/provisioning:ro - ./logging/grafana/dashboards:/etc/grafana/dashboards:ro - grafana_data:/var/lib/grafana ports: - "3010:3000" depends_on: - loki volumes: timescaledb_data: mosquitto_data: mosquitto_log: redis_data: srtm_data: loki_data: grafana_data: