GPU-accelerated embedding server in a box.
Part of CueCrux — private, provider-independent embeddings via Tailscale mesh.
HuggingFace TEI + a thin gateway + Tailscale mesh networking, single docker compose up.
EmbedderCrux is a self-contained Docker appliance that turns any NVIDIA GPU into a private embedding endpoint on your Tailscale network. It pairs HuggingFace Text Embeddings Inference (TEI) with a small HTTP gateway and a Tailscale sidecar so your infrastructure can call it like any internal service: no public ports, no API keys to rotate, no vendor lock-in.
Built to power the embedding pipeline for VaultCrux, but useful for anyone who wants fast, private, provider-independent embeddings without sending data to a third-party API.
The stack publishes the gateway locally (127.0.0.1:8080 by default). /embed remains TEI-compatible. /embed/sequence is available for Engine late-chunking; by default it returns 501 so Engine can fall back to per-chunk embeddings unless sequence mode is explicitly enabled.
Hosted embedding APIs are convenient, but they bill per token and can deprecate models with little notice. Running embeddings locally is usually faster and removes per-token costs, yet securely exposing a GPU endpoint to remote infrastructure is where most teams get stuck. EmbedderCrux solves that network problem by combining TEI with Tailscale mesh networking, so your GPU appears as a private internal service on your tailnet.
- NVIDIA GPU with CUDA support
- NVIDIA Container Toolkit installed (
nvidia-ctk) - Docker Engine + Docker Compose v2
- Tailscale account (free tier works)
- HashiCorp Vault access to a KVv2 path
curl+jqon the host (used byscripts/compose-up-from-vault.sh)
-
Clone the repo:
git clone https://github.com/CueCrux/EmbedderCrux.git cd EmbedderCrux -
Create a Tailscale OAuth client: https://login.tailscale.com/admin/settings/oauth
- Define
tag:embedderin your ACLtagOwnersfirst. - Create an OAuth client scoped for tag-based node auth.
- Define
-
Write OAuth creds to Vault KV:
vault kv put kv/app/embeddercrux/prod \ ts_client_id="your-client-id" \ ts_client_secret="your-client-secret"
-
Create your runtime config:
cp .env.example .env
-
Start the appliance:
./scripts/compose-up-from-vault.sh
The launcher starts GPU TEI first and only falls back to CPU TEI if GPU startup/health fails.
-
Verify service health and embeddings:
./healthcheck.sh curl http://localhost:8080/embed \ -X POST \ -H 'Content-Type: application/json' \ -d '{"inputs":["Hello world"]}' curl http://embedder:8080/embed \ -X POST \ -H 'Content-Type: application/json' \ -d '{"inputs":["Hello world"]}'
| Variable | Description | Default |
|---|---|---|
TS_HOSTNAME |
Hostname shown for this node in Tailscale | embedder |
TS_TAG |
ACL tag name used for --advertise-tags=tag:<value> |
embedder |
VAULT_ADDR |
Vault API address used by launcher script | http://100.74.157.35:8200 |
VAULT_SKIP_VERIFY |
Set true to skip TLS verification when using self-signed certs |
true |
VAULT_TOKEN_FILE |
File containing Vault token (used when VAULT_TOKEN is unset) |
${HOME}/.vault-token |
VAULT_SECRET_PATH |
KVv2 path containing ts_client_id and ts_client_secret |
kv/app/embeddercrux/prod |
MODEL_ID |
HuggingFace embedding model ID | nomic-ai/nomic-embed-text-v1.5 |
TEI_IMAGE_TAG |
GPU TEI image variant for your accelerator architecture | cuda-1.9 |
TEI_CPU_IMAGE_TAG |
CPU TEI image used only for fallback startup path | cpu-1.9 |
ALLOW_CPU_FALLBACK |
If true, launcher switches to CPU TEI when GPU health fails |
true |
TEI_HEALTH_TIMEOUT_SECONDS |
Health-check timeout before GPU is treated as failed startup | 300 |
MAX_BATCH_TOKENS |
TEI max tokens per dynamic batch | 16384 |
MAX_CLIENT_BATCH_SIZE |
Max input strings accepted per /embed request |
64 |
MAX_CONCURRENT_REQUESTS |
TEI max in-flight requests | 512 |
EMBEDDER_LOCAL_BIND |
Host/IP bind for optional local port publishing | 127.0.0.1 |
EMBEDDER_HOST_PORT |
Host port mapped to TEI :8080 |
8080 |
EMBEDDER_SEQUENCE_MODE |
/embed/sequence mode: disabled or synthetic |
disabled |
EMBEDDER_SEQUENCE_MAX_TOKENS |
Max tokens allowed for synthetic /embed/sequence |
384 |
EMBEDDER_TOKEN_BATCH_SIZE |
Batch size for synthetic token embedding calls | 64 |
Use a least-privilege ACL so only infrastructure nodes can call the embedding endpoint:
{
"tagOwners": {
"tag:embedder": ["autogroup:admin"],
"tag:infra": ["autogroup:admin"]
},
"acls": [
{
"action": "accept",
"src": ["tag:infra"],
"dst": ["tag:embedder:8080"]
}
]
}With this policy, only nodes tagged tag:infra can reach tag:embedder on port 8080.
| GPU Family | Examples | TEI Image Tag |
|---|---|---|
| Broad CUDA default | Mixed/unknown modern NVIDIA fleets | cuda-1.9 |
| Ada Lovelace | RTX 4000 Ada, RTX 4090, L4 | 89-1.9 |
| Ampere | A10G, A100, RTX 3090 | 86-1.9 (A10G / SM86) or 1.9 (A100 / SM80) |
| Blackwell / Hopper | RTX PRO 6000, B100, H100 | cuda-1.9 |
| Turing | T4, RTX 2080 | turing-1.9 |
| CPU (testing) | Any | cpu-1.9 |
Compatibility note: older TEI examples often used 86-1.9 as the default for Ada and Ampere cards. This repo defaults to cuda-1.9 for wider compatibility; if you want architecture-specific builds, set TEI_IMAGE_TAG explicitly (for example 89-1.9 or 86-1.9).
CPU fallback note: CPU TEI is profile-gated and only activated by the launcher fallback path (or by explicitly starting tei-cpu). When GPU startup succeeds, the launcher removes any stale CPU fallback container.
Run another node by cloning a second copy (or a separate checkout), then changing at least:
TS_HOSTNAME(must be unique)MODEL_ID(optional, if you want a different embedding model)VAULT_SECRET_PATH(or credentials at that path) for that node identity
Then launch as normal with ./scripts/compose-up-from-vault.sh. Each instance joins the tailnet as a separate tagged node.
Gateway /embed request fields used most often:
inputs: array of text stringstruncate: optional boolean
Response shape: array of float arrays (one embedding vector per input string).
Gateway /embed/sequence request fields:
text: one document window to embedmodel: optional model label echoed in the response
Response shape when enabled:
embedding/pooled_embedding: pooled document vectortoken_embeddings: per-token vectorstoken_offsets:{ start, end }character offsetstokens: token strings
Fallback behaviour:
- default mode is
EMBEDDER_SEQUENCE_MODE=disabled - disabled or oversized requests return
501 - Engine is expected to catch
404/501and fall back to per-chunk embeddings
curl http://embedder:8080/embed \
-X POST \
-H 'Content-Type: application/json' \
-d '{"inputs":["first text","second text"],"truncate":true}'
curl http://embedder:8080/embed/sequence \
-X POST \
-H 'Content-Type: application/json' \
-d '{"text":"One document window for late chunking."}'import requests
payload = {
"inputs": ["first text", "second text"],
"truncate": True,
}
resp = requests.post("http://embedder:8080/embed", json=payload, timeout=30)
resp.raise_for_status()
vectors = resp.json()
print(f"received {len(vectors)} embeddings")-
TEI metrics:
http://embedder:8080/metrics -
TEI health:
http://embedder:8080/health -
Tailscale status:
docker exec embedder-ts tailscale status
- GPU not detected
- Run
nvidia-smion the host. - Confirm NVIDIA Container Toolkit is installed and configured.
- Verify Docker can access GPU devices.
- If GPU startup still fails, launcher can temporarily fall back to
TEI_CPU_IMAGE_TAGwhenALLOW_CPU_FALLBACK=true.
- Run
- Tailscale not connecting
- Confirm Vault secret path contains valid
ts_client_idandts_client_secret. - Confirm
VAULT_ADDR,VAULT_TOKEN/VAULT_TOKEN_FILE, andVAULT_SECRET_PATHare correct. - Confirm
tag:embedderexists in ACLtagOwners. - Check logs:
docker logs embedder-ts.
- Confirm Vault secret path contains valid
- Slow first startup
- Expected on first boot while model weights download (about 500 MB for
nomic-embed-text-v1.5). - Subsequent starts use the persisted
tei-datavolume.
- Expected on first boot while model weights download (about 500 MB for
- Remote node gets connection refused
- Confirm ACL allows
tag:infra(or your source tag) totag:embedder:8080. - Confirm destination node is healthy (
./healthcheck.shanddocker ps).
- Confirm ACL allows
| GPU | Est. Throughput (TEI) | 1M Tokens | Notes |
|---|---|---|---|
| RTX 4000 Ada (20 GB) | ~22,000 tok/s | ~45 sec | Single-slot, 130 W |
| RTX 4090 (24 GB) | ~35,000 tok/s | ~29 sec | Consumer, 450 W |
| A10G (24 GB) | ~33,000 tok/s | ~30 sec | Common cloud GPU |
| RTX PRO 6000 Blackwell (96 GB) | ~130,000+ tok/s | ~8 sec | Datacentre class |
| CPU only | ~500-1,000 tok/s | ~17-33 min | Testing only |
Throughput varies by model size, sequence length, and batching configuration. These estimates are for roughly 137M-parameter embedding models and default batch settings.
MIT (see LICENSE).
- Platform overview: CueCrux README
- Progress tracker: PlanCrux/docs/master-plan/progress-tracker.md
- Port registry: PlanCrux/docs/reference/infrastructure/Port-Registry.md
- Related repos: Engine (embedding consumer), FactoryCrux (ingestion pipeline), VaultCrux (retrieval platform)
- HuggingFace TEI documentation
- Tailscale Docker guide