Install Preloop Enterprise on minikube or Kubernetes¶
Use the Enterprise API and console images with the OSS Helm chart. This guide is for a single-node trial, with one flow at a time. For production, use an external managed PostgreSQL 16 database with pgvector or CloudNativePG, tested backups, TLS ingress, and a cluster administrator's resource and network review. The Compose alternative appears below in the distribution README.
Release example: 0.17.0-rc.1, Linux amd64. The registry images are not multi-platform. On an arm64 Mac, use locally built arm64 images for minikube as described below. Pull credentials do not grant access to the Enterprise source.
Get the install bundle¶
You need Docker, kubectl, Helm 3, minikube, 4 CPUs, 8 GB available to minikube,
and disk space for images and a 5 GiB database volume. A local model needs
additional memory outside this budget. You also need your read-only GitLab
deploy token with read_registry, and access to the distribution repository.
Use repository read credentials separately if your registry token lacks
read_repository.
Use chart.tgz from a released bundle. It includes the OSS chart and its NATS
dependency, pinned to the release's OSS commit in RELEASE.json. The chart
version can differ from the Enterprise image version. For this first release's
source checkout, produce that same package with:
# From an EE source checkout with its preloop submodule initialized:
helm dependency build preloop/helm/preloop
helm package preloop/helm/preloop --destination /tmp
# Copy /tmp/preloop-<chart-version>.tgz into the bundle as chart.tgz.
Start minikube and create secrets¶
Calico enforces the agent NetworkPolicy. The default minikube CNI may accept
NetworkPolicy objects without enforcing them. Use a separate kube context for
this trial, and keep the namespace name preloop for the commands below.
minikube start -p apw-k8s --driver=docker --cpus=4 --memory=8192 --cni=calico
kubectl config use-context apw-k8s
kubectl create namespace preloop
# Enter your deploy token without putting its password in shell history.
read -r REGISTRY_USER
read -rs REGISTRY_PASSWORD; echo
kubectl -n preloop create secret docker-registry registry-pull \
--docker-server=registry.spacecode.ai \
--docker-username="$REGISTRY_USER" --docker-password="$REGISTRY_PASSWORD"
unset REGISTRY_PASSWORD
# Flow Jobs use the namespace default ServiceAccount, not the chart account.
# Retain any other pull secrets if you adapt this to an existing namespace.
kubectl -n preloop patch serviceaccount default --type=merge \
-p '{"imagePullSecrets":[{"name":"registry-pull"}]}'
# Keep generated credentials outside values and version control.
# Hex avoids URL-encoding problems in the connection string.
POSTGRES_PASSWORD=$(openssl rand -hex 24)
JWT_SECRET=$(openssl rand -hex 32)
kubectl -n preloop create secret generic preloop-app \
--from-literal=jwt-secret="$JWT_SECRET" \
--from-literal=postgres-password="$POSTGRES_PASSWORD" \
--from-literal=database-url="postgresql+psycopg://postgres:$POSTGRES_PASSWORD@postgres:5432/preloop"
unset POSTGRES_PASSWORD JWT_SECRET
Secrets are namespaced. If you later move Jobs to a separate execution namespace,
create registry-pull there too and patch that namespace's default account.
The chart's imagePullSecrets applies to API, console, workers and Helm hook
Jobs, but it does not configure dynamically created agent pods.
Install the signed Enterprise licence¶
Store the signed licence supplied by Preloop in a private local file, then create its Kubernetes Secret without putting the licence in command arguments:
kubectl -n preloop create secret generic preloop-license \
--from-file=PRELOOP_EE_LICENSE_KEY=/secure/path/license.key
The supplied values-minikube.yaml includes an optional preloop-license
secretRef in extraEnvFrom, alongside the optional preloop-network
ConfigMap. Keep both entries in any private override. The chart passes these
to backend processes and hooks. Without a valid licence, licensed capabilities
are unavailable. After renewing the Secret, restart the backend deployments to
load the changed environment. A superuser can check /api/v1/licence/status.
Install and migrate¶
The provided database is a trial StatefulSet with a PVC. It is deliberately
separate from the chart, so there is no CloudNativePG operator prerequisite.
The values use its URL from preloop-app, disable ingress and autoscaling,
start the Jobs executor and limit orchestration to one concurrent flow.
kubectl -n preloop apply -f postgres-minikube.yaml
kubectl -n preloop rollout status statefulset/postgres --timeout=5m
helm upgrade --install preloop ./chart.tgz -n preloop \
-f values-minikube.yaml --timeout 15m
kubectl -n preloop rollout status deployment/preloop-api --timeout=5m
kubectl -n preloop rollout status deployment/preloop-console --timeout=5m
kubectl -n preloop get pods,jobs,pvc
kubectl -n preloop port-forward svc/preloop-console 6230:80
Open http://localhost:6230. In another terminal:
The post-install hook runs scripts/init_db.py --force, which upgrades the
schema, then synchronizes flow presets. Successful hooks are deleted; Helm
reports a successful release only after they complete. Do not use --wait on
the initial install: application readiness can depend on the schema the
post-install hook creates. Inspect failed hooks with
kubectl -n preloop logs job/preloop-init-db-job. On an upgrade, the
pre-upgrade migration hook runs the lock-aware migration wrapper first.
arm64 source checkout test¶
An Enterprise source checkout can build native images without pushing them. The distributed amd64 images can instead be smoke-tested under Compose's emulation, but that does not validate them on arm64 Kubernetes.
# From the EE source checkout, not the distribution checkout:
docker build -f Dockerfile -t preloop-ee-apw:230 .
docker build -f Dockerfile.frontend --build-arg BRAND=preloop-selfhosted -t preloop-console-apw:230 .
minikube -p apw-k8s image load preloop-ee-apw:230
minikube -p apw-k8s image load preloop-console-apw:230
# Keep a registry-pull secret even for preloaded images. Dummy credentials
# may be used for this local-only test; they do not test registry authorization.
helm upgrade --install preloop ./preloop/helm/preloop -n preloop \
-f distribution/values-minikube.yaml --timeout 15m \
--set image.repository=preloop-ee-apw --set image.tag=230 \
--set console.repository=preloop-console-apw --set console.tag=230
Register, connect GitLab, run a flow¶
- Register the first user. It owns its account. After setup, change the
REGISTRATION_ENABLEDentry in your private values copy to"false", upgrade with that file, and invite colleagues from the console. Configure SMTP for invitations, password resets and email approvals. - In Models, add a provider credential and enable gateway routing on the
model used by your flow. For an OpenAI-compatible in-cluster Qwen service,
use
http://qwen.models.svc.cluster.local:8000/v1. Label its actual podsapp=qwenin namespacemodels, or editextraEgressto match them. The provided rule allows agent pods to that service's backend pods on TCP 8000; DNS and the Preloop API/gateway are allowed separately. The gateway also needs access to Qwen. This policy isolates agent pods, not the gateway. Separately, the model endpoint SSRF guard denies private addresses until an operator allowlists them. Allow only this Service IP, then restart the application Deployments so they read the optionalpreloop-networkConfigMap:
MODEL_IP=$(kubectl -n models get service qwen -o jsonpath='{.spec.clusterIP}')
kubectl -n preloop create configmap preloop-network \
--from-literal=MODEL_ENDPOINT_ALLOWED_CIDRS="$MODEL_IP/32" \
--dry-run=client -o yaml | kubectl -n preloop apply -f -
kubectl -n preloop rollout restart deployment -l app.kubernetes.io/instance=preloop
kubectl -n preloop rollout status deployment/preloop-api --timeout=5m
Do this before saving the model. Preserve the ConfigMap, and update its IP if
the model Service is recreated. Do not allow all private ranges just to
bypass the endpoint guard.
3. In Trackers, add GitLab, your instance URL, and a dedicated bot user's
personal access token (or a group access token). Use api, read_repository
and write_repository, with Maintainer access on the selected test project.
Project access tokens cannot list groups. Complete the first sync. Confirm
the webhook under GitLab Settings > Webhooks points at this installation.
4. In Flows, create a small manually triggered flow with your configured
model and no repository writes, for example "Reply with INSTALL_OK". Trigger
it, wait for a completed execution, and check its output. Then create a
Pull Request Reviewer preset, bind the synced project, and test an MR
opened/updated webhook.
5. Inspect a running agent pod. Kubernetes adds the default account's pull
secret at admission, so inspect the pod, not only the Job template:
kubectl -n preloop get pods -l app=agent-execution \
-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.imagePullSecrets}{"\n"}{end}'
kubectl -n preloop get jobs -l app=agent-execution
kubectl -n preloop logs deployment/preloop-preloop-sync-worker-flow-execution
Agent images also need to support your node architecture. A private agent image
needs a token authorized for its registry, added to the same ServiceAccount.
image.repository and console.repository do not change the agent image.
Use the chart's agents.codexImage override if your operator supplies one.
Expose the trial to self-hosted GitLab¶
A port-forward is local to the laptop. GitLab cannot send a webhook to your
localhost. Keep the port-forward running and expose it using your approved
HTTPS tunnel, for example:
Use the resulting HTTPS hostname as environment.preloopUrl in your private
values file and repeat helm upgrade --install with all the same image and
secret overrides. A stable named tunnel is preferable: changing the hostname
requires updating tracker webhooks. GitLab must resolve and reach that URL and
trust its certificate; Preloop and agent pods must reach the GitLab API and git
HTTPS endpoint. Permit WebSocket upgrades for live execution logs and keep
streaming proxy timeouts at least 900 seconds. No public tunnel was used for
the local acceptance test.
For a private GitLab address, an administrator may need to allow local-network
webhook requests. The agent policy excludes private ranges from general
internet egress. Add a narrow extraEgress peer and port 443 for your GitLab's
private CIDR; do not remove the database isolation to make it work. For a private
CA, mount it using extraVolumes/extraVolumeMounts and set SSL_CERT_FILE and
REQUESTS_CA_BUNDLE with extraEnv. Use a TLS ingress instead of a laptop tunnel
for a persistent installation.
Upgrade, back up and restore¶
Back up before upgrading, keep the old image version and chart, and preserve
preloop-app (JWT and encryption material) in your secret manager. The database
contains tracker credentials and audit data. Protect backups accordingly.
kubectl -n preloop exec postgres-0 -- pg_dump -U postgres -d preloop -Fc > preloop.dump
# Check the archive can be read. Rehearse a restore into a separate trial DB.
kubectl -n preloop exec -i postgres-0 -- pg_restore --list < preloop.dump
Fetch the next release bundle, review schema compatibility, update both
image.tag and console.tag in your private values file, and run:
helm upgrade --install preloop ./chart.tgz -n preloop \
-f values-minikube.yaml --timeout 15m
kubectl -n preloop rollout status deployment/preloop-api --timeout=5m
curl -fsS http://localhost:6230/api/v1/health
Keep every private override on each upgrade. The migration hook handles schema
upgrades; Helm rollback does not undo a schema change. If a release requires
recovery, stop all API/gateway/workers/scheduler/monitor Deployments, restore
into an empty compatible PostgreSQL database using pg_restore --no-owner,
restore the original secrets, and reinstall the matching old chart/images.
Point database.urlFromSecret at the restored DB. Validate health and a flow
before reopening traffic. For production use automated backups with restore
drills and a retention policy, not only this manual dump.
helm uninstall preloop -n preloop removes the application, not the separate
PostgreSQL StatefulSet. Do not delete PVCs or minikube delete -p apw-k8s until
you have exported data you intend to keep.
Compose alternative and troubleshooting¶
For a single Docker host, use the distribution's compose.yml, Caddyfile and
.env.example; its README includes the complete Compose instructions. This
uses Docker containers for flow execution rather than Kubernetes Jobs.
- ImagePullBackOff: check the image tag, node architecture, secret namespace,
token expiry and
read_registryscope withkubectl describe pod. Recreate an expired pull secret and restart affected pods.docker loginon your laptop does not configure Kubernetes credentials. - Hook fails: inspect the hook Job's logs and database readiness; check the secret URL and pgvector availability. Do not bypass a failed migration.
- Flow fails: inspect the flow worker and agent pod logs, model gateway routing, ServiceAccount pull secret, agent image architecture, and policy selectors/ports. A rendered policy alone is not evidence of enforcement.
- Billing mode: leave
PRELOOP_HOSTEDunset. Billing starts without Stripe, retains cost tooling and applies no Cloud subscription limits. The console usesfrontend-selfhosted, opens login and has no Plan or pricing page. NoBILLING_ENFORCE_ENTITLEMENTSsetting is required.
Console metadata on a custom domain¶
The preloop-selfhosted brand's domain in frontend/brands.yaml is a
build-time value. It defaults to localhost because the distributable image
cannot know an operator's hostname. The console uses relative application URLs,
but generated public-page canonical URLs, sitemap, robots and other discovery
metadata use that build-time domain. PRELOOP_URL does not rewrite those static
files. If these public metadata files matter for your installation, set the
brand's domain to your hostname and rebuild Dockerfile.frontend with
--build-arg BRAND=preloop-selfhosted before loading the console image.