Skip to content

Private Registry Deployment

Audience: Administrators deploying Sentari using an internal container registry (Nexus, Harbor, or Artifactory)


This guide covers two scenarios:

  1. Online mirroring — Your registry proxies or mirrors ghcr.io. Servers have outbound internet access (or access to your registry which does).
  2. Air-gap / offline delivery — No outbound internet access at all. Images are transferred via physical media or a one-way file transfer system.

Image inventory

The following images must be available in your registry for a complete Sentari deployment:

Image Source Purpose
ghcr.io/sentari-dev/sentari/server:<version> GitHub Container Registry API server, Celery worker, Beat scheduler
ghcr.io/sentari-dev/sentari/docs:<version> GitHub Container Registry API documentation site
timescale/timescaledb:2.14.2-pg16 Docker Hub Database (recommended)
postgres:16-alpine Docker Hub Database (plain PostgreSQL alternative)
redis:7.2-alpine Docker Hub Task queue broker

Replace <version> with the Sentari release tag (e.g., 1.0.0). Sentari images use the linux/amd64 architecture.


Option 1: Online mirroring via Nexus Docker proxy

This approach lets Nexus act as a pull-through cache. The first pull fetches from ghcr.io; subsequent pulls are served from the Nexus cache without hitting the internet.

1.1 Configure a Nexus Docker proxy repository for ghcr.io

In the Nexus Repository Manager UI:

  1. Navigate to Administration > Repositories > Create Repository.
  2. Select recipe: docker (proxy).
  3. Configure the repository:
Field Value
Name ghcr-proxy
HTTP port 8082 (or leave blank to use the group port)
Remote storage URL https://ghcr.io
Docker Index Use Docker Hub (leave default)
Allow anonymous docker pull Disabled (recommended)
Authentication See step 1.2
  1. Repeat for Docker Hub if you are also proxying timescale/timescaledb and redis:
Name Remote storage URL
dockerhub-proxy https://registry-1.docker.io
  1. Add both proxy repositories to a docker (group) repository (e.g., docker-group) so clients use a single endpoint.

1.2 Configure ghcr.io credentials in Nexus

ghcr.io requires authentication for private repositories:

  1. Generate a GitHub personal access token (PAT) with the read:packages scope.
  2. In Nexus, go to Administration > Security > Anonymous Access and ensure anonymous access is disabled on the group repository.
  3. On the ghcr-proxy repository, set:
  4. Authentication type: Username
  5. Username: your GitHub username (or a service account)
  6. Password: the PAT generated above

Nexus uses these credentials when fetching from ghcr.io on behalf of your clients.

1.3 Pull Sentari images through the proxy

Clients authenticate to Nexus (not to ghcr.io directly):

docker login nexus.internal:8443

docker pull nexus.internal:8443/sentari/sentari/server:1.0.0
docker pull nexus.internal:8443/sentari/sentari/docs:1.0.0
docker pull nexus.internal:8443/timescale/timescaledb:2.14.2-pg16
docker pull nexus.internal:8443/redis:7.2-alpine

Option 2: Air-gap transfer

Use this approach when no network path exists between the transfer workstation and the target environment.

skopeo is the preferred tool because it copies multi-architecture manifests directly between registries without requiring a local Docker daemon and without repackaging images.

Install skopeo on the transfer workstation (requires network access to ghcr.io):

# RHEL / CentOS / Fedora
dnf install skopeo

# Debian / Ubuntu
apt install skopeo

Copy each image directly into your internal registry:

# Sentari server
skopeo copy \
  docker://ghcr.io/sentari-dev/sentari/server:1.0.0 \
  docker://nexus.internal:8443/sentari/sentari/server:1.0.0

# Sentari docs
skopeo copy \
  docker://ghcr.io/sentari-dev/sentari/docs:1.0.0 \
  docker://nexus.internal:8443/sentari/sentari/docs:1.0.0

# TimescaleDB
skopeo copy \
  docker://docker.io/timescale/timescaledb:2.14.2-pg16 \
  docker://nexus.internal:8443/timescale/timescaledb:2.14.2-pg16

# Redis
skopeo copy \
  docker://docker.io/redis:7.2-alpine \
  docker://nexus.internal:8443/redis:7.2-alpine

If the source registry requires authentication:

skopeo copy \
  --src-creds=<github-username>:<pat-token> \
  docker://ghcr.io/sentari-dev/sentari/server:1.0.0 \
  --dest-creds=<nexus-user>:<nexus-password> \
  docker://nexus.internal:8443/sentari/sentari/server:1.0.0

To copy to a directory for physical media transfer instead:

skopeo copy \
  docker://ghcr.io/sentari-dev/sentari/server:1.0.0 \
  dir:/mnt/usb/sentari-images/server-1.0.0

# On the air-gapped side:
skopeo copy \
  dir:/mnt/usb/sentari-images/server-1.0.0 \
  docker://nexus.internal:8443/sentari/sentari/server:1.0.0

2.2 Alternative: docker save / load

Use docker save and docker load when skopeo is not available. Note that this approach produces single-architecture archives.

On the transfer workstation (with internet access):

# Pull all images
docker pull ghcr.io/sentari-dev/sentari/server:1.0.0
docker pull ghcr.io/sentari-dev/sentari/docs:1.0.0
docker pull timescale/timescaledb:2.14.2-pg16
docker pull redis:7.2-alpine

# Export to a tarball
docker save \
  ghcr.io/sentari-dev/sentari/server:1.0.0 \
  ghcr.io/sentari-dev/sentari/docs:1.0.0 \
  timescale/timescaledb:2.14.2-pg16 \
  redis:7.2-alpine \
  | gzip > sentari-1.0.0-images.tar.gz

# Verify the archive
sha256sum sentari-1.0.0-images.tar.gz > sentari-1.0.0-images.tar.gz.sha256

Transfer sentari-1.0.0-images.tar.gz and the .sha256 file to the air-gapped environment via approved transfer procedure.

On the air-gapped server:

# Verify integrity
sha256sum -c sentari-1.0.0-images.tar.gz.sha256

# Load all images
docker load < sentari-1.0.0-images.tar.gz

# Retag for your internal registry
docker tag ghcr.io/sentari-dev/sentari/server:1.0.0 nexus.internal:8443/sentari/sentari/server:1.0.0
docker tag ghcr.io/sentari-dev/sentari/docs:1.0.0   nexus.internal:8443/sentari/sentari/docs:1.0.0
docker tag timescale/timescaledb:2.14.2-pg16         nexus.internal:8443/timescale/timescaledb:2.14.2-pg16
docker tag redis:7.2-alpine                          nexus.internal:8443/redis:7.2-alpine

# Push to the internal registry
docker push nexus.internal:8443/sentari/sentari/server:1.0.0
docker push nexus.internal:8443/sentari/sentari/docs:1.0.0
docker push nexus.internal:8443/timescale/timescaledb:2.14.2-pg16
docker push nexus.internal:8443/redis:7.2-alpine

Configuring Sentari to use your registry

Docker Compose

Set the REGISTRY variable in your .env file:

REGISTRY=nexus.internal:8443/sentari
VERSION=1.0.0

The docker-compose.yml file references ${REGISTRY:-ghcr.io/sentari-dev/sentari} for Sentari images. Database and Redis images are declared with their own image: lines — override them directly in docker-compose.yml if you are mirroring Docker Hub images:

  db:
    image: nexus.internal:8443/timescale/timescaledb:2.14.2-pg16

  redis:
    image: nexus.internal:8443/redis:7.2-alpine

If your Nexus registry requires authentication, log in on the deployment host before running Compose:

docker login nexus.internal:8443

Helm (Kubernetes / OpenShift)

The chart does not ship registry or secrets values files — author your own overrides file (named whatever you like, e.g. my-registry.yaml) or pass the values via --set. Override the registry at install time:

helm install sentari deploy/helm/sentari/ \
  --namespace sentari \
  --set image.registry=nexus.internal:8443 \
  --set database.image.registry=nexus.internal:8443 \
  --set redis.image.registry=nexus.internal:8443 \
  --set secrets.existingSecretName=sentari-credentials

Or place the overrides in your own values file:

# my-registry.yaml
image:
  registry: nexus.internal:8443
  repository: sentari-dev/sentari/server
  tag: "1.0.0"

database:
  image:
    registry: nexus.internal:8443

redis:
  image:
    registry: nexus.internal:8443

Apply (supply secrets via --set, in the same file, or with a pre-created Secret via secrets.existingSecretName):

helm install sentari deploy/helm/sentari/ \
  --namespace sentari \
  -f my-registry.yaml \
  --set secrets.existingSecretName=sentari-credentials

imagePullSecrets (Kubernetes / OpenShift)

If your Nexus registry requires authentication, create a pull secret and reference it in the Helm values:

kubectl create secret docker-registry sentari-pull \
  --namespace sentari \
  --docker-server=nexus.internal:8443 \
  --docker-username=<nexus-user> \
  --docker-password=<nexus-password>

Reference the secret in your overrides file:

imagePullSecrets:
  - name: sentari-pull

The Helm chart propagates imagePullSecrets to all workloads (API, worker, Beat).


Image verification

Sentari release images are signed with Cosign. Verify signatures before loading images into your registry.

Cosign signature verification

Sentari images are signed keyless (Sigstore) by the GitHub Actions release workflow. The OIDC issuer is https://token.actions.githubusercontent.com, and the certificate identity is the release workflow's identity under the sentari-dev/sentari repository. The example below is illustrative; substitute the exact workflow identity for the release you are verifying (obtain it from the release notes or by inspecting the signature with cosign verify in attestation mode):

# Install cosign (https://docs.sigstore.dev/cosign/installation/)
# Replace the identity regexp with the actual release-workflow OIDC identity.
cosign verify \
  --certificate-identity-regexp="^https://github.com/sentari-dev/sentari/\.github/workflows/release\.yml@.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/sentari-dev/sentari/server:1.0.0

A valid signature produces output similar to:

Verification for ghcr.io/sentari-dev/sentari/server:1.0.0 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate claims were validated

Contact your Sentari representative for the release signing key if you require key-pinned verification.

SHA256 digest verification

Release digests are published in the Sentari release notes and at https://github.com/sentari-dev/sentari/releases. Verify before transfer:

# Get the digest from ghcr.io
docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/sentari-dev/sentari/server:1.0.0

# Or with skopeo (no pull required)
skopeo inspect docker://ghcr.io/sentari-dev/sentari/server:1.0.0 \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Digest'])"

Compare the output against the digest published in the release notes.


For Harbor and Artifactory, the workflow is identical to Nexus. The proxy repository configuration UI differs, but the skopeo copy and Helm override steps are unchanged.

For additional assistance, contact your Sentari representative or visit the support portal.