Skip to main content

Docker Compose — Self-Hosted

The self-hosted deployment runs the entire Iotistica cloud stack on a single machine using Docker Compose. It requires no Kubernetes cluster, no cloud account, and no license key — the API starts in community mode with unlimited devices when IOTISTIC_LICENSE_KEY is not set.

Stack

ServiceImageInternal portPurpose
postgrestimescale/timescaledb:latest-pg165432Primary database + TimescaleDB
redisredis:7-alpine6379Ingestion stream, pub/sub, dedup cache
mosquittoiegomez/mosquitto-go-auth1883 / 8883 / 9002MQTT broker with HTTP auth callback
apiiotistic/api (built locally)3002REST API, device auth, MQTT subscription
ingestioniotistic/ingestion (built locally)3003Redis Streams → TimescaleDB writer

Only the API and Mosquitto ports are exposed on the host. Postgres and Redis are internal to the Docker network.


Prerequisites

  • Docker 24+ and Docker Compose v2
  • At least 4 GB RAM and 10 GB free disk
  • TLS certificates for Mosquitto (see TLS Certificates below)

Quick Start

1. Clone and enter the repo:

git clone https://github.com/iotistica/iotistica.git
cd iotistica

2. Copy the environment template and fill in your secrets:

cp .env.self.example .env.self # or copy the table below into a new .env.self

At minimum, change every value marked change-me. See the Environment Variables reference below.

3. Generate TLS certificates for Mosquitto (or provide your own — see TLS Certificates).

4. Start the stack:

docker compose -f docker-compose.self.yml --env-file .env.self up -d

5. Verify all services are healthy:

docker compose -f docker-compose.self.yml ps

All five containers should reach healthy or running within ~30 seconds.

6. Done. The API seeds the MQTT admin user automatically on first startup from MQTT_USERNAME and MQTT_PASSWORD — no manual step needed.


Environment Variables

Copy this block into .env.self and replace every change-me value with a strong random string.

# Usage: docker compose -f docker-compose.self.yml --env-file .env.self up -d

# ---- PostgreSQL ----
POSTGRES_DB=iotistica
POSTGRES_USER=postgres
POSTGRES_PASSWORD=change-me
DB_HOST=postgres
DB_PORT=5432

# ---- Redis ----
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=change-me
REDIS_INGESTION_STREAM_KEY=tenant:{default}:agent:devices:ingestion

# ---- MQTT / Mosquitto ----
MQTT_USERNAME=admin
MQTT_PASSWORD=change-me
MOSQUITTO_PORT_EXT=7883
MOSQUITTO_TLS_PORT_EXT=9883
MOSQUITTO_WSS_PORT_EXT=9003

# ---- API ----
API_PORT_EXT=4003
JWT_SECRET=change-me-use-min-32-random-chars
AGENT_SHELL_HMAC_KEY=change-me

# ---- Ingestion ----
INGESTION_PROFILE=balanced

# ---- Auth0 (optional — leave AUTH0_ENABLED=false to skip) ----
AUTH0_ENABLED=false

Variable reference

PostgreSQL

VariableDefaultRequiredDescription
POSTGRES_DBiotisticaDatabase name
POSTGRES_USERpostgresDatabase superuser username
POSTGRES_PASSWORDYesDatabase password
DB_HOSTpostgresHostname — matches the Compose service name
DB_PORT5432PostgreSQL port

Redis

VariableDefaultRequiredDescription
REDIS_HOSTredisHostname — matches the Compose service name
REDIS_PORT6379Redis port
REDIS_PASSWORDYesRedis requirepass password
REDIS_INGESTION_STREAM_KEYtenant:{default}:agent:devices:ingestionRedis Stream key the ingestion service reads from

MQTT / Mosquitto

VariableDefaultRequiredDescription
MQTT_USERNAMEadminAdmin MQTT username (must match a row in mqtt_users)
MQTT_PASSWORDYesAdmin MQTT password
MOSQUITTO_PORT_EXT7883Host port mapped to plain MQTT (1883 inside)
MOSQUITTO_TLS_PORT_EXT9883Host port mapped to MQTTS (8883 inside)
MOSQUITTO_WSS_PORT_EXT9003Host port mapped to MQTT over WSS (9002 inside)

API

VariableDefaultRequiredDescription
API_PORT_EXT4003Host port mapped to the REST API (3002 inside)
JWT_SECRETYesSecret used to sign JWT access and refresh tokens. Use at least 32 random characters.
AGENT_SHELL_HMAC_KEYYesHMAC key used to sign remote shell commands sent to agents.
AUTH0_ENABLEDfalseSet to true to enable Auth0 for dashboard authentication. When false, the API uses its own JWT issuer.
AUTH0_DOMAINIf Auth0Auth0 tenant domain, e.g. your-tenant.auth0.com
AUTH0_AUDIENCEIf Auth0Auth0 API audience identifier
AUTH0_ISSUERIf Auth0Token issuer URL, e.g. https://your-tenant.auth0.com/

Ingestion

VariableDefaultRequiredDescription
INGESTION_PROFILEbalancedThroughput/latency trade-off profile. See table below.

Ingestion profiles:

ProfileBehaviour
batchLargest batches, highest throughput, higher write latency
balancedDefault — balanced latency and throughput
streamingSmallest batches, lowest latency, higher database load
benchmarkMaximum throughput for load testing
hpHigh-performance tuning for large fleets

Compose File

docker-compose.self.yml
services:
postgres:
image: timescale/timescaledb:latest-pg16
restart: unless-stopped
environment:
- POSTGRES_DB=${POSTGRES_DB:-iotistica}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- iotistic-pg-data:/var/lib/postgresql/data
command: >
postgres
-c shared_preload_libraries=timescaledb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- iotistica-net

redis:
image: redis:7-alpine
restart: unless-stopped
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--maxmemory 2gb
--maxmemory-policy noeviction
--save ""
--appendonly no
deploy:
resources:
limits:
memory: 2560m
reservations:
memory: 2048m
healthcheck:
test: ["CMD-SHELL", "redis-cli --no-auth-warning -a \"$$REDIS_PASSWORD\" ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- iotistica-net

mosquitto:
image: iegomez/mosquitto-go-auth
restart: unless-stopped
ports:
- "${MOSQUITTO_TLS_PORT_EXT:-8883}:8883"
- "${MOSQUITTO_WSS_PORT_EXT:-9002}:9002"
- "${MOSQUITTO_PORT_EXT:-1883}:1883"
volumes:
- ./mosquitto/mosquitto-tls.conf:/etc/mosquitto/mosquitto.conf
- ./mosquitto/certs:/mosquitto/certs
healthcheck:
test: ["CMD-SHELL", "cat /proc/1/status > /dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
networks:
- iotistica-net

api:
build:
context: ./api
dockerfile: Dockerfile
restart: always
environment:
- PORT=${PORT:-3002}
- NODE_ENV=${NODE_ENV:-production}
- LOG_LEVEL=${LOG_LEVEL:-info}
- JWT_SECRET=${JWT_SECRET}
- AUTH0_ENABLED=${AUTH0_ENABLED:-false}
- AUTH0_DOMAIN=${AUTH0_DOMAIN}
- AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
- AUTH0_ISSUER=${AUTH0_ISSUER}
- DB_HOST=${DB_HOST:-postgres}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${POSTGRES_DB:-iotistica}
- DB_USER=${POSTGRES_USER:-postgres}
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_SSL=${DB_SSL:-false}
- MQTT_BROKER_URL=${MQTT_BROKER_URL:-mqtt://mosquitto:1883}
- MQTT_USERNAME=${MQTT_USERNAME:-admin}
- MQTT_PASSWORD=${MQTT_PASSWORD}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_INGESTION_STREAM_KEY=${REDIS_INGESTION_STREAM_KEY:-tenant:{default}:agent:devices:ingestion}
- AGENT_SHELL_HMAC_KEY=${AGENT_SHELL_HMAC_KEY}
ports:
- "${API_PORT_EXT:-4002}:3002"
volumes:
- iotistic-api-spool:/var/lib/iotistic/spool
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
mosquitto:
condition: service_started
networks:
- iotistica-net

ingestion:
build:
context: .
dockerfile: ingestion/Dockerfile
restart: always
environment:
- PORT=${INGESTION_PORT:-3003}
- NODE_ENV=${NODE_ENV:-production}
- LOG_LEVEL=${LOG_LEVEL:-info}
- INGESTION_PROFILE=${INGESTION_PROFILE:-balanced}
- DB_HOST=${DB_HOST:-postgres}
- DB_PORT=${DB_PORT:-5432}
- DB_NAME=${POSTGRES_DB:-iotistica}
- DB_USER=${POSTGRES_USER:-postgres}
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_SSL=${DB_SSL:-false}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- REDIS_INGESTION_STREAM_KEY=${REDIS_INGESTION_STREAM_KEY:-tenant:{default}:agent:devices:ingestion}
- INGESTION_SPOOL_ENABLED=${INGESTION_SPOOL_ENABLED:-true}
- INGESTION_SPOOL_PATH=/var/lib/iotistic/spool
- INGESTION_SPOOL_MAX_SIZE_MB=${INGESTION_SPOOL_MAX_SIZE_MB:-1000}
volumes:
- iotistic-ingestion-spool:/var/lib/iotistic/spool
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3003/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- iotistica-net

volumes:
iotistic-pg-data:
driver: local
iotistic-api-spool:
driver: local
iotistic-ingestion-spool:
driver: local

networks:
iotistica-net:
driver: bridge

TLS Certificates

Mosquitto requires TLS certificates mounted at mosquitto/certs/. For a quick start, generate a self-signed certificate:

mkdir -p mosquitto/certs
openssl req -x509 -newkey rsa:4096 -keyout mosquitto/certs/server.key \
-out mosquitto/certs/server.crt -days 3650 -nodes \
-subj "/CN=localhost"

For production, replace the self-signed certificate with one issued by your CA or Let's Encrypt. Clients connecting over TLS with a self-signed certificate should disable certificate verification (rejectUnauthorized: false / insecure: true in client config).

The Mosquitto configuration at mosquitto/mosquitto-tls.conf defines three listeners:

ListenerPortProtocolNotes
MQTT1883TCP (plain)No TLS — suitable for LAN / internal traffic
MQTTS8883TLSRequires cert files
WSS9002WebSocket over TLSRequires cert files

MQTT Users

Mosquitto authenticates clients via an HTTP callback to the API at /mosquitto-auth/*. The API looks up credentials against the mqtt_users table in PostgreSQL.

The admin user is seeded automatically on every API startup. The bootstrapDatabase init step runs an idempotent INSERT ... ON CONFLICT DO UPDATE using the MQTT_USERNAME and MQTT_PASSWORD environment variables. No manual seeding is required.

Passwords are stored as scrypt hashes (Node.js native crypto). If you change MQTT_PASSWORD in .env.self and restart the API, the stored hash is updated automatically on the next startup.

Each edge agent is also provisioned its own MQTT credentials during the provisioning flow — the admin user is only used by the API itself to subscribe to the broker.


Operational Reference

View logs:

# All services
docker compose -f docker-compose.self.yml logs -f

# Single service
docker compose -f docker-compose.self.yml logs -f api
docker compose -f docker-compose.self.yml logs -f ingestion

Check service health:

docker compose -f docker-compose.self.yml ps
curl http://localhost:4003/health

Restart a single service:

docker compose -f docker-compose.self.yml restart api

Stop the stack:

docker compose -f docker-compose.self.yml down

Stop and remove all data volumes (destructive — deletes the database):

docker compose -f docker-compose.self.yml down -v

Check ingestion stream depth:

docker compose -f docker-compose.self.yml exec redis \
redis-cli --no-auth-warning -a "$REDIS_PASSWORD" \
XLEN "tenant:{default}:agent:devices:ingestion"

Updating

git pull
docker compose -f docker-compose.self.yml --env-file .env.self build
docker compose -f docker-compose.self.yml --env-file .env.self up -d

Database migrations run automatically on API startup.