Port Standardization (NEM-3148)¶
Overview¶
Port configuration is standardized across all environments (development and Docker). The same internal ports are used for service-to-service communication in both environments - only the hostname/network location changes.
Status note: This document originated with NEM-3148 (per-model AI services on 8091-8096). The vision models have since been consolidated into a single
ai-gatewayservice (port 8090, routers/yolo26/florence>/clip/enrichment/enrich-lt; seeai/gateway/main.pyanddocker-compose.prod.yml). The port model below reflects the current tree;.env.exampleanddocker-compose.prod.ymlremain the source of truth.
Principle: "Build Once, Deploy Anywhere"¶
This standardization ensures:
- Consistency: Same port numbers across all environments
- Predictability: No surprises when moving from dev to production
- Simplicity: Configuration files don't need environment-specific port rewrites
- Reduced errors: No accidental hardcoded port mismatches
Standardized Ports¶
Internal Service Ports (Constant - Never Change)¶
These ports are used for service-to-service communication and remain the same in all environments:
| Service | Port | Purpose | Network Location |
|---|---|---|---|
| Backend API | 8000 | FastAPI server | Development: localhost:8000 / Docker: backend:8000 |
| PostgreSQL | 5432 | Database | Development: localhost:5432 / Docker: postgres:5432 |
| Redis | 6379 | Cache & Queue | Development: localhost:6379 / Docker: redis:6379 |
| AI Gateway | 8090 | Vision models (all routers) | Development: localhost:8090 / Docker: ai-gateway:8090 |
| Nemotron LLM | 8091 | LLM Reasoning | Development: localhost:8091 / Docker: ai-llm:8091 |
| AI Gateway metrics | 8002 | Prometheus metrics | Docker: ai-gateway:8002 |
The old per-model ports (YOLO26 8095, Florence-2 8092, CLIP 8093, Enrichment 8094, Enrichment-light 8096) survive only as host-port variables in .env.example for standalone model servers (ai/yolo26/, ai/florence/, etc., still used for export/benchmark tooling). In production compose all model traffic goes through the gateway:
# .env.example (current)
AI_GATEWAY_PORT=8090
AI_GATEWAY_URL=http://ai-gateway:8090
USE_AI_GATEWAY=true
YOLO26_URL=http://localhost:8090/yolo26
FLORENCE_URL=http://localhost:8090/florence
CLIP_URL=http://localhost:8090/clip
ENRICHMENT_URL=http://localhost:8090/enrichment
NEMOTRON_URL=http://localhost:8091
External/Host Ports (Configurable)¶
These ports expose services to external hosts and can vary based on availability:
| Service | Default | Purpose | External Access |
|---|---|---|---|
| Frontend HTTP | 8080 | Web UI | http://localhost:8080, configurable via FRONTEND_HTTP_PORT |
| Frontend HTTPS | 8444 | Web UI (SSL) | https://localhost:8444, configurable via FRONTEND_HTTPS_PORT |
| Grafana | 3002 | Dashboards | http://localhost:3002, configurable via GRAFANA_PORT |
| Prometheus | 9090 | Metrics | http://localhost:9090, configurable |
| Alertmanager | 9093 | Alerts | http://localhost:9093, configurable |
| Redis Exporter | 9121 | Redis metrics | http://localhost:9121, configurable |
| JSON Exporter | 7979 | JSON metrics | http://localhost:7979, configurable |
Frontend Port Notes:
- Production containers (docker-compose.prod.yml): nginx serves the React app on internal ports 8080 (HTTP) and 8443 (HTTPS), mapped to host ports
FRONTEND_HTTP_PORT(8080) andFRONTEND_HTTPS_PORT(8444).- Local development (npm run dev): Vite dev server runs on 8444 with HTTPS (
frontend/vite.config.tsserver.port).- SSL is disabled by default in production (
SSL_ENABLEDdefaults tofalse); setSSL_ENABLED=trueto serve HTTPS with auto-generated self-signed certificates.
Environment Separation¶
Development Environment¶
Services are accessed via localhost with standard ports:
# Development .env # pragma: allowlist secret
DATABASE_URL=postgresql+asyncpg://security:password@localhost:5432/security # pragma: allowlist secret
REDIS_URL=redis://localhost:6379/0
YOLO26_URL=http://localhost:8090/yolo26
NEMOTRON_URL=http://localhost:8091
FLORENCE_URL=http://localhost:8090/florence
CLIP_URL=http://localhost:8090/clip
ENRICHMENT_URL=http://localhost:8090/enrichment
Docker Compose Environment¶
Services are accessed via container service names with the same standard internal ports:
# backend service environment (docker-compose.prod.yml) # pragma: allowlist secret
DATABASE_URL=postgresql+asyncpg://security:password@postgres:5432/security # pragma: allowlist secret
REDIS_URL=redis://redis:6379/0
AI_GATEWAY_URL=http://ai-gateway:8090
NEMOTRON_URL=http://ai-llm:8091
YOLO26_URL=http://ai-gateway:8090/yolo26
Notice: Only the hostname changes, ports remain 5432, 6379, 8090, 8091, etc.
Configuration Files¶
backend/core/config.py¶
Service URLs default to the gateway in Docker:
yolo26_url: str = Field(
default="http://ai-gateway:8090/yolo26",
# Docker: http://ai-gateway:8090/yolo26 (via AI gateway)
)
.env.example¶
The example environment file documents the standardized ports and points all model URLs at AI_GATEWAY_PORT (see the AI SERVICE PORTS section).
setup.py¶
The setup script generates environment-appropriate configuration, reading port values from .env.example as the single source of truth:
# Service metadata; ports loaded from .env.example
"yolo26": {"category": "AI", "desc": "YOLO26 object detection"},
# ... etc
docker-compose.prod.yml¶
Standard internal ports are exposed via environment variables:
ai-gateway:
ports:
- '127.0.0.1:${AI_GATEWAY_PORT:-8090}:8090'
- '127.0.0.1:${AI_GATEWAY_METRICS_PORT:-8002}:8002'
docker-compose.override.yml¶
External ports may differ, but internal container ports remain standard:
# Generated by setup.py - maps external ports to standard internal ports
postgres:
ports:
- '5433:5432' # Host 5433 -> Container 5432 (internal standard)
redis:
ports:
- '6380:6379' # Host 6380 -> Container 6379 (internal standard)
backend:
ports:
- '8000:8000' # Host 8000 -> Container 8000 (always standard)
When Ports Are Standard vs. When They Can Vary¶
Always Standard (Never Change)¶
- Backend API: 8000 (internal communication between frontend/backend)
- PostgreSQL: 5432 (internal communication with database)
- Redis: 6379 (internal communication with cache)
- AI Gateway: 8090 (internal communication with all vision models)
These ports are embedded in configuration and service discovery.
Can Vary Based on Availability¶
- Frontend HTTP/HTTPS: host ports remappable (
FRONTEND_HTTP_PORT,FRONTEND_HTTPS_PORT) - Monitoring: 3002, 9090, 9093, etc. (all configurable external ports)
These ports are for external access only and are safely remappable.
Migration Guide¶
If you have existing configurations using the pre-gateway per-model URLs:
From Older Configuration¶
# Old (per-model services) # pragma: allowlist secret
YOLO26_URL=http://ai-yolo26:8095
FLORENCE_URL=http://ai-florence:8092
CLIP_URL=http://ai-clip:8093
ENRICHMENT_URL=http://ai-enrichment:8094
To Current Configuration¶
# Current (AI gateway) # pragma: allowlist secret
USE_AI_GATEWAY=true
YOLO26_URL=http://ai-gateway:8090/yolo26
FLORENCE_URL=http://ai-gateway:8090/florence
CLIP_URL=http://ai-gateway:8090/clip
ENRICHMENT_URL=http://ai-gateway:8090/enrichment
Key Change: Remove per-model hostnames/ports from internal service URLs. Run python setup.py to generate correct configuration for your environment.
Running setup.py¶
The setup.py script automatically:
- Reads port values from
.env.example(single source of truth) - Generates correct service URLs with standard ports
- Checks for external port conflicts
- Finds alternatives for conflicting external ports (5433, 6380, etc.)
- Creates docker-compose.override.yml with proper port mappings
This ensures your configuration uses standard internal ports while automatically handling external port conflicts.
Configuration Examples¶
Development (All Services Local)¶
# All services on localhost with standard ports # pragma: allowlist secret
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/db # pragma: allowlist secret
REDIS_URL=redis://localhost:6379/0
YOLO26_URL=http://localhost:8090/yolo26
NEMOTRON_URL=http://localhost:8091
API_HOST=0.0.0.0
API_PORT=8000
Docker Compose (Service Network)¶
# Backend service environment (auto-generated) # pragma: allowlist secret
DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/db # pragma: allowlist secret
REDIS_URL=redis://redis:6379/0
YOLO26_URL=http://ai-gateway:8090/yolo26
NEMOTRON_URL=http://ai-llm:8091
Mixed Environment (Local Services + Docker)¶
# Custom setup for special cases # pragma: allowlist secret
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/db # pragma: allowlist secret
REDIS_URL=redis://redis.example.com:6379/0 # Remote redis
YOLO26_URL=http://ai-gateway:8090/yolo26 # Docker network gateway
Note: Even in mixed environments, ports remain standard (5432, 6379, 8090).
Troubleshooting¶
Port Conflicts¶
If you see "Address already in use":
# 1. Run setup.py again - it will find available ports
python setup.py
# 2. Or check what's using the port
lsof -i :5432 # Check PostgreSQL port
lsof -i :6379 # Check Redis port
lsof -i :8000 # Check Backend port
Service Can't Connect¶
If backend can't connect to Redis:
# Wrong: Using external port from docker-compose.override.yml
REDIS_URL=redis://localhost:6380 # ❌ This won't work!
# Right: Use internal standard port
REDIS_URL=redis://localhost:6379 # ✅ Correct for local dev
REDIS_URL=redis://redis:6379 # ✅ Correct for Docker
Environment Variable Mismatch¶
If you see "Connection refused":
- Check hostname matches environment (localhost for dev, service names for docker)
- Verify port is standard (5432, 6379, 8090, not 5433, 6380)
- Confirm DATABASE_URL is set in environment variables, not just .env.example
Monitoring Configuration Files¶
The monitoring stack configuration files (monitoring/) use Docker service names for all service-to-service communication, which means no templating is required even though they contain hardcoded port numbers.
Why No Templating Is Needed¶
The .env port variables (e.g., PROMETHEUS_PORT=9090, GRAFANA_PORT=3002) only control host port bindings in docker-compose files. They define which port on the host machine maps to the container's internal port.
Inside the Docker network, all services communicate using:
- Service names (e.g.,
prometheus,backend,alertmanager) - Internal container ports (fixed, not configurable)
For example, in docker-compose.prod.yml:
The ${PROMETHEUS_PORT} only affects external access (localhost:9090). Internal services always connect to prometheus:9090 regardless of what PROMETHEUS_PORT is set to.
Files That Use Service Names (No Templating Needed)¶
| File | Example References | Notes |
|---|---|---|
monitoring/prometheus.yml | alertmanager:9093, backend:8000, ai-llm:8091, ai-gateway:8002 | All scrape targets use service names |
monitoring/alertmanager.yml | http://backend:8000/api/webhooks/alerts | Webhook receivers use service names |
monitoring/grafana/provisioning/datasources/prometheus.yml | http://prometheus:9090, http://loki:3100 | Datasource URLs use service names |
monitoring/alloy/config.alloy | http://loki:3100, http://pyroscope:4040, tempo:4317 | All endpoints use service names |
Internal vs External Ports¶
| Service | Internal Port (Fixed) | External Port (Configurable via .env) |
|---|---|---|
| Prometheus | 9090 | PROMETHEUS_PORT |
| Alertmanager | 9093 | ALERTMANAGER_PORT |
| Grafana | 3000 | GRAFANA_PORT (default host 3002) |
| Loki | 3100 | LOKI_PORT |
| Tempo (OTLP) | 4317 | TEMPO_OTLP_GRPC |
| Pyroscope | 4040 | PYROSCOPE_PORT |
| Node Exporter | 9100 | NODE_EXPORTER_PORT |
| cAdvisor | systemd service | CADVISOR_PORT (host service, not compose) |
Tracing moved from Jaeger to Grafana Tempo (NEM-5545); the
JAEGER_*variables remaining in.env.exampleare vestigial.
Special Case: DCGM Exporter¶
The dcgm-exporter job in prometheus.yml uses host.containers.internal:9400 because:
- DCGM exporter runs in rootful Podman (requires elevated privileges)
- Prometheus runs in rootless Podman
- They are in separate network namespaces, so service-name DNS doesn't work
This is the only service that references a host-accessible endpoint, and 9400 is the standard DCGM exporter port. (cAdvisor moved to a rootful systemd service with the same pattern — see monitoring/cadvisor/cadvisor.service.)
When Would Templating Be Needed?¶
Templating would only be necessary if:
- A monitoring config file needed to reference a host-accessible endpoint (not service-name based)
- That endpoint's port was configurable in
.env
Currently, no monitoring configuration files have this requirement.
Benefits¶
- Simplified Configuration: No special port handling per environment
- Better Documentation: Clear separation of hostname vs. port changes
- Fewer Bugs: Reduces misconfigured ports causing connection failures
- Easier Migration: Moving from dev to Docker/production requires only hostname changes
- Standard Compliance: Uses industry-standard ports (PostgreSQL 5432, Redis 6379, etc.)
- Automated Setup:
setup.pyhandles complexity automatically
References¶
docker-compose.prod.yml- Production configurationdocker-compose.override.yml- Development overrides (generated).env.example- Environment template with port documentationbackend/core/config.py- Configuration settingssetup.py- Setup script with port standardization