DataHub

Production Deployment

Deploy DataHub behind a reverse proxy with TLS and production secrets.

This guide covers a single-host production layout: Docker Compose for the stack, nginx for TLS termination, and loopback-only service ports.

For local demo setup, see Quick start. For hardware and ports, see System requirements.

Prerequisites

  • Linux VPS or on-prem server with Docker Engine + Compose v2
  • DNS pointing at the server (A/AAAA record)
  • Host ports 4300 (frontend) and 4400 (API) available on loopback — not exposed on the public firewall
  • ~8 GB RAM recommended for production workloads

1. Deploy the stack

git clone <repo-url> datahub
cd datahub/deploy
cp env.example .env

Edit .env with production values:

POSTGRES_PASSWORD=<strong>
JWT_SECRET=<long-random>
ADMIN_PASSWORD=<strong>
NEXT_PUBLIC_API_URL=https://your-domain.example

NEXT_PUBLIC_API_URL is a build-time variable for the frontend. Rebuild the frontend container after changing it.

Start services:

docker compose up -d --build
docker compose ps
curl -s http://127.0.0.1:4400/api/health

If you see pull access denied for datahub-backend, build the API image first:

docker compose build api
docker compose up -d --build

2. Reverse proxy (nginx)

Publish only 443 (and 80 for Certbot). Proxy /api/ to the API container and / to the frontend:

server {
    listen 80;
    server_name your-domain.example;

    location /api/ {
        proxy_pass http://127.0.0.1:4400;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 30m;
    }

    location / {
        proxy_pass http://127.0.0.1:4300;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Add TLS with Certbot (certbot --nginx -d your-domain.example), then verify:

  1. https://your-domain.example — login UI
  2. https://your-domain.example/api/health — OK
  3. Sign in with admin and your ADMIN_PASSWORD

3. Security checklist

  • Rotate JWT_SECRET, ADMIN_PASSWORD, and POSTGRES_PASSWORD from defaults
  • Do not expose Postgres, Redis, NATS, or Compose ports on the public network
  • Terminate TLS at nginx (or equivalent)
  • Restrict console access via network policy or VPN if required

4. Asset file storage

Uploaded plant/machine/device files live in the Docker volume asset-files (inside the API container at /data/asset-files). docker compose down keeps volumes; docker compose down -v wipes uploads and the database.

5. Day-2 operations

cd deploy
docker compose ps
docker compose logs -f api

# After code or NEXT_PUBLIC_API_URL changes
docker compose up -d --build

# Restart workers only
docker compose up -d telemetry-worker rule-worker action-worker \
  adapter-orchestrator program-runner

6. Telemetry scale (optional)

Default compose runs one telemetry-worker with batched Postgres writes. For higher ingest or plant isolation, optional env vars in deploy/env.example include:

VariablePurpose
TELEMETRY_BATCH_SIZEEvents per Postgres batch (default 500)
TELEMETRY_BATCH_MAX_MSMax wait before flushing a partial batch
TELEMETRY_FILTER_SUBJECTJetStream subject filter (e.g. datahub.telemetry.izmit.>)
TELEMETRY_CONSUMER_NAMEDurable consumer name per replica

Use deploy/docker-compose.telemetry-scale.yml to run one worker per plant — scale the default telemetry-worker to 0 when using plant-scoped replicas.

Troubleshooting

SymptomCheck
UI loads but API failsNEXT_PUBLIC_API_URL matches public origin; rebuild frontend
API restart loopdocker compose logs api — database / Prisma
No live telemetryadapter-orchestrator logs; device adapter status
413 on file uploadIncrease nginx client_max_body_size