BMS/docker-compose.prod.yml
megaproxy de9b8c51bd Add Unraid deployment via the Portainer API
ops/deploy.py ships each service's build context to the remote Docker
daemon's /build endpoint and pushes docker-compose.prod.yml as a Portainer
stack — there is no docker CLI in WSL and Unraid's SSH is closed.

The prod compose file drops the dev bind mounts and uvicorn --reload,
publishes only the frontend port (8000 is taken by Portainer's Edge
tunnel), and pins bms_net to 172.31.42.0/24 because the host's default
address pools are fully subnetted. Mosquitto's config is baked into an
image since the repo is not checked out on the host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:43:00 +01:00

116 lines
4 KiB
YAML

# Production stack — deployed to the Unraid Docker host as a Portainer stack.
#
# Differences from docker-compose.yml (the local dev file):
# * No build: sections. ops/deploy.py builds the four images on the Unraid daemon
# first; this file only references them by tag.
# * No source bind mounts and no uvicorn --reload. Code lives in the image.
# * No env_file. Everything is set inline — there are no .env files on the host.
# * Only the frontend publishes a host port. Postgres and MQTT stay on the
# internal bms_net network, as the README's reverse-proxy note recommends.
# * Postgres data is bind-mounted to /mnt/user/appdata/bms/db so it lands on the
# Unraid array rather than growing inside docker.img.
#
# Deploy / redeploy with: python3 ops/deploy.py
services:
# ── MQTT Broker ──────────────────────────────────────────────────
mqtt:
image: bms-mqtt:latest
container_name: bms_mqtt
restart: unless-stopped
networks: [bms_net]
healthcheck:
test: ["CMD-SHELL", "mosquitto_sub -t '$$SYS/#' -C 1 -i healthcheck -W 3"]
interval: 10s
timeout: 5s
retries: 5
# ── PostgreSQL + TimescaleDB ─────────────────────────────────────
db:
image: timescale/timescaledb:latest-pg16
container_name: bms_db
restart: unless-stopped
networks: [bms_net]
environment:
POSTGRES_USER: dcim
POSTGRES_PASSWORD: dcim_pass
POSTGRES_DB: dcim
volumes:
- /mnt/user/appdata/bms/db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dcim -d dcim"]
interval: 10s
timeout: 5s
retries: 5
# ── FastAPI backend ──────────────────────────────────────────────
backend:
image: bms-backend:latest
container_name: bms_backend
restart: unless-stopped
networks: [bms_net]
environment:
DATABASE_URL: postgresql+asyncpg://dcim:dcim_pass@db:5432/dcim
MQTT_HOST: mqtt
MQTT_PORT: "1883"
CORS_ORIGINS: "[]"
DEBUG: "false"
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
# ── Simulator bots (seed first, then run bots) ───────────────────
simulators:
image: bms-simulators:latest
container_name: bms_simulators
restart: unless-stopped
networks: [bms_net]
environment:
MQTT_HOST: mqtt
MQTT_PORT: "1883"
DATABASE_URL: postgresql://dcim:dcim_pass@db:5432/dcim
SEED_MINUTES: "30"
depends_on:
db:
condition: service_healthy
mqtt:
condition: service_healthy
backend:
condition: service_healthy
# ── Next.js frontend (the only publicly reachable service) ───────
frontend:
image: bms-frontend:latest
container_name: bms_frontend
restart: unless-stopped
networks: [bms_net]
ports:
- "5646:5646"
environment:
PORT: "5646"
HOSTNAME: "0.0.0.0"
NODE_ENV: production
BACKEND_INTERNAL_URL: http://backend:8000
depends_on:
- backend
networks:
# The Unraid daemon's default address pools are fully subnetted — 24 existing
# stacks have claimed every 172.17-172.31/16 and 192.168.x/20 slot, so letting
# Docker auto-allocate fails with "all predefined address pools have been fully
# subnetted". An explicit subnet bypasses the allocator entirely.
# 172.31.42.0/24 is free: the only other 172.31 tenant is wg0 on 172.31.200.0/24.
bms_net:
driver: bridge
ipam:
config:
- subnet: 172.31.42.0/24