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 |
|
Password |
|
|
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 databaseKC_DB_PASSWORD– Keycloak databaseKEYCLOAK_ADMIN_PASSWORD– Keycloak admin consoleREDIS_PASSWORD– cache and sessionsOIDC_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 noX-Forwarded-Forheader is present, so it cannot be exploited through a proxy.TRUST_PROXY(default: unset)Tells Express how much of an incoming
X-Forwarded-Forchain was written by infrastructure you operate, 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: a hop count (
1behind a single ingress or reverse proxy),loopback,linklocal,uniquelocal, an IP/CIDR (e.g.10.0.0.0/8), or a comma-separated list.A hop count is the only form a Kubernetes deployment can use: the ingress controller’s pod address is neither stable nor knowable in advance, so an address policy names nothing there and every request resolves to the ingress pod – putting all clients into one rate-limit bucket. Left unset, rate limiting keys on the TCP peer, with the same effect for traffic arriving through a proxy.
Warning
Per-client rate limiting requires a reverse proxy in front of ubTrace.
Browsers do not talk to the API directly: the web app and the admin app proxy
/api/* to the API same-origin, which is what keeps session cookies
working in Safari. The API therefore sees the frontend container as its
TCP peer for every browser request, and the frontend cannot append the real
client address – its proxy runtime is not given one. So in a Compose stack
with no reverse proxy in front, all browser traffic shares one rate-limit
bucket per endpoint: a single abusive client can exhaust everyone’s budget,
and no individual client is limited on its own.
Setting TRUST_PROXY is not a fix for that topology and makes it worse.
With nothing in the path that writes X-Forwarded-For, the only value the
API would find in the chain is the one the caller sent, so every request
could mint itself a fresh bucket by changing a header – weaker than one
shared bucket. Set TRUST_PROXY only when a proxy you operate actually
appends the client address to the chain (nginx
$proxy_add_x_forwarded_for, Traefik, Caddy, or a Kubernetes ingress
controller – all of them do by default).
Deployment scenarios:
Scenario |
|
|
Rate-limit granularity |
|---|---|---|---|
Docker Compose, no reverse proxy |
|
(leave unset) |
One shared bucket per endpoint |
Docker Compose behind a reverse proxy (nginx/Traefik) |
|
|
Per client IP |
|
|
(leave unset) |
Bypassed (loopback) |
Kubernetes with ingress (nginx) |
|
|
Per client IP |
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:
Log in to Keycloak admin (
http://localhost:7181,admin/ your admin password)In the left sidebar, click Manage realms, then select ubtrace (you’ll land on the “Welcome to ubtrace” page)
Go to Clients → nestjs-app → Credentials tab → Regenerate Secret
Copy the new secret into
.envasOIDC_CLIENT_SECRETGo to Users and delete the pre-created test user
Restart the API server:
make restart-api
Permissions: the Default Posture¶
A fresh install grants no role to a new user, and three roles see everything. Read this before creating a restricted user – the two facts interact.
Access needs both tables to grant. RolePermissionMapping says what a
role may do; ArtifactPermissionRule says what it may see, and the
artifact read path is default-deny. A role that has a read action but no
visibility rule receives empty results from every artifact endpoint, which
reads as “there is no data” rather than as “you were denied” – so configure
both, not one.
The boot-time seed writes a wildcard visibility rule for ubtrace-admin,
editor and viewer. Each of those three therefore sees every artifact in
every organization. The ubtrace-api-migrate job additionally runs the
canonical, idempotent prisma/seed-permissions.ts seed (migrate + seed) at
startup, the same command the local dev stack runs.
Because of that, the shipped realm assigns no default role
(defaultRoles: []). A newly created user holds nothing and sees nothing
until you assign a role.
Restricting a user¶
Assign only the narrow role, then verify the user does not also hold
viewer, editor or ubtrace-admin – including indirectly, through
Keycloak’s default-roles-ubtrace composite. Visibility rules are a
union: any one of those three re-grants the wildcard, and no rule you add
can narrow it back.
# Authenticate kcadm inside the container (once per shell).
docker compose -f docker-compose-prod.yml exec ubtrace-keycloak \
/opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master \
--user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD"
# What the user actually holds, composites included.
docker compose -f docker-compose-prod.yml exec ubtrace-keycloak \
/opt/keycloak/bin/kcadm.sh get-roles -r ubtrace \
--uusername <user> --effective
Warning
Upgrading an existing install. Releases before this one shipped
defaultRoles: ["viewer"]. Keycloak migrates that list into the
default-roles-ubtrace composite, which already exists in your realm
database, and importing the new realm does not rewrite it. A fresh install
gets the new posture automatically; an upgrade does not. Inspect and
edit the composite once, by hand:
docker compose -f docker-compose-prod.yml exec ubtrace-keycloak \
/opt/keycloak/bin/kcadm.sh get-roles -r ubtrace \
--rname default-roles-ubtrace --effective
docker compose -f docker-compose-prod.yml exec ubtrace-keycloak \
/opt/keycloak/bin/kcadm.sh remove-roles -r ubtrace \
--rname default-roles-ubtrace --rolename viewer
For Kubernetes deployments the same posture applies; see the “Permissions: the
default posture” section of the Helm chart README for the kubectl exec
equivalents.
Importing Artifacts¶
ubTrace uses a worker pipeline that automatically detects new or changed artifacts. No manual rebuild or API server restart is needed.
Option A: Pre-Built CI Output (Recommended)¶
If your CI pipeline already builds with ubt_sphinx, place the output into
input_build/:
# Using the import helper
make import-build SRC=./ci-output ORG=mycompany PROJECT=my-project VERSION=v1
# Or copy manually (SRC must be the build leaf -- see note below)
mkdir -p input_build/mycompany/my-project/v1
cp -r ci-output/* input_build/mycompany/my-project/v1/
The make import-build helper accepts SRC pointing either at the
ubt_sphinx build leaf (the directory that directly contains
docs/ubtrace/needs.json) or at the build root that contains
<org>/<project>/<version>/ underneath; it locates the leaf automatically so
the destination path is never doubled. When copying manually, SRC (here
ci-output) must already be the leaf – otherwise you create a doubled
input_build/{org}/{project}/{version}/{org}/{project}/{version}/ path and the
worker will skip it.
The ub-worker service polls input_build/ every 30 seconds, runs Rust
preprocessing, and loads data into the database.
Required structure (Layout A / ubt_sphinx output):
input_build/{org}/{project}/{version}/
docs/ubtrace/needs.json # required (fixed path)
docs/ubtrace/*.fjson # document files
config/ubtrace_project.toml # may carry an [ubtrace.git] block
config/ubtrace_global_context.json # optional; names the Sphinx that built it
reports/schema_violations.json # optional
Note
reports/schema_violations.json really is optional: a build that does not
run Sphinx-Needs schema validation ships no such file, and that is a
supported setup. Such a version reports Schema violations: missing in
its health record – an expected state, not a failed ingest and not a passing
validation. See “Schema violations: missing” is a supported state.
Important
The fields in config/ubtrace_project.toml (organization,
project_id, version) must exactly match the filesystem path. For
example, if the path is input_build/mycompany/my-project/v1/, the TOML
must have organization = "mycompany", project_id = "my-project",
version = "v1". A mismatch causes the worker to cache incorrect metadata,
resulting in empty API responses or wrong organization assignments.
When building with ubt_sphinx, set these in your conf.py to produce the
correct output:
ubtrace_organization = "mycompany"
ubtrace_project = "my-project"
ubtrace_version = "v1"
Tip
Use an atomic write pattern (staging directory + mv) to avoid
partial-read races with the scanner.
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 |
|
|
A |
|
|
C |
Flat ( |
|
Directory Overview¶
Directory |
Customer-facing |
Purpose |
|---|---|---|
|
Yes (mount) |
Sphinx source files for builder |
|
Yes (mount) |
Pre-built CI output for worker |
|
No (internal) |
Builder output (optionally mountable for debug) |
|
No (internal) |
Worker-processed data: NDJSON, parquet exports, 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+
kubectlconfigured for your clusterDocker 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 |
|---|---|
|
Private registry URL |
|
Application database password |
|
Redis password |
|
Keycloak client secret |
|
API server hostname(s) |
|
Frontend hostname(s) |
Available Values Files¶
File |
Purpose |
|---|---|
|
Base defaults (matches docker-compose-prod.yml) |
|
Production hardening (replicas, resources, PDBs) |
|
AWS EKS settings (ALB ingress, gp3 storage) |
|
OpenShift-specific settings (routes, SCCs, GID 0) |
|
Local evaluation (reduced resources, standard SC) |
|
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.
Warning
Breaking change in 1.13.0 — production mode is decided by the build
From 1.13.0, production mode is anchored to the build, not to your
configuration. Released images are built as production artifacts and carry
that fact inside the compiled bundle, so license enforcement holds on them
whatever DEPLOY_ENV is set to.
The DEPLOY_ENV=development workaround is gone. Documentation
before 1.13.0 suggested it as a way to run without a license for local
evaluation. On a released image it no longer relaxes the license gates, so
combining it with UBTRACE_LICENSE_DISABLED=true crash-loops
ubtrace-api and ubtrace-worker with:
UBTRACE_LICENSE_DISABLED must not be enabled in production (build channel=production, DEPLOY_ENV=production). License validation cannot be disabled in production deployments. Remove UBTRACE_LICENSE_DISABLED or set it to false. A production BUILD stays in production mode whatever DEPLOY_ENV is set to (#1839).
The DEPLOY_ENV= part echoes whatever you have set; the refusal is driven
by build channel=production alone, so development there produces the
same message.
To evaluate ubTrace, request a license from
support@useblocks.com and install it as
described under License Configuration. The stack also starts with
no license at all — health, authentication and license-status endpoints
stay reachable — but every feature API endpoint returns 403 Forbidden
until a license is installed.
Day-to-Day Operations¶
Command |
Description |
|---|---|
|
Show service health |
|
Follow all logs ( |
|
Start all services |
|
Stop all services (preserves data) |
|
Restart the API server only |
|
Import pre-built CI output into |
|
Import Sphinx source into |
|
Run the offline verification script |
|
Pull latest images (requires internet or registry) |
|
Pull latest images and restart |
|
Stop services and delete all data volumes |
|
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.13.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 and
the logs show:
UBTRACE_LICENSE_DISABLED must not be enabled in production (build channel=production, DEPLOY_ENV=production). License validation cannot be disabled in production deployments. Remove UBTRACE_LICENSE_DISABLED or set it to false. A production BUILD stays in production mode whatever DEPLOY_ENV is set to (#1839).
then UBTRACE_LICENSE_DISABLED=true is set while running a released image.
Remove UBTRACE_LICENSE_DISABLED from your .env (or set it to false)
and restart.
Setting DEPLOY_ENV=development does not help: from 1.13.0 a released
image is a production build and stays in production mode whatever
DEPLOY_ENV says, so the DEPLOY_ENV= part of the message above only
echoes your setting. To evaluate ubTrace, request a license from
support@useblocks.com and install it as
described under License Configuration — see the upgrade note above.
Stack does not come up after upgrade: database migration failed¶
ubtrace-api-migrate applies the database migrations and exits; both the API
and the worker wait for it with
condition: service_completed_successfully. If it fails, neither starts.
Check it first:
docker compose -f docker-compose-prod.yml logs ubtrace-api-migrate
P3018 with database error code 42P07
Error: P3018 ... Database error code: 42P07
ERROR: relation "CopilotThread" already exists
Migrations are matched by name, so a database that applied a migration under a
name that has since changed sees it as new and runs its SQL a second time.
Eight ubAI migrations were renamed before release. From this version on the
migrate image repairs this itself – it runs repair-restamped-migrations
before applying migrations, with no action from you. If you still see the
error, the running migrate image predates the fix; upgrade it, or repair by
hand:
docker compose -f docker-compose-prod.yml run --rm --entrypoint sh ubtrace-api-migrate -c \
'npx prisma migrate resolve --rolled-back <migration-name> && npx prisma migrate deploy'
P3009
Error: P3009
migrate found failed migrations in the target database, new migrations will not be applied.
This follows a partial hand repair. Renaming rows in _prisma_migrations
directly is safe only before the failed migration has been attempted;
afterwards the table already holds a failed row for the new name, it has no
unique constraint on the migration name, and the rename adds a second row.
Clear it with the same migrate resolve --rolled-back and migrate deploy
pair 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 images from public registries – 11 by default, plus the two ubAI images (``ub-copilot``, ``parqour``) when ``UBAI_ENABLED=true`` – re-tags them with flattened paths under the private registry, and pushes them:
Public image |
Mirrored as |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The parqour graph engine image is only needed for the optional ubAI
assistant. The script mirrors the ubAI pair only when UBAI_ENABLED=true
(or your .env sets it); without the switch it mirrors 11 images and never
touches the private parqour package – but a bundle published by useblocks
is built with UBAI_ENABLED=true and expects all 13 in your registry, so
set the switch unless you are deliberately running without ubAI.
Important
The parqour image requires authentication. It is published, but
its GitHub Container Registry package is private: an unauthenticated
docker pull (or docker manifest inspect) fails with
401 Unauthorized — which Docker reports as if the tag did not exist, so a
missing credential looks like a missing image. Before mirroring, log in with
a GitHub token that has read:packages and has been granted access to the
useblocks/parqour package:
echo "$GITHUB_TOKEN" | docker login ghcr.io -u <your-github-username> --password-stdin
docker pull ghcr.io/useblocks/parqour:0.11.1 # confirm access before mirroring
Ask useblocks support to grant your GitHub account or organization access to
the package if the pull still returns 401 or denied after logging in.
Once the image is mirrored into your own registry, your deployment pulls it
from there and needs no GHCR credentials at runtime — and an offline bundle
published by useblocks (built with UBAI_ENABLED=true) already contains
it, so the air-gapped path needs no GHCR access at all.
Step 3: Configure ubTrace
Set IMAGE_REGISTRY in your .env file:
IMAGE_REGISTRY=harbor.corp.example/ubtrace
UBTRACE_VERSION=1.13.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 |
|---|---|
|
The image references this bundle carries; |
|
Production Compose configuration |
|
Environment variable template |
|
Convenience targets ( |
|
This deployment guide |
|
Load images from bundle into Docker |
|
Verify images, compose config, health checks |
|
ubAI copilot start gate |
|
Helper |
|
Create offline bundle (pull + save + package) |
|
Re-tag and push images to a private registry |
|
Pre-configured Keycloak realm (production) |
|
Helm chart for Kubernetes deployments |
|
Default Helm values |
|
Production hardening overlay |
|
AWS EKS overlay (ALB ingress, gp3 storage) |
|
OpenShift-specific overlay |
|
Minikube local evaluation overlay |
|
AWS EKS with Terraform overlay |
|
|
|
Sphinx source directory structure guide |
|
Pre-built artifact directory structure guide |
|
Assistant skills directory guide (ubAI; the copilot mounts |
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.13.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. From 1.13.0 the
# license, auth and artifact-RBAC gates are anchored to the BUILD, not to
# this variable: a released image is a production build, so 'development'
# relaxes none of them. It does still relax API rate limits and beta gating.
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. Defaults to ubtrace-admin; set it
# to the name of the realm role your own Keycloak assigns to administrators.
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.
# Any base path on this URL (e.g. the trailing "/api") is preserved by the proxy,
# which appends the request path after it rather than replacing it.
# 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
# How the API and worker authenticate: none | basic | api_key | bearer.
# OPTIONAL -- every variable below may stay blank for an unauthenticated
# cluster, which is the default. Unset with BOTH username and password set
# resolves to `basic`. An explicit `none` overrides that and sends nothing,
# reported at boot naming the variables that will not be sent. A mode
# missing a required field is logged as a named configuration error and
# degrades to `none`, never a refused boot.
ELASTICSEARCH_AUTH_MODE=
ELASTICSEARCH_USERNAME=
ELASTICSEARCH_PASSWORD=
ELASTICSEARCH_API_KEY=
ELASTICSEARCH_BEARER_TOKEN=
# TLS -- independent of the auth mode; only relevant for an https:// node.
# Paths INSIDE the container, so the files must be mounted in.
ELASTICSEARCH_CA_CERT_PATH=
ELASTICSEARCH_CLIENT_CERT_PATH=
ELASTICSEARCH_CLIENT_KEY_PATH=
ELASTICSEARCH_TLS_REJECT_UNAUTHORIZED=true
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)
# Metric Builder: largest version selection one request may resolve to
# (1-1000). Counted in versions, not releases -- a dimensioned release
# carries several variants and each is its own chart point. An over-bound
# selection is cut down and reported, never silently shortened.
METRIC_BUILDER_VERSION_BOUND=1000 # Max versions per metric request (default: 1000)
# 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)
# Usage statistics (Optional, ON by default)
# ubTrace counts feature usage -- documentation reads, traceability and report
# reads, sign-ins -- locally, in its own database. Nothing is sent anywhere: an
# air-gapped install behaves exactly like a connected one, and enabling this
# creates no data-processor relationship with useblocks.
# No individual is ever shown to anybody but themselves: a user sees their own
# figures with no total, rank or comparison, and administrators see adoption
# percentages only, never a person.
# THE OFF SWITCH: set this to false if your deployment must not collect usage
# data at all (for example where a works council has not agreed to it). That
# removes the /v1/usage endpoints -- they answer 404 -- AND stops the counting,
# so nothing per-user is written in the first place. Takes effect at startup.
USAGE_STATS_ENABLED=true
# How many days per-user usage rows are kept before they are collapsed into
# anonymous daily totals. Also the window every reported figure covers, so a
# figure is always a rolling window and never an all-time total. Positive
# integer; the default matches the activity feed's retention.
USAGE_RETENTION_DAYS=90
# The population at or above which an adoption figure is published in the
# administrative usage view. Below it the view reports an explicit "population
# too small" state and no distinct-user count at all, because a percentage over
# a handful of people effectively names someone. Integer of at least 2; a lower
# value is not honoured as a lower threshold and degrades to the default.
USAGE_ADOPTION_MIN_POPULATION=5
# parqour Graph Engine (configuration; enabled by UBAI_ENABLED below)
PARQOUR_URL=http://ubtrace-parqour:7190
PARQOUR_MAX_INSTANCES=10
PARQOUR_MAX_RECORDS=100
PARQOUR_QUERY_TIMEOUT_MS=30000
# Keep authz on: with DEPLOY_ENV=production the API refuses to start when
# graph authz is disabled. Share the SAME key with the parqour service.
PARQOUR_AUTHZ_ENABLED=true
PARQOUR_AUTHZ_SIGNING_KEY=<CHANGE-ME>
PARQOUR_AUTHZ_GRANT_TTL_SECONDS=60
# Redis cache for graph query RESULTS (between the assistant and the engine).
# OFF by default: graph rows are permission-filtered inside the engine per user,
# and nothing downstream re-filters them, so an operator opts in deliberately.
# TTL is capped at 900 seconds by the API itself -- graph rows are requirements
# text, classified Confidential.
GRAPH_QUERY_CACHE_ENABLED=false
GRAPH_QUERY_CACHE_TTL_SECONDS=300
GRAPH_QUERY_CACHE_MAX_ENTRY_BYTES=262144
# The engine's own sidecar-cache key. Required once UBAI_ENABLED=true --
# parqour refuses to START without it, whether or not that cache is
# enabled. A DIFFERENT value from the grant key above. At least 32 bytes.
PARQOUR_SIDECAR_HMAC_KEY=<CHANGE-ME>
# ubAI assistant + graph engine (optional). One switch for both; ``make up``
# starts the extra containers automatically when this is true.
UBAI_ENABLED=false
# LLM provider: openrouter (default), anthropic, openai, google, azure-openai,
# openai-compatible. Only openai-compatible against a server you host keeps all
# data on your own infrastructure -- every other option sends question text and
# retrieved traceability content to a third-party API.
COPILOT_LLM_PROVIDER=
COPILOT_LLM_MODEL=google/gemini-3.8-flash
COPILOT_LLM_API_KEY=
COPILOT_LLM_BASE_URL=
# Reasoning depth the assistant asks the provider for:
# provider-default (default) | off | any depth your MODEL accepts. A higher
# depth improves multi-step retrieval and planning, and costs more reasoning
# tokens. The depth names are NOT a fixed list here -- they belong to the model
# (e.g. low/high/max on some, minimal/low/medium/high on others), so the value
# is passed through and the PROVIDER validates it; a wrong one is reported on
# the first call. No value guarantees zero reasoning -- several current models
# think by default and some cannot be told to stop at all, so `off` is a request
# honoured to varying degrees. On Anthropic an explicit depth is also what makes
# the reasoning visible in the chat.
COPILOT_LLM_REASONING_EFFORT=provider-default
# Langfuse LLM tracing (optional). OFF by default; with LANGFUSE_ENABLED=true
# both keys AND LANGFUSE_BASE_URL are mandatory -- there is no default
# destination, so point LANGFUSE_BASE_URL at your own Langfuse instance to
# keep everything inside your network. When enabled it exports prompts, model
# outputs and retrieved artifact content, plus the requesting user's id and
# the organization/project/version, to LANGFUSE_BASE_URL.
LANGFUSE_ENABLED=false
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_BASE_URL=
COPILOT_API_TOKEN=
# How long a skill catalogue snapshot is reused before the next assistant
# turn re-reads the mounted skills directory, in ms (an expiry, not a timer)
COPILOT_SKILLS_RESCAN_TTL_MS=30000
COPILOT_PORT=4000
# 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 both halves of the
# integration need on a private repository: filing an issue, and PR review
# -- reading a pull request with its changed files and submitting a review
# with comments. Use `public_repo` if only public repositories are in
# scope; nothing narrower still allows submitting a review.
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.
Note
Versions ingested while UBAI_ENABLED=false have no graph export: the
parquet files the assistant queries are written during processing, so nothing
was produced for them. The assistant reports this explicitly (“This version
has no graph export … Re-ingest the version with ubAI enabled to create
one”) rather than answering as if the version held no matching data.
Re-ingest the version – upload it again for the same
organization/project/version – with UBAI_ENABLED=true to create the
export.
Note
The assistant also reads a skills/ directory beside your
docker-compose-prod.yml, mounted read-only into the copilot container.
A skill is a folder of instructions you write once and the assistant then
follows – your release checklist, your naming conventions, the wording your
auditors expect. Anything placed there changes what the assistant does, so
treat write access to it as you treat write access to this configuration.
Some skills ship with ubTrace and are already in the container; currently
exactly one skill ships: traceability. Your directory is read FIRST, so
a folder reusing a shipped skill’s name OVERRIDES the shipped copy, and the
shipped one is what is reported as skipped on
GET /api/v1/copilot/health. Overriding is the supported way to replace a
shipped skill; note that an accidental name collision therefore silently
replaces product behaviour, so check the health endpoint’s rejected list
if behaviour changes unexpectedly. The endpoint returns that list only to
an authenticated caller – without an access token it is the liveness probe
and answers {"status": "ok"} alone.
See Assistant Skills for the file format, the list of agent ids and the exact
request.
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 |
|---|---|---|
|
License is active and not expired |
Allowed |
|
No license configured |
Blocked (403) |
|
License was valid but has passed its expiry |
Blocked (403) |
|
License is revoked, blocked, or malformed |
Blocked (403) |
Offline License Activation¶
For air-gapped deployments, ubTrace supports offline license validation via
.skm activation files:
Place your
.skmlicense file in thelicenses/directory next todocker-compose-prod.yml:licenses/ license.skm
Note
The
.skmfile must be in signed activation format containinglicenseKey(base64-encoded),signature, andresultfields. Contact support@useblocks.com if your activation file uses a different format.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/.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
ubpublicuser,grants read-only (
read:packages) access to the ubTrace images, andcan 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 |
|---|---|
|
Database migration job |
|
NestJS API server |
|
Next.js frontend |
|
Sphinx builder |
|
Artifact ingest worker |
|
Keycloak login theme |
|
Admin interface |
Pull a pinned version:
export UBTRACE_VERSION=1.13.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.13.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.13.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/unauthorizedwhen pullingThe token is missing, expired, or lacks
read:packagesscope. Re-rundocker loginwith a freshubpublictoken.manifest unknownThe requested
UBTRACE_VERSIONtag does not exist for that image. Verify the version number against your release notes.