Air-Gapped Installation
This guide covers deploying Plexicus to clusters that have no outbound internet access, can only reach an internal container registry, or operate under strict egress policies. The Plexicus umbrella chart is fully air-gap capable — every external dependency is configurable and every optional integration that phones home can be disabled.
The umbrella chart fully supports air-gapped deployments via image mirroring and
pluggable cert-manager issuers. The authoritative step-by-step mirror procedure ships
inside the chart artifact under docs/image-registry.md (extract via helm pull --untar).
This page is a high-level orientation for self-hosted customers — it summarises what
to mirror, what to override, and points at the bundled material. Examples on this page
assume chart 1.2.32 or later.
What Air-Gapped Means Here
In this guide, "air-gapped" refers to any of the following deployment scenarios:
- The Kubernetes cluster has no outbound internet access at all
- The cluster can only reach an internal mirror registry, not the public Google Artifact Registry
- Strict egress policies prevent reaching public OAuth providers, public ACME servers (Let's Encrypt), or external AI APIs
The Plexicus chart is designed to support these environments — every external dependency is configurable, and every optional integration that requires public internet access can be disabled.
High-Level Procedure
-
Mirror all images to your internal registry.
The full image set falls into three categories depending on whether
global.imageRegistryrewrites the pull path.Category A — rewritten by
global.imageRegistry(mirror under the same registry prefix)Image Conditional? europe-west3-docker.pkg.dev/plexicus-registry/platform-prod/fastapi:1.2.36Always on .../worker:1.2.36Always on .../frontend:1.2.36Always on .../analysis-scheduler:1.2.36Always on .../codex-remedium:1.2.36codex-remedium.enabled: true.../exporter:1.2.36exporter.enabled: true.../plexalyzer-code:1.2.36plexalyzer-code.enabled: true.../plexalyzer-prov:1.2.36plexalyzer-prov.enabled: true.../strix:1.2.36strix.enabled: truedocker.io/temporalio/admin-tools:1.29.1-tctl-1.18.4-cli-1.5.0Always on — used by the worker-temporal-setuppost-install Job; also rewritten byglobal.imageRegistry. Mirror it under the same registry prefix as the nine service images.Category B — NOT rewritten by
global.imageRegistry(must be overridden explicitly)Image Where used Override path europe-west3-docker.pkg.dev/.../mongo-seed-data:latestMongoDB seed initContainer Override mongodb.initContainers[0].imagein your values overlay (see step 2)busybox:1.36Every Plexicus service wait-for-dependenciesinitContainerNo per-image override key exists. Set global.dependencies.waitEnabled: falseto skip the wait loop entirely, or pre-loadbusybox:1.36into your mirrorpostgres:15-alpineTemporal admintools and server initContainers Override the entire temporal.admintools.additionalInitContainersandtemporal.server.initContainersblocks in your values overlayBitnami infra images (mongodb, redis, postgresql, minio, os-shell, and sidecars) Bundled subcharts Per-subchart image.registry/image.repository/image.tagoverrides. Warning: Bitnami subchart defaults use:latesttags — pin specific tags (e.g.,mongodb.image.tag: 8.0.8-debian-12-r4) before mirroring to avoid capturing a moving digestTemporal own images (server, ui, admin-tools via temporal subchart values) temporal.enabled: truetemporal.server.image,temporal.admintools.image,temporal.ui.imageCategory C — Temporal's embedded monitoring stack (strongly recommend disabling)
The
temporal-0.73.2.tgzsubchart bundles Prometheus and Grafana enabled by default. Disable them in your overlay to avoid pulling 7 additional images:temporal:
prometheus:
enabled: false
grafana:
enabled: falseIf you need Temporal metrics, mirror all 7 images instead:
grafana/grafana:11.0.0,quay.io/prometheus/prometheus:v2.53.0,quay.io/prometheus/alertmanager:v0.27.0,quay.io/prometheus/node-exporter:v1.8.1,quay.io/prometheus/pushgateway:v1.8.0,registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.12.0,quay.io/prometheus-operator/prometheus-config-reloader:v0.74.0.The bundled-infra overlay (
values-bundled-infra.yaml) already setstemporal.prometheus.enabled: falseandtemporal.grafana.enabled: false.Optional — only needed for
helm testor Temporal test hooksbusybox(no tag, resolves to:latest) — used by every service's helm test pod. No override exists; pre-load into your mirror or skiphelm testin air-gapped environments.docker.io/curlimages/curl:7.85.0— used by Temporal subchart test hooks. Mirror it or note that hook failures do not affect Temporal's runtime operation.
Full procedure: see
docs/image-registry.mdinside the chart artifact (extracted viahelm pull --untar).Bitnami licensing change (August 2025)Bitnami moved their official chart images to a paid distribution. Free legacy images live under
docker.io/bitnamilegacy/*. If you mirror these for air-gapped use, pull frombitnamilegacy/*and re-tag into your internal registry. Production deployments on amd64 Kubernetes can use these images directly. -
Mirror the mongo-seed-data init image separately. The MongoDB seed image (
platform-prod/mongo-seed-data) is an init container passed directly to the Bitnami subchart and is not covered byglobal.imageRegistry. It must be mirrored independently and overridden explicitly in your values overlay:mongodb:
extraVolumes:
- name: shared-init-data
emptyDir: {}
initContainers:
- name: data-extractor
image: "registry.internal.example.com/plexicus/mongo-seed-data:latest"
imagePullPolicy: Always
securityContext:
runAsUser: 0
command: ["sh", "-c",
"(cp -r /seed-data/. /shared-data/ 2>/dev/null || true) && chown -R 1001:1001 /shared-data/ && chmod -R 755 /shared-data/"]
volumeMounts:
- name: shared-init-data
mountPath: /shared-dataReplace
registry.internal.example.com/plexicus/mongo-seed-data:latestwith the path where you mirrored the image fromeurope-west3-docker.pkg.dev/plexicus-registry/platform-prod/mongo-seed-data:latest. The init container is idempotent and can be re-run safely. -
Set
global.imageRegistryin your values overlay. This single field redirects the 9 custom Plexicus service images and thetemporalio/admin-toolsimage used by theworker-temporal-setuppost-install Job. It does not redirectmongo-seed-data,busybox:1.36,postgres:15-alpine, Bitnami subchart images, or Temporal's own monitoring stack (see step 1 Category B above).global:
imageRegistry: "registry.internal.example.com/plexicus"
imagePullSecrets:
- name: my-registry-secret # pull secret for your mirror
security:
allowInsecureImages: true # required when mirror is non-BitnamiLeave
global.imageRegistryempty to pull directly from Plexicus GAR (requires network access and thegar-secretpull secret). Setting it to your mirror removes the runtime dependency on Plexicus's GCP account. -
Replace Let's Encrypt with an internal CA. Override
global.certManager.clusterIssuerto the name of your in-cluster issuer (for example,internal-ca-issuer). Provision the correspondingClusterIssuerresource before runninghelm install.global:
certManager:
enabled: true
clusterIssuer: "internal-ca-issuer" -
Apply the
plexicus-licenseSecret. The license Secret is cluster-local and makes no external network calls — it is mounted as a file at/etc/plexicus/inside thefastapiandworkerpods. Create it in theplexicusnamespace before installing the chart:kubectl -n plexicus create secret generic plexicus-license \
--from-file=license.jwt=/path/to/license.jwt
# add --from-file=license-key.pem=/path/to/license-key.pem if providedIn a fully disconnected environment, transfer the license file(s) to the air-gapped host via your normal secure file-transfer process (USB, scp over a bastion, or a pre-provisioned package). A missing Secret surfaces as
CreateContainerConfigError; a present-but-invalid or expired license surfaces asCrashLoopBackOff(fastapi/workercallSystemExitat startup) — see the license troubleshooting section of the install guide. -
Disable integrations that require public internet. Leave the following keys empty or unset in your values overlay:
Integration Values key(s) to leave empty Cloudflare Turnstile global.required.turnstile.siteKey/global.required.turnstile.secretKeyGoogle OAuth global.required.oauth.google.clientId/global.required.oauth.google.clientSecretPublic OpenAI Use global.required.ai.*to point at a self-hosted or internal Azure OpenAI endpoint insteadAI SAST scanning needs a reachable BAAI/bge-m3 embedding endpointSince chart 1.2.27, every AI SAST scan indexes the target repo with the
BAAI/bge-m3embedding model before scanning it — this is a hard dependency, not an opt-in feature, so an air-gapped cluster with no path to an embedding endpoint has AI SAST silently impossible, not merely degraded. The public default (DeepInfra, for both chat and embeddings) is outside the air gap. You have two options:- Mirror/self-host an embedding-capable model server that serves
BAAI/bge-m3at 1024 dimensions inside your cluster or reachable internal network, and pointworker.envs.PLEXICUS_AI_SCAN_EMBEDDING_BASE_URL(plusPLEXICUS_AI_SCAN_EMBEDDING_API_KEYin theplexicus-workerSecret) at it. - Enable the bundled AI Gateway (
ai-gateway.enabled: true,existingSecret: plexicus-ai-gateway) as a single egress point for all AI traffic, and allow only that one endpoint through your egress policy instead of opening every service to the public internet individually.
Either way,
PLEXICUS_AI_SCAN_EMBEDDING_DIMENSIONS(default1024) MUST match what your embedding endpoint actually outputs — a mismatch corrupts the index silently instead of erroring. See Self-Hosted Installation → Key catalog for the full credential list.Turnstile test tokens are not air-gap safeThe chart ships with Cloudflare's "always passes" test tokens by default (
1x00000000000000000000AA), and the evaluation overlay (values-evaluation.yaml) does not override them. Whenglobal.required.turnstile.siteKeyis non-empty, the frontend loads the Turnstile widget JavaScript fromchallenges.cloudflare.com— an outbound call to Cloudflare. For true air-gapped deployments, explicitly blank both keys in your overlay:global:
required:
turnstile:
siteKey: ""
secretKey: ""With both keys empty the Turnstile widget is skipped and no outbound call is made.
- Mirror/self-host an embedding-capable model server that serves
-
Optionally disable the dependency wait-loop. If your cluster DNS does not resolve the bundled service names before first boot, set:
global:
dependencies:
waitEnabled: falseOr override individual hosts:
global:
dependencies:
redis:
host: "my-redis.internal"
temporal:
host: "temporal.internal" -
Install with Helm (or ArgoCD). Pass your air-gapped values overlay alongside the standard customer overlay:
export CHART_VERSION=1.2.32 # use the version you received from Plexicus
helm install plexicus \
oci://my-internal-registry.example.com/charts/plexicus \
--version $CHART_VERSION \
--namespace plexicus --create-namespace \
-f values.yaml \
-f values-override.yaml
Authoritative Reference
For the complete mirror procedure — every image to mirror, exact tags, registry auth
wiring, and the mongo-seed-data init container override — pull and extract the chart
artifact and read the bundled docs/image-registry.md:
helm pull oci://europe-west3-docker.pkg.dev/plexicus-registry/charts/plexicus \
--version $CHART_VERSION --untar
cat plexicus/docs/image-registry.md
This file is the source of truth, maintained alongside the chart and shipped with the version you installed. For environment-specific questions, contact engineering@plexicus.ai.
Verifying No Phone-Home
After install, verify the cluster makes zero unexpected outbound calls. Suggested checks:
-
NetworkPolicy — two independent layers. Network isolation in Kubernetes operates at two levels, and you may need both:
-
Per-service chart policies (opt-in): The chart ships NetworkPolicy templates for each Plexicus service, controlled by
<service>.networkPolicy.enabled: true. These are disabled by default and add per-pod ingress/egress rules when enabled. Inspect what is currently active with:kubectl get networkpolicy -n plexicus -
Cluster-level default-deny baseline (operator responsibility): In restricted environments, apply a default-deny NetworkPolicy to the
plexicusnamespace before enabling per-service allow rules. Without a default-deny baseline, the per-service allow policies are redundant — all traffic is permitted anyway. The chart does not deploy this cluster-level baseline; operators must apply it separately based on their security posture. A minimal example:apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: plexicus
spec:
podSelector: {}
policyTypes:
- Ingress
- EgressApply the default-deny first, then enable per-service chart policies so that explicit allow rules take effect.
-
-
Managed-SaaS integrations — confirm that any env vars referencing services Plexicus operates on the managed SaaS (commercial integrations) are unset in your values. The chart ships with these disabled by default; verify nothing was accidentally set. See the Configuration Reference for the policy.
-
Egress audit — use a transparent egress proxy or DNS sink to capture any outbound DNS queries during a 1-hour soak after install.
Need Help?
For environment-specific guidance — custom CA chains, FIPS clusters, GovCloud, or air-gapped ArgoCD setups — contact engineering@plexicus.ai with your environment description.