Skip to content

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.

git clone https://gitlab.spacecode.ai/spacecode/preloop-ee-dist.git
cd preloop-ee-dist

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:

curl -fsS http://localhost:6230/api/v1/health
helm -n preloop status preloop

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

  1. Register the first user. It owns its account. After setup, change the REGISTRATION_ENABLED entry 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.
  2. 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 pods app=qwen in namespace models, or edit extraEgress to 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 optional preloop-network ConfigMap:
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:

cloudflared tunnel --url http://localhost:6230

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_registry scope with kubectl describe pod. Recreate an expired pull secret and restart affected pods. docker login on 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_HOSTED unset. Billing starts without Stripe, retains cost tooling and applies no Cloud subscription limits. The console uses frontend-selfhosted, opens login and has no Plan or pricing page. No BILLING_ENFORCE_ENTITLEMENTS setting 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.