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 .envEdit .env with production values:
POSTGRES_PASSWORD=<strong>
JWT_SECRET=<long-random>
ADMIN_PASSWORD=<strong>
NEXT_PUBLIC_API_URL=https://your-domain.exampleNEXT_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/healthIf you see pull access denied for datahub-backend, build the API image first:
docker compose build api
docker compose up -d --build2. 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:
https://your-domain.example— login UIhttps://your-domain.example/api/health— OK- Sign in with
adminand yourADMIN_PASSWORD
3. Security checklist
- Rotate
JWT_SECRET,ADMIN_PASSWORD, andPOSTGRES_PASSWORDfrom 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-runner6. 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:
| Variable | Purpose |
|---|---|
TELEMETRY_BATCH_SIZE | Events per Postgres batch (default 500) |
TELEMETRY_BATCH_MAX_MS | Max wait before flushing a partial batch |
TELEMETRY_FILTER_SUBJECT | JetStream subject filter (e.g. datahub.telemetry.izmit.>) |
TELEMETRY_CONSUMER_NAME | Durable 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
| Symptom | Check |
|---|---|
| UI loads but API fails | NEXT_PUBLIC_API_URL matches public origin; rebuild frontend |
| API restart loop | docker compose logs api — database / Prisma |
| No live telemetry | adapter-orchestrator logs; device adapter status |
| 413 on file upload | Increase nginx client_max_body_size |
