Installation

This guide covers deploying ubTrace in environments with no internet access, such as automotive OEM networks with strict security policies (TISAX, ISO 27001).

Note

For users of the offline version: The latest version of this guide is hosted at ubtrace.useblocks.com/latest/installation.html. Pin to a specific release with /X.Y.Z/installation.html.

Quick Start

Prerequisites: Docker Engine 24+ with Compose v2, ~5 GB disk for images.

# 1. Extract the bundle
tar xzf ubtrace-offline-bundle-*.tar.gz
cd ubtrace-offline-bundle-*

# 2. Load Docker images into the local daemon
./offline-load.sh ubtrace-images-*.tar.gz

# 3. Create .env from the bundled template and fill in passwords
make init
vim .env  # Replace password placeholders (see "Passwords & Secrets" below)

# 4. Start ubTrace
make up

# 5. Verify all services are healthy
make status

# 6. (Optional) Import your documentation artifacts
#    The worker auto-detects changes -- no restart needed.
#    Pre-built CI output:
#      make import-build SRC=./ci-output ORG=mycompany PROJECT=my-project VERSION=v1
#    Or Sphinx source (builder builds automatically):
#      make import-src SRC=./my-sphinx-project ORG=mycompany PROJECT=my-project VERSION=v1

Note

macOS / Apple Silicon: Elasticsearch requires a workaround before step 4. See Platform Notes.

First Login

The bundled Keycloak realm includes a pre-created test user so you can log in immediately after deployment. This user only works when the pre-configured OIDC client secret is kept unchanged (see Passwords & Secrets below):

Username

test

Password

Test1234!

Email

test-keycloak-user@useblocks.com

Open the ubTrace frontend (default: http://localhost:7155) and log in with these credentials to verify that the system is working.

Note

If you have already rotated the OIDC client secret (see Keycloak / OIDC Hardening below), this test user will still exist in Keycloak but the backend must be configured with the new secret for authentication to work.

Warning

This test user is intended for initial verification only. Before going to production, delete it and create your own users in Keycloak. See Keycloak / OIDC Hardening for the full hardening checklist.

Platform Notes

macOS / Apple Silicon (M1/M2/M3/M4)

The offline bundle ships linux/amd64 images targeting production Linux servers. On Apple Silicon Macs, most services run fine under Rosetta emulation, but Elasticsearch 9.x will fail with a seccomp unavailable error because the emulation layer does not support the Linux seccomp sandbox.

Workaround: Pull the native arm64 Elasticsearch image (requires internet):

docker pull docker.elastic.co/elasticsearch/elasticsearch:9.3.0
make up

This replaces the bundled amd64 image with the native arm64 variant for Elasticsearch only. You can ignore the platform (linux/amd64) does not match warnings for other services – they run correctly under Rosetta.

Note

This is only needed for local testing on Mac. Production deployments on Linux/amd64 servers work without any workaround.

Linux (Production)

No special steps required. The bundled images are linux/amd64 and run natively. Ensure Docker Engine 24+ with Compose v2 is installed, and that the user running Docker has permissions to load images (docker load).

Configuration

Passwords & Secrets

The bundle includes .env.example with all required variables and sensible defaults. make init copies it to .env – you only need to replace the <CHANGE-ME> placeholders with strong, unique passwords:

  • POSTGRES_PASSWORD – application database

  • KC_DB_PASSWORD – Keycloak database

  • KEYCLOAK_ADMIN_PASSWORD – Keycloak admin console

  • REDIS_PASSWORD – cache and sessions

  • OIDC_CLIENT_SECRET – see note below

Important

OIDC Client Secret: The bundled Keycloak realm ships with a pre-configured client secret. The .env.example already contains the correct value (FnZuseq02pwaNKqsvDxL3jq4HhzPey2b). Do not change it unless you also regenerate the secret in Keycloak (see Keycloak / OIDC Hardening below). Changing the .env value without updating Keycloak will cause authentication failures on startup.

Warning

Do not use the # character in password values. Docker Compose treats # as an inline comment delimiter, which causes health check scripts and service commands to receive truncated values. Use only alphanumeric characters and symbols like !@$%^&*()-_=+ instead.

See Environment Variables for the complete list.

Accessing via IP Address (HTTP)

The default setup assumes HTTPS or localhost. If you access ubTrace over plain HTTP using an IP address (e.g., http://192.168.1.100:7155 in a lab or workshop network), authentication cookies will be silently rejected by browsers because they require Secure (HTTPS-only) by default.

To fix this, set COOKIE_SECURE=false in your .env:

COOKIE_SECURE=false

# All URL env vars must use the same protocol and host:
API_SERVER_PUBLIC_URL=http://192.168.1.100:7150
FRONTEND_PUBLIC_URL=http://192.168.1.100:7155
FRONTEND_UBT_URL=http://192.168.1.100:7155
ADMIN_UBT_URL=http://192.168.1.100:7156
ADMIN_PUBLIC_URL=http://192.168.1.100:7156
# ADMIN_PUBLIC_URL is mapped to UBTRACE_ADMIN_URL inside the frontend
# container. It enables the "Go to Admin" button for ubtrace-admin users.
# Relative /api so browser requests go through the Next.js same-origin proxy
# (avoids Safari ITP cross-origin cookie issues). Only set an absolute URL if
# you have a reverse proxy that serves both frontend and API on one origin.
UBTRACE_API_URL=/api
KC_HOSTNAME=http://192.168.1.100:7181
OIDC_ISSUER=http://192.168.1.100:7181/realms/ubtrace

Then restart: make restart-api

Warning

Only use COOKIE_SECURE=false in trusted networks. For production deployments, place a reverse proxy (nginx, Traefik, Caddy) in front of ubTrace with TLS termination and keep COOKIE_SECURE=true.

Rate Limiting & Reverse Proxy

ubTrace applies IP-based rate limiting to API endpoints (e.g. 10 req/min for auth, 100 req/min default). Two environment variables control behavior behind proxies or kubectl port-forward:

RATE_LIMIT_SKIP_LOOPBACK (default: true)

Skip IP-based rate limiting for direct loopback connections (127.0.0.1, ::1). This prevents all port-forwarded users from sharing a single rate-limit bucket. The bypass only triggers when no X-Forwarded-For header is present, so it cannot be exploited through a proxy.

TRUST_PROXY (default: unset)

Configures Express to parse X-Forwarded-For headers from trusted proxies, so rate limiting sees the real client IP instead of the proxy’s IP. Set this when running behind a reverse proxy or ingress controller.

Valid values: loopback, linklocal, uniquelocal, an IP/CIDR (e.g. 10.0.0.0/8), or a comma-separated list.

Deployment scenarios:

Scenario

RATE_LIMIT_SKIP_LOOPBACK

TRUST_PROXY

Docker Compose (direct access)

true (default)

(leave unset)

kubectl port-forward

true (default)

(leave unset)

Kubernetes with ingress (nginx)

true (default)

loopback

Behind reverse proxy (nginx/Traefik)

true (default)

loopback or proxy IP/CIDR

When deploying via the Helm chart, set these as api.config.rateLimitSkipLoopback and api.config.trustProxy in your values overlay. The bundled values-minikube.yaml and values-eks.yaml overlays already include the appropriate trustProxy setting.

Keycloak / OIDC Hardening

Before going live, rotate the client secret and remove the pre-created test user in Keycloak:

  1. Log in to Keycloak admin (http://localhost:7181, admin / your admin password)

  2. In the left sidebar, click Manage realms, then select ubtrace (you’ll land on the “Welcome to ubtrace” page)

  3. Go to Clientsnestjs-appCredentials tab → Regenerate Secret

  4. Copy the new secret into .env as OIDC_CLIENT_SECRET

  5. Go to Users and delete the pre-created test user

  6. Restart the API server: make restart-api

Importing Artifacts

ubTrace uses a worker pipeline that automatically detects new or changed artifacts. No manual rebuild or API server restart is needed.

Option B: Sphinx Source Files

If you want ubTrace to build your Sphinx projects, place source files into input_src/:

# Using the import helper
make import-src SRC=./my-sphinx-project ORG=mycompany PROJECT=my-project VERSION=v1

# Or copy manually
mkdir -p input_src/mycompany/my-project/v1
cp -r my-sphinx-project/* input_src/mycompany/my-project/v1/

The ubtrace-builder service polls input_src/ every 60 seconds, builds with Sphinx, and writes output to input_src_build/ (internal volume). The ub-worker then picks up the build output automatically.

Each version directory must be a self-contained Sphinx project with its own conf.py. The builder auto-detects three layouts:

Layout

Description

Example

B

conf.py inside source/

v1/source/conf.py + v1/source/index.rst

A

conf.py at root + source/ subdir

v1/conf.py + v1/source/index.rst

C

Flat (conf.py alongside content)

v1/conf.py + v1/index.rst

Directory Overview

Directory

Customer-facing

Purpose

input_src/

Yes (mount)

Sphinx source files for builder

input_build/

Yes (mount)

Pre-built CI output for worker

input_src_build/

No (internal)

Builder output (optionally mountable for debug)

output/

No (internal)

Worker-processed data: NDJSON + Sphinx output (optionally mountable for debug)

Kubernetes Deployment (Helm)

The offline bundle includes a Helm chart for deploying ubTrace on Kubernetes. This is an alternative to the Docker Compose deployment described above.

Prerequisites

  • Kubernetes 1.25+

  • Helm 3.12+

  • kubectl configured for your cluster

  • Docker images loaded into a registry accessible from the cluster

Install from Bundle

# Extract the bundle
tar xzf ubtrace-offline-bundle-*.tar.gz
cd ubtrace-offline-bundle-*

# Load images (on each node, or push to a private registry)
./offline-load.sh ubtrace-images-*.tar.gz

Install with default values:

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace

Install with production hardening:

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace \
  -f charts/values-production.yaml

Install on AWS EKS:

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace \
  -f charts/values-eks.yaml \
  -f charts/values-production.yaml

Install on OpenShift:

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace \
  -f charts/values-openshift.yaml \
  -f charts/values-production.yaml

Try locally with Minikube:

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace \
  -f charts/values-minikube.yaml

Customizing Values

Copy the default values file and adjust for your environment:

cp charts/values.yaml my-values.yaml
vim my-values.yaml  # Set passwords, ingress hosts, storage classes, etc.

helm install ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace --create-namespace \
  -f my-values.yaml \
  -f charts/values-production.yaml

Key values to configure:

Value

Description

global.imageRegistry

Private registry URL

api.env.POSTGRES_PASSWORD

Application database password

api.env.REDIS_PASSWORD

Redis password

api.env.OIDC_CLIENT_SECRET

Keycloak client secret

api.ingress.hosts

API server hostname(s)

frontend.ingress.hosts

Frontend hostname(s)

Available Values Files

File

Purpose

values.yaml

Base defaults (matches docker-compose-prod.yml)

values-production.yaml

Production hardening (replicas, resources, PDBs)

values-eks.yaml

AWS EKS settings (ALB ingress, gp3 storage)

values-openshift.yaml

OpenShift-specific settings (routes, SCCs, GID 0)

values-minikube.yaml

Local evaluation (reduced resources, standard SC)

values-eks-terraform.yaml

AWS EKS with Terraform-managed infrastructure

Upgrade

helm upgrade ubtrace ./charts/ubtrace-*.tgz \
  --namespace ubtrace \
  -f my-values.yaml \
  -f charts/values-production.yaml

Warning

Breaking change in 1.9.0 — license enforcement is hard-gated in production

From 1.9.0, DEPLOY_ENV defaults to production and is baked into the images, and in production the master bypass UBTRACE_LICENSE_DISABLED=true is a hard startup error — the API and worker refuse to boot.

Before upgrading, if your .env (or Helm values) sets UBTRACE_LICENSE_DISABLED=true — for example following earlier documentation that suggested it to run without a license — remove it or set it to false. Otherwise the ubtrace-api and ubtrace-worker containers will crash-loop after the upgrade with:

UBTRACE_LICENSE_DISABLED must not be enabled in production
(DEPLOY_ENV=production).

To run without a license for local evaluation only, set DEPLOY_ENV=development instead — but note this is not secure: it also relaxes authentication and exposes test-only endpoints.

Day-to-Day Operations

Command

Description

make status

Show service health

make logs

Follow all logs (make logs SERVICE=ubtrace-api for one)

make up

Start all services

make down

Stop all services (preserves data)

make restart-api

Restart the API server only

make import-build

Import pre-built CI output into input_build/

make import-src

Import Sphinx source into input_src/ for builder

make verify

Run the offline verification script

make pull

Pull latest images (requires internet or registry)

make update

Pull latest images and restart

make clean

Stop services and delete all data volumes

make help

Show all available targets

Troubleshooting

Images not loading

# Verify Docker daemon is running
docker info

# List loaded ubTrace-related images
docker images | grep -E 'postgres|redis|elasticsearch|keycloak|ub-backend|ub-frontend|ub-builder|ub-worker'

Compose config errors

# Validate compose file resolves all variables correctly
UBTRACE_VERSION=1.11.0 docker compose -f docker-compose-prod.yml config

API or worker crash-loops after upgrade

If ubtrace-api or ubtrace-worker restart repeatedly after upgrading to 1.9.0 and the logs show:

UBTRACE_LICENSE_DISABLED must not be enabled in production
(DEPLOY_ENV=production).

then UBTRACE_LICENSE_DISABLED=true is set while running in production (the default from 1.9.0). Remove UBTRACE_LICENSE_DISABLED from your .env (or set it to false) and restart. To run without a license for evaluation, set DEPLOY_ENV=development instead — this is not secure (see the upgrade note above).

Services not starting

# Check service logs
docker compose -f docker-compose-prod.yml logs <service-name>

# Check health status of all services
docker compose -f docker-compose-prod.yml ps

Password authentication failed (stale Docker volumes)

If PostgreSQL or Keycloak fail with FATAL: password authentication failed, this is almost always caused by stale Docker volumes from a previous installation. PostgreSQL only sets passwords on first initialization – changing passwords in .env has no effect on existing database volumes.

Warning

docker system prune -a does NOT remove named volumes.

To fully reset and start fresh:

# Stop all services AND remove named volumes
docker compose -f docker-compose-prod.yml down -v

# Then start again
make up

Private registry TLS issues

If using a self-signed certificate with your private registry:

# Add your CA certificate to Docker's trusted certs
sudo mkdir -p /etc/docker/certs.d/harbor.corp.example
sudo cp ca.crt /etc/docker/certs.d/harbor.corp.example/
sudo systemctl restart docker

Advanced

Private Registry Mirror

For enterprise deployments with a private Docker registry (Harbor, Nexus, GitLab Registry, or Docker Distribution).

Step 1: Set up a private registry (if needed)

docker run -d \
  --name registry \
  --restart=unless-stopped \
  -p 5000:5000 \
  -v registry-data:/var/lib/registry \
  registry:3

For production, configure TLS and authentication per your registry’s documentation.

Step 2: Mirror images

On a machine with internet access and network access to the private registry:

./scripts/offline-mirror.sh \
  --registry harbor.corp.example/ubtrace \
  --version 1.0.0

This pulls all 10 images from public registries, re-tags them with flattened paths under the private registry, and pushes them:

Public image

Mirrored as

postgres:16-alpine

harbor.corp.example/ubtrace/postgres:16-alpine

redis:7-alpine

harbor.corp.example/ubtrace/redis:7-alpine

docker.elastic.co/elasticsearch/elasticsearch:9.3.0

harbor.corp.example/ubtrace/elasticsearch:9.3.0

quay.io/keycloak/keycloak:26.4.0

harbor.corp.example/ubtrace/keycloak:26.4.0

ghcr.io/useblocks/ub-backend-migrate:1.0.0

harbor.corp.example/ubtrace/ub-backend-migrate:1.0.0

ghcr.io/useblocks/ub-backend:1.0.0

harbor.corp.example/ubtrace/ub-backend:1.0.0

ghcr.io/useblocks/ub-frontend:1.0.0

harbor.corp.example/ubtrace/ub-frontend:1.0.0

ghcr.io/useblocks/ub-builder:1.0.0

harbor.corp.example/ubtrace/ub-builder:1.0.0

ghcr.io/useblocks/ub-worker:1.0.0

harbor.corp.example/ubtrace/ub-worker:1.0.0

ghcr.io/useblocks/keycloak-theme:1.0.0

harbor.corp.example/ubtrace/keycloak-theme:1.0.0

Step 3: Configure ubTrace

Set IMAGE_REGISTRY in your .env file:

IMAGE_REGISTRY=harbor.corp.example/ubtrace
UBTRACE_VERSION=1.11.0

Step 4: Deploy

docker compose -f docker-compose-prod.yml up -d

With IMAGE_REGISTRY set, all images are pulled from the private registry instead of public sources.

Verification

# Check images and compose config (no services started)
./scripts/offline-verify.sh --version 1.0.0

# Full verification: start services and wait for health checks
./scripts/offline-verify.sh --version 1.0.0 --up

# With private registry
IMAGE_REGISTRY=harbor.corp.example/ubtrace ./scripts/offline-verify.sh --version 1.0.0

Reference

Bundle Contents

File

Purpose

docker-compose-prod.yml

Production Compose configuration

.env.example

Environment variable template

Makefile

Convenience targets (make up, make down, …)

README-offline.md

This deployment guide

offline-load.sh

Load images from bundle into Docker

offline-verify.sh

Verify images, compose config, health checks

scripts/offline-bundle.sh

Create offline bundle (pull + save + package)

scripts/offline-mirror.sh

Re-tag and push images to a private registry

keycloak/import/ubtrace-realm.json

Pre-configured Keycloak realm (production)

charts/ubtrace-*.tgz

Helm chart for Kubernetes deployments

charts/values.yaml

Default Helm values

charts/values-production.yaml

Production hardening overlay

charts/values-eks.yaml

AWS EKS overlay (ALB ingress, gp3 storage)

charts/values-openshift.yaml

OpenShift-specific overlay

charts/values-minikube.yaml

Minikube local evaluation overlay

charts/values-eks-terraform.yaml

AWS EKS with Terraform overlay

wheels/

ubt_sphinx Python wheels for offline installation (no internet required)

input_src/README.md

Sphinx source directory structure guide

input_build/README.md

Pre-built artifact directory structure guide

Environment Variables

All customer-facing variables. IMAGE_REGISTRY and UBTRACE_VERSION control image sources; the rest configure services. When IMAGE_REGISTRY is empty or unset, images are pulled from their default public registries.

# Image Registry (leave empty for public Docker registries)
IMAGE_REGISTRY=

# Version
UBTRACE_VERSION=1.11.0

# PostgreSQL (Application Database)
POSTGRES_DB=ubtrace
POSTGRES_USER=ubtrace
POSTGRES_PASSWORD=<your-secure-password>
# Schema inside the database. Leave as "public" unless your policy forbids
# it; the database role must be allowed to CREATE in the schema you name.
# Set it before the first start -- changing it later points ubTrace at a new,
# empty schema rather than moving the existing tables.
POSTGRES_SCHEMA=public

# PostgreSQL (Keycloak Database)
KC_DB_DATABASE=keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=<your-secure-password>

# Keycloak (Authentication Server)
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=<your-secure-password>
KC_HOSTNAME=http://localhost:7181
KEYCLOAK_PORT=7181

# Redis (Cache & Sessions)
REDIS_PASSWORD=<your-secure-password>
REDIS_MAXMEMORY=256mb
BACKCHANNEL_LOGOUT_TTL=15m

# Cookie Security (for HTTP / IP-based deployments)
COOKIE_SECURE=true            # Set to false for HTTP-only access (see above)

# Cookie Domain (optional). When the frontend and admin apps live on
# different subdomains (e.g. app.company.com + admin.company.com), set
# this to a leading-dot parent domain like `.company.com` so the session
# cookie is shared across both. Leave empty for single-host deployments.
COOKIE_DOMAIN=

# API Server (NestJS Backend)
API_SERVER_PORT=7150
API_SERVER_PUBLIC_URL=http://localhost:7150
CLOUD_PROVIDER=local
# Deployment mode -- keep 'production' for real deployments (enables license
# enforcement hardening). Only 'development' for local evaluation (NOT
# secure: it also relaxes authentication).
DEPLOY_ENV=production

# OIDC Configuration
OIDC_ISSUER=http://localhost:7181/realms/ubtrace
OIDC_CLIENT_ID=nestjs-app
# IMPORTANT: Must match the secret in Keycloak. Do not change unless you
# also regenerate it in Keycloak (see "Keycloak / OIDC Hardening").
OIDC_CLIENT_SECRET=FnZuseq02pwaNKqsvDxL3jq4HhzPey2b
# Realm role granting administrator access (optional, default: ubtrace-admin).
# Uncomment and set only if your Keycloak realm names the admin role differently.
# ADMIN_ROLE_NAME=ubtrace-admin

# Frontend (Next.js)
FRONTEND_PORT=7155
FRONTEND_PUBLIC_URL=http://localhost:7155
FRONTEND_UBT_URL=http://localhost:7155
# Relative /api so browser requests go through the Next.js same-origin proxy
# (avoids Safari ITP cross-origin cookie issues). Only set an absolute URL if
# you have a reverse proxy that serves both frontend and API on one origin.
UBTRACE_API_URL=/api
# Internal URL for the Next.js proxy to reach the backend (container-to-container).
# Normally set automatically in docker-compose; override only for custom networking.
# UBTRACE_API_INTERNAL_URL=http://ubtrace-api:3000/api
# Direct backend URL for login/logout redirects (browser navigation).
# These cannot go through the proxy — the proxy would follow the Keycloak
# 302 chain server-side instead of in the browser.
# UBTRACE_API_LOGIN_URL=http://localhost:7150/api
# Additional CORS origins (comma-separated URLs, optional)
# CORS_EXTRA_ORIGINS=https://custom-app.example.com,https://other.example.com
CORS_EXTRA_ORIGINS=

# Admin App (Next.js)
ADMIN_PORT=7156
ADMIN_PUBLIC_URL=http://localhost:7156
# Admin app URL for CORS and auth redirects (optional)
ADMIN_UBT_URL=http://localhost:7156

# Elasticsearch (Search & Analytics)
ELASTICSEARCH_PORT=7184
# Host interface the Elasticsearch port is published on (default: loopback).
ELASTICSEARCH_BIND=127.0.0.1
# Security of the BUNDLED Elasticsearch. OFF by default; an upgrade changes
# nothing. Enabling it on an existing deployment also needs the data volume
# recreated -- see README-offline.md.
ELASTICSEARCH_SECURITY_ENABLED=false
# Basic auth for the API and worker. OPTIONAL -- both may stay blank for an
# unauthenticated cluster, which is the default. Set BOTH or NEITHER: a
# half-config is logged as a named configuration error and both halves are
# dropped, never a refused boot.
ELASTICSEARCH_USERNAME=
ELASTICSEARCH_PASSWORD=
ES_JAVA_OPTS=-Xms1g -Xmx1g    # JVM heap size (min 1GB, increase for large projects)

# Ingest Endpoint (Optional)
# Applied end-to-end: backend API AND the Next.js frontend proxy honour
# this limit, so it holds whether you call /api/... directly or through
# the frontend.
INGEST_MAX_FILE_SIZE=524288000  # Max upload size in bytes (default: 500MB)

# Elasticsearch: max bytes per bulk request when indexing doc pages.
# Keep comfortably below ES http.max_content_length (default 100MB).
# On Kubernetes set api.config.elasticsearchBulkMaxBytes and
# worker.elasticsearchBulkMaxBytes in the Helm values.
ELASTICSEARCH_BULK_MAX_BYTES=52428800  # Max ES bulk request size in bytes (default: 50MB)

# Elasticsearch: per-request timeout (1-600000 ms). Raise it where
# Elasticsearch is reached over a high-latency path (tunnel, proxy hop,
# managed service). The readiness health check keeps its own separate
# timeout. On Kubernetes set api.config.elasticsearchRequestTimeoutMs and
# worker.elasticsearchRequestTimeoutMs in the Helm values.
ELASTICSEARCH_REQUEST_TIMEOUT_MS=5000  # Per-request ES timeout in ms (default: 5000)

# Worker Pipeline
WORKER_POLL_INTERVAL_MS=30000   # Worker scanner polling interval (milliseconds)
BUILDER_POLL_INTERVAL=60     # Builder Sphinx build polling interval (seconds)
UBT_COVERAGE_MAX_INPUT_BYTES=1073741824  # Max coverage input (needs.json) size in bytes (default: 1 GiB)

# External issue-tracker integration (Optional, off by default)
# Lets users create issues in their own tracker (GitHub Enterprise Server,
# self-managed GitLab, Jira Data Center) from the documentation view.
# Air-gapped installs leave this off -- the related endpoints return 404.
# Turning it on REQUIRES a valid SERVICE_CONNECTION_ENCRYPTION_KEY: without
# one the API refuses to start rather than storing tracker credentials
# unprotected.
ISSUE_TRACKER_INTEGRATION_ENABLED=false
# Encryption key for stored tracker credentials: 32 random bytes, base64
# (44 characters). Generated per install -- never shipped inside an image or
# bundle. Generate one with either of:
#   docker compose -f docker-compose-prod.yml run --rm --no-deps ubtrace-api gen-key
#   openssl rand -base64 32
# Back it up with your other secrets: there is no escrow and no recovery.
# Replacing or losing it makes every stored credential permanently unreadable
# and every user has to link their connection again.
SERVICE_CONNECTION_ENCRYPTION_KEY=
# Which tracker instances users may connect to. Comma-separated host,
# host:port, or full origin; a bare host means https on the default port
# (443), so write host:port to permit a different one. An IPv6 literal is
# written bracketed: [fd00::1] or [fd00::1]:8443. Wildcards are not
# supported.
# Strongly recommended: the base URL is the one field a user types that the
# server then makes authenticated outbound requests to, so listing your
# handful of trackers keeps a logged-in user from aiming it at an internal
# address.
# Prefix an entry with http:// to allow plaintext for that host only.
# Set but unreadable, the API refuses to start -- an unreadable list must not
# quietly become "no allowlist at all".
# Left empty, loopback, link-local and private addresses are still refused
# (checked after DNS resolution), but any public host becomes reachable.
# On a github.com deployment, allowlist the WEB host github.com: the OAuth
# endpoints live there while the API is on api.github.com, and the derived
# web root is re-checked against this list, fail-closed. Self-managed
# instances serve both from one origin and are unaffected.
#   SERVICE_CONNECTION_ALLOWED_HOSTS=ghe.acme.internal,gitlab.acme.internal:8443
SERVICE_CONNECTION_ALLOWED_HOSTS=

# OAuth connections (optional -- Personal Access Tokens need none of this).
# OAuth requires an OAuth APPLICATION REGISTERED ON YOUR OWN TRACKER
# INSTANCE: a one-time administrator action inside GitHub Enterprise
# (Settings -> Developer settings -> OAuth Apps). ubTrace cannot perform it
# -- the application belongs to your instance and its client id and secret
# are yours. Register it with the callback URL below, then paste the values
# it gives you.
# Leave all four blank to offer Personal Access Tokens only. Setting some
# but not all of them refuses the boot: a half-registered application would
# fail only at the end of a user's first login round-trip.
# The callback URL must match the registered one EXACTLY and must be https.
#   SERVICE_CONNECTION_OAUTH_REDIRECT_URI=https://ubtrace.acme.internal/settings/connections/callback
SERVICE_CONNECTION_OAUTH_REDIRECT_URI=
SERVICE_CONNECTION_GITHUB_OAUTH_CLIENT_ID=
SERVICE_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET=
# Scopes to request; defaults to `repo`, what filing an issue on a private
# repository needs. Use `public_repo` if only public repositories are in
# scope.
SERVICE_CONNECTION_GITHUB_OAUTH_SCOPES=
# Send the PKCE parameters on the authorization request; defaults to true.
# Set to false only if your instance rejects the request with them present.
SERVICE_CONNECTION_GITHUB_OAUTH_PKCE=

# License (required)
UBTRACE_LICENSE_FILE=/data/licenses/license.skm

See docker-compose-prod.yml for all available environment variables including advanced tuning parameters.

License Configuration

A valid license is required to use ubTrace. Without one, all feature API endpoints return 403 Forbidden. Only health checks, authentication, license status, and endpoint-availability endpoints remain accessible.

Contact support@useblocks.com for license activation.

License Status Values

Status

Meaning

Feature Endpoints

valid

License is active and not expired

Allowed

unlicensed

No license configured

Blocked (403)

expired

License was valid but has passed its expiry

Blocked (403)

invalid

License is revoked, blocked, or malformed

Blocked (403)

Offline License Activation

For air-gapped deployments, ubTrace supports offline license validation via .skm activation files:

  1. Place your .skm license file in the licenses/ directory next to docker-compose-prod.yml:

    licenses/
      license.skm
    

    Note

    The .skm file must be in signed activation format containing licenseKey (base64-encoded), signature, and result fields. Contact support@useblocks.com if your activation file uses a different format.

  2. Configure the license in your .env:

    UBTRACE_LICENSE_FILE=/data/licenses/license.skm
    

    The licenses/ directory is mounted read-only into the API server container at /data/licenses/.

  3. Restart the API server:

    make restart-api
    

Verify License Status

Check the current license status via the API:

curl -s http://localhost:7150/api/v1/license/status | python3 -m json.tool

Example responses:

Valid license:

{
  "status": "valid",
  "tier": "licensed",
  "expiresAt": "2026-12-31T23:59:59.000Z",
  "daysUntilExpiration": 288,
  "features": ["all-organizations", "full-access"]
}

No license configured:

{
  "status": "unlicensed",
  "tier": "free",
  "features": []
}

Expired license:

{
  "status": "expired",
  "tier": "free",
  "expiresAt": "2025-01-01T00:00:00.000Z",
  "features": []
}

Note

License changes take effect after restarting the API server:

make restart-api

Deploying Directly from GHCR

Instead of shipping images inside the offline bundle, you can pull them directly from the useblocks GitHub Container Registry (ghcr.io/useblocks). This is the recommended approach when your deployment host has outbound network access to ghcr.io and you want an automated, repeatable pipeline that always pulls a pinned version.

The ubTrace images are private. To pull them you authenticate as the read-only ubpublic service account using a personal access token (PAT) scoped to read:packages. Contact support@useblocks.com to request this token before continuing.

Requesting Access

Contact support@useblocks.com to request a ubpublic access token. The token:

  • authenticates as the ubpublic user,

  • grants read-only (read:packages) access to the ubTrace images, and

  • can be revoked or rotated by useblocks at any time.

Treat the token as a secret. Store it in a credential manager or CI secret store – never commit it to version control.

Authenticating to GHCR

Log in to ghcr.io with the ubpublic username and your token. Pipe the token via stdin so it never lands in your shell history:

echo "$GHCR_TOKEN" | docker login ghcr.io -u ubpublic --password-stdin

In CI, expose the token as a masked secret (e.g. GHCR_TOKEN) and run the same command in a setup step.

Pulling the Images

All ubTrace application images live under ghcr.io/useblocks and are tagged with the release version:

Image

Purpose

ghcr.io/useblocks/ub-backend-migrate

Database migration job

ghcr.io/useblocks/ub-backend

NestJS API server

ghcr.io/useblocks/ub-frontend

Next.js frontend

ghcr.io/useblocks/ub-builder

Sphinx builder

ghcr.io/useblocks/ub-worker

Artifact ingest worker

ghcr.io/useblocks/keycloak-theme

Keycloak login theme

ghcr.io/useblocks/ub-admin

Admin interface

Pull a pinned version:

export UBTRACE_VERSION=1.11.0

for image in ub-backend-migrate ub-backend ub-frontend ub-builder ub-worker keycloak-theme ub-admin; do
  docker pull "ghcr.io/useblocks/${image}:${UBTRACE_VERSION}"
done

The infrastructure images (PostgreSQL, Redis, Elasticsearch, Keycloak) are pulled from their public registries as usual – no authentication required.

Important

Always pin UBTRACE_VERSION to a specific release. Avoid latest in production so deployments stay reproducible.

Configuring Compose to Use GHCR

Set the version in your .env:

UBTRACE_VERSION=1.11.0

The application images in docker-compose-prod.yml already default to ghcr.io/useblocks, so there is no need to set IMAGE_REGISTRY. Leave it unset – setting it would also redirect the infrastructure images (PostgreSQL, Redis, Elasticsearch, Keycloak) to ghcr.io/useblocks, where they do not exist, breaking the pull.

Then start the stack as usual:

docker compose -f docker-compose-prod.yml up -d

Automated Deployment

A typical automated pipeline on the customer side runs these steps:

#!/usr/bin/env bash
set -euo pipefail

export UBTRACE_VERSION=1.11.0

# 1. Authenticate (token injected from a secret store)
echo "$GHCR_TOKEN" | docker login ghcr.io -u ubpublic --password-stdin

# 2. Pull pinned application images
docker compose -f docker-compose-prod.yml pull

# 3. Roll out
docker compose -f docker-compose-prod.yml up -d

# 4. Log out so the token is not left on the host
docker logout ghcr.io

For Kubernetes, create an image pull secret from the same credentials and reference it in your Helm values, following the same global.imageRegistry / global.imagePullSecrets pattern documented in Private Registry:

kubectl create secret docker-registry ubtrace-pull-secret \
  --docker-server=ghcr.io \
  --docker-username=ubpublic \
  --docker-password="$GHCR_TOKEN" \
  --namespace ubtrace

kubectl has no stdin equivalent of --password-stdin, so the token is passed as a flag here. Run this from a CI step with a masked secret rather than an interactive shell, to avoid leaving the token in shell history.

# values overlay
global:
  imageRegistry: ghcr.io/useblocks
  imagePullSecrets:
    - name: ubtrace-pull-secret

Troubleshooting

denied / unauthorized when pulling

The token is missing, expired, or lacks read:packages scope. Re-run docker login with a fresh ubpublic token.

manifest unknown

The requested UBTRACE_VERSION tag does not exist for that image. Verify the version number against your release notes.