Files
meshcore-hub/docs/deployment.md
T
Louis King 5a8794f5c9 fix(compose): keep bundled postgres out of the core profile in production
Starting core with docker-compose.prod.yml also started the bundled
postgres container, whose 'postgres' network alias shadowed a shared
instance of the same name on the same Docker network — the live site
silently talked to an empty local database instead of DATABASE_HOST.

- Base file: postgres profiles are now [all, postgres] (core dropped);
  the dev override unions 'core' back in, so the documented dev
  workflow (make up, --profile core) is unchanged
- migrate's postgres dependency is required: false — still health-gated
  when the bundled container is active, skipped when profile-inactive
- prod override documents the behavior; opt in explicitly with
  --profile postgres (or --profile all) for a bundled production DB
- Docs: database.md/deployment.md/configuration.md/README/.env.example
  updated; upgrading.md gains a one-time cleanup runbook (full down to
  remove the previously started hub-postgres container)
2026-08-30 11:22:08 +01:00

6.3 KiB
Raw Blame History

Deployment

This document covers production deployments of MeshCore Hub: reverse-proxy setup, multi-instance routing, API scaling, and the optional Redis response cache. For the full environment-variable reference, see configuration.md.

Production Setup

For production deployments, use docker-compose.prod.yml which connects services to an external proxy network. No ports are exposed directly — all traffic goes through your reverse proxy.

Prerequisites:

  1. A reverse proxy (Nginx Proxy Manager, Caddy, Traefik, etc.)
  2. Docker network for proxy communication

Steps:

# Create proxy network (once)
docker network create proxy-net

# Download compose files and config
mkdir meshcore-hub && cd meshcore-hub
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.prod.yml
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/.env.example
cp .env.example .env
# Edit .env: set COMPOSE_PROJECT_NAME, MQTT credentials, API keys, etc.

# Start core services
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile core up -d

# Or include local MQTT broker
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core up -d

# Or include packet capture on the same host
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile mqtt --profile core --profile observer up -d

Database in production: the bundled postgres container is not started by the core profile with docker-compose.prod.yml — its postgres network alias would shadow a shared instance of the same name on the same network. Point the DATABASE_* variables at your shared/managed Postgres (see database.md). If this host has no external database, opt in to the bundled container explicitly with --profile postgres and set a strong DATABASE_PASSWORD.

Configure your reverse proxy to forward to the containers:

Service Container Port Path
Web Dashboard {COMPOSE_PROJECT_NAME}-web 8080 /
API {COMPOSE_PROJECT_NAME}-api 8000 /api, /metrics, /health
MQTT WebSocket {COMPOSE_PROJECT_NAME}-mqtt 1883 / (only if using local broker)

Important: Do not host under a subpath (e.g., /meshcore). Proxy at /.

Reverse Proxy

MeshCore Hub is designed to run behind a reverse proxy in production. A Traefik override file is provided with pre-configured labels:

# Download the Traefik override
wget https://raw.githubusercontent.com/ipnet-mesh/meshcore-hub/refs/heads/main/docker-compose.traefik.yml

# Set your domain in .env
echo "TRAEFIK_DOMAIN=meshcore.example.com" >> .env

# Start with Traefik labels
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.traefik.yml --profile core up -d

This routes the web dashboard and API to TRAEFIK_DOMAIN with automatic TLS.

Multi-Instance Deployments

To run multiple Hub instances (e.g., production + staging) on the same Docker host, set TRAEFIK_PRIORITY to control which router wins when domains overlap. Higher values are matched first:

# Production (.env)
COMPOSE_PROJECT_NAME=hub
TRAEFIK_DOMAIN=example.com
TRAEFIK_PRIORITY=10

# Staging (.env) — separate directory with its own config
COMPOSE_PROJECT_NAME=hub-beta
TRAEFIK_DOMAIN=beta.example.com
TRAEFIK_PRIORITY=20

This ensures beta.example.com (priority 20) is matched before the production wildcard *.example.com (priority 10). For other services on the same network (e.g., an MQTT broker at mqtt.example.com), use an even higher priority (e.g., 30).

Shared Postgres cluster: the setup above runs each instance in its own directory with its own volumes — add --profile postgres to each instance's up command for a bundled Postgres container per instance. To instead run several instances (e.g. prod + stg) against one PostgreSQL cluster — isolated via a per-instance schema (search_path) — see database.md.

Scaling the API

The API is read-mostly and holds no per-process state — the response cache lives in Redis and authentication is stateless — so it scales across multiple worker processes. Set API_WORKERS to run more than one worker in a single container:

# .env
API_WORKERS=4

Each worker is an independent process sharing one listening socket, so the kernel balances connections across them and CPU-bound work (JSON serialisation, validation) spreads over multiple cores. Workers read their configuration from environment variables (CLI flags are not propagated to forked workers), which is how Docker Compose already supplies config. Enabling Redis (REDIS_ENABLED=true) is recommended so all workers share one cache.

Pick a worker count around the number of CPU cores available to the container; start with 2–4 and measure under realistic load. Workers all talk to the same PostgreSQL database (the bundled container or an external instance — see database.md), so scaling extends across hosts unchanged.

Prefer API_WORKERS over running multiple api containers (--scale api=N): the api service uses a fixed container_name, and one process-managed container per stack keeps logs, health checks, and monitoring simple.

Redis Caching

Optional Redis-backed caching for API responses. When disabled or unavailable, the API queries the database directly.

Docker: Redis is included in the cache profile. Disabled by default — set REDIS_ENABLED=true to enable.

docker compose --profile cache up    # Start with bundled Redis
docker compose --profile core up     # Start without Redis

Bare-metal: Install Redis separately, then set REDIS_ENABLED=true and REDIS_HOST=localhost.

Multi-instance: Use different REDIS_KEY_PREFIX values per instance to share one Redis without key collisions.

For the full list of REDIS_* environment variables, see configuration.md → Caching.