LiquidCompute is the self-hosted "Render.com for GPUs" UI and control plane that sits on top of the GreenThread engine. It adds a Service abstraction, Projects, custom domains, a unified /v1/* OpenAI-aggregator endpoint, and a live Fleet view across GPUs.
It is optional — the engine alone is a complete platform if all you need is kubectl + per-Model URLs. Install LiquidCompute when you want a UI, multi-tenant Projects, or one OpenAI endpoint that fronts every Model in the cluster.
What's in the box
| Workload | Role |
|---|---|
| lcd | Control-plane Go service. Owns Projects, Services, custom-domain verification, fleet aggregation, the /api/* surface the SPA talks to. |
| lc-proxy | OpenAI-compatible aggregator. Single /v1/chat/completions, /v1/embeddings, etc. — routes by request model to the right engine Model. Also hosts the async batch dispatcher. |
| frontend | React/Vite SPA. Projects board, Services UI, Fleet view, Metrics. |
| postgres | Bundled by default; flip postgres.bundled.enabled=false and supply a DSN to use external Postgres (CloudNativePG, RDS, etc.). |
Key concepts
Project
A namespace-shaped grouping. Every Service belongs to one Project. The Project's slug becomes part of every emitted Kubernetes object's labels (liquidcompute.io/project=<slug>).
Service
A unified abstraction over both inference Models (tier=inference) and generic CPU / GPU container workloads (tier=cpu, tier=gpu). The Service spec is small and constrained — lcd compiles it into the right Kubernetes objects (Model CR, Deployment + Service, Ingress, Certificate).
Fleet
A live view that joins the engine's GPU CRDs (per physical device, with mode + health + available VRAM) into a single dashboard. No new state — just an aggregation.
Custom domain
Bring your own app.example.com. lcd issues a TXT challenge, you publish it, lcd verifies, and the next Service apply threads the hostname into its Ingress + cert-manager Certificate.
Install
LiquidCompute assumes the engine, an ingress controller, and cert-manager are already installed. See Prerequisites and Install GreenThread first.
Pull credentials
The chart and images live on licence.greenthread.ai (see Licensing › Pulling charts and images). If you already ran helm registry login for the engine install you can skip that step — credentials are stored per-workstation, not per-chart. You'll still need an image-pull Secret in the LC namespace:
kubectl create namespace liquidcompute-system
kubectl create secret docker-registry greenthread-registry \
-n liquidcompute-system \
--docker-server=licence.greenthread.ai \
--docker-username=licence \
--docker-password=<your-licence-token>
Helm install
helm upgrade --install liquidcompute \
oci://licence.greenthread.ai/greenthread/charts/liquidcompute \
--namespace liquidcompute-system \
--set 'global.imagePullSecrets[0].name=greenthread-registry' \
--set 'images.lcd.repository=licence.greenthread.ai/greenthread/liquidcompute/lcd' \
--set 'images.lcProxy.repository=licence.greenthread.ai/greenthread/liquidcompute/lc-proxy' \
--set 'images.frontend.repository=licence.greenthread.ai/greenthread/liquidcompute/frontend' \
--set 'console.host=app.example.com' \
--set 'global.platformDomain=example.com' \
--set 'certManager.issuer=letsencrypt-production' \
--set 'ingress.class=nginx' \
--set 'ingress.platformService.namespace=ingress-nginx' \
--set 'ingress.platformService.name=ingress-nginx-controller' \
--set 'lcd.auth.enabled=true' \
--set 'lcd.auth.username=admin' \
--set 'lcd.auth.password=<generate-a-password>'
| Value | Purpose |
|---|---|
console.host | Hostname the SPA + /api/* are served at. |
global.platformDomain | Wildcard suffix for auto-generated Service subdomains (e.g. whisper-prod.example.com). |
certManager.issuer | cert-manager (Cluster)Issuer used for every emitted Certificate. |
ingress.class | IngressClass lcd stamps on emitted Ingress objects (nginx, traefik, etc.). |
ingress.platformService.{namespace,name} | The shared cluster ingress Service — lcd surfaces this in the UI as the CNAME target for custom domains. |
lcd.auth.username / password | Single shared admin login for v1 (multi-user / SSO are tracked separately). |
postgres.bundled.enabled | true (default) bundles Postgres in-chart. Set false + postgres.dsn=... to use external. |
storage.defaultStorageClass | StorageClass for per-Service /workspace PVCs. Empty → cluster default. |
Verify
# All pods Running
kubectl get pods -n liquidcompute-system
# Ingress has an address
kubectl get ingress -n liquidcompute-system
# lcd is reachable
kubectl -n liquidcompute-system port-forward svc/liquidcompute-lcd 8080:8080 &
curl http://localhost:8080/api/v1/fleet | jq '.gpus | length'
A fresh install on a 4-GPU dev cluster:
$ kubectl get pods -n liquidcompute-system
NAME READY STATUS AGE
liquidcompute-frontend-58fdcfdbcd-65qlv 1/1 Running 23h
liquidcompute-frontend-58fdcfdbcd-wfk5r 1/1 Running 23h
liquidcompute-lc-proxy-8574f5d469-k7h24 1/1 Running 23h
liquidcompute-lc-proxy-8574f5d469-spfmv 1/1 Running 23h
liquidcompute-lcd-565f79b8f7-rfwkt 1/1 Running 23h
liquidcompute-lcd-565f79b8f7-zqjmr 1/1 Running 23h
liquidcompute-postgres-6cd87d78f4-5zqdt 1/1 Running 2d5h
$ kubectl get ingress -n liquidcompute-system
NAME CLASS HOSTS ADDRESS PORTS AGE
liquidcompute-console nginx app.example.com 203.0.113.10 80, 443 20d
Using the API
Once console.host resolves, open the URL in a browser and sign in with the admin credentials. Or drive it from the command line:
Create a Project
LCD=https://app.example.com/api
curl -X POST $LCD/v1/projects \
-H 'Content-Type: application/json' \
-d '{"name":"Default","slug":"default"}'
Deploy a Model via Service
A Service with tier=inference is compiled into a Model CR in the matching Project namespace:
curl -X POST $LCD/v1/projects/default/services \
-H 'Content-Type: application/json' \
-d '{
"name":"llama-3-8b",
"tier":"inference",
"kind":{"model":{
"modelName":"meta-llama/Llama-3.1-8B-Instruct",
"dtype":"bfloat16",
"endpoints":["chat_completions","completions"]
}}
}'
Deploy a CPU service
curl -X POST $LCD/v1/projects/default/services \
-H 'Content-Type: application/json' \
-d '{
"name":"hello","tier":"cpu",
"kind":{"container":{
"image":"hashicorp/http-echo:latest",
"command":["/http-echo","-text=hi","-listen=:8080"],
"ports":[{"port":8080,"name":"http"}]
}}
}'
Call inference through lc-proxy
lc-proxy exposes the OpenAI aggregator at the same hostname as lcd, under /v1/*. The model field in the request body is the upstream model ID (the HuggingFace name on the Model spec):
# Discover what's addressable
curl https://app.example.com/v1/models | jq '.data[].id'
# Send a chat completion
curl https://app.example.com/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model":"meta-llama/Llama-3.1-8B-Instruct",
"messages":[{"role":"user","content":"Hello!"}]
}'
Unlike the engine's per-Model URLs (/<model>/v1/*), lc-proxy exposes a single /v1/* surface and routes by the model field — exactly like OpenAI's API. Apps written against OpenAI need only a base_url change. The model value matches whatever Model.spec.modelName is set to (the upstream HuggingFace ID), so swapping providers is a one-line change.
Custom domains
# 1. Register the domain → server returns a TXT challenge.
curl -X POST $LCD/v1/domains \
-H 'Content-Type: application/json' \
-d '{"fqdn":"chat.acme.com","projectSlug":"default","serviceName":"llama-3-8b"}'
# → { "challengeRR": "_lc-challenge.chat.acme.com", "challengeToken": "abc..." }
# 2. Publish the TXT record at your DNS provider.
# 3. Verify.
curl -X POST $LCD/v1/domains/<id>/verify
After verification, the next Service apply rewrites its Ingress to include chat.acme.com and emits a cert-manager Certificate. The customer's CNAME for chat.acme.com should point at the platform's shared ingress Service (surfaced in the UI under Domains).
Where to next
- GreenThread engine — what LiquidCompute is built on
- AI Console — the layer above LiquidCompute that exposes per-customer API keys + a customer-facing UI
- Licensing — token + Secret + offline JWT cache the engine enforces
