Quickstart¶
Build one model-aware service through the complete beginner path: install,
readiness, management authentication, rule creation, traffic, metrics, and
cleanup. The configuration and traffic steps use the public
ai-model-routing scenario; operational steps are checked against the frozen
Gateway Swagger union and CLI contract.
Choose a compatible version pair
The readiness route is implemented on current Gateway main, not Gateway
v0.9.8.9-rc.1. loxicmd get ready is present in CLI
v0.9.8.9-rc.2, but it still needs a compatible Gateway main build. If you
deploy the Gateway release, use GET /version as a process-liveness probe
and do not treat it as configuration readiness.
What you will build¶
- Management API:
https://gateway.example.com/netlox/v1 - Data-plane VIP:
192.0.2.10:2020 - Model:
llama-70b - Backend:
198.51.100.11:8080 - Rule mode: fullproxy (
mode: 4) - Credential policy: omitted for this isolated data-plane lab
The addresses are documentation placeholders. Replace them with your own reachable addresses before running the commands.
flowchart LR
O[Operator] -->|Bearer token| C[Management API]
U[Inference client] -->|model llama-70b| V[VIP 192.0.2.10:2020]
C --> V
V --> B[Backend 198.51.100.11:8080]
P[Prometheus client] -->|GET /metrics| C
1. Install¶
Example status: verified for documentation syntax and the published
container/startup contract; Linux data-plane execution is not performed by the
documentation checks.
Prerequisites¶
- A Linux host with Docker, a modern eBPF-capable kernel, and permission to run the privileged Gateway container.
- A version-pinned public Gateway image or a build from the exact source commit you intend to test.
- A persistent host directory for
/etc/loxilb.
Exact command¶
Complete the Installation page's container startup procedure. Then record the running image identity:
Expected result¶
The command exits 0 and gateway-image.id contains one nonempty image ID.
Validate¶
docker ps --filter name='^/loxilb$' --filter status=running --format '{{.Names}}' \
| grep -qx loxilb
curl --fail-with-body --silent --show-error \
http://127.0.0.1:11111/netlox/v1/version > gateway-version.json
jq -e 'type == "object"' gateway-version.json
Cleanup¶
Do not remove the Gateway yet; the remaining steps use it. At the end, remove only the container and persistent directory that you created for this lab.
Diagnose¶
| Symptom | Check |
|---|---|
| Container is absent | Inspect docker ps -a and the container log. |
/version is refused |
Confirm host networking or the published management port. |
| eBPF initialization fails | Recheck kernel support, privileges, and required mounts. |
2. Check readiness¶
Example status: verified against current-main Swagger, CLI
v0.9.8.9-rc.2, and the frozen readiness contract. This is source/static
evidence, not a live Linux result.
Prerequisites¶
- A current-main Gateway build that exposes
GET /status/ready. - CLI
v0.9.8.9-rc.2or later if you use the CLI form. - A management token in
CONTROL_PLANE_TOKENand a trusted TLS endpoint.
Prepare protected credential files once:
export CONTROL_API='https://gateway.example.com/netlox/v1'
install -m 600 /dev/null ./gateway.token ./control-plane.headers
printf '%s\n' "$CONTROL_PLANE_TOKEN" > ./gateway.token
printf 'Authorization: Bearer %s\n' "$CONTROL_PLANE_TOKEN" > ./control-plane.headers
Exact command¶
Expected result¶
Ready returns HTTP 200; not ready returns 503. Both responses carry the
typed body. The CLI exits nonzero for not-ready while preserving that body.
Validate¶
Cleanup¶
Keep the protected credential files for the next steps. Remove ready.json if
it contains deployment details you do not want to retain.
Diagnose¶
| Symptom | Check |
|---|---|
HTTP 404 |
The Gateway is the release build; /status/ready is current-main only. |
HTTP 503 |
Read reasons[], boot replay, external dependency, and persist state. |
HTTP 401 |
Recheck the management token file and the configured auth mode. |
3. Verify management authentication¶
Example status: verified against the management-auth contract. The
negative request is mandatory: an authenticated success alone cannot prove the
listener is protected.
Prerequisites¶
- One supported management authentication mode is enabled.
control-plane.headerscontains the valid bearer token from the readiness step.
Exact command¶
unauthenticated_status=$(curl --silent --show-error \
--output unauthenticated.json \
--write-out '%{http_code}' \
"$CONTROL_API/config/loadbalancer/all")
authenticated_status=$(curl --silent --show-error \
--header @control-plane.headers \
--output authenticated.json \
--write-out '%{http_code}' \
"$CONTROL_API/config/loadbalancer/all")
Expected result¶
The unauthenticated request returns 401; the authenticated request returns
200. Do not continue if both return 200.
Validate¶
test "$unauthenticated_status" = 401
test "$authenticated_status" = 200
jq -e 'type == "object" or type == "array"' authenticated.json
Cleanup¶
Keep the credential files until final cleanup. Remove the two response files after reviewing only sanitized fields.
Diagnose¶
| Symptom | Check |
|---|---|
Unauthenticated request returns 200 |
No management authenticator is active; do not expose the listener. |
Authenticated request returns 401 |
Token, issuer, expiry, or header-file contents are wrong. |
| TLS verification fails | Install the correct CA; do not make insecure TLS the permanent fix. |
Management and inference credentials are separate
The bearer token above protects configuration on port 11111. It is not an
inference API key or JWT. This lab deliberately omits api_key_auth, so a
backend-owned X-Api-Key passes through unchanged. Follow
Data-Plane Authentication and JWT
before exposing a protected inference VIP.
4. Create one model rule¶
Example status: verified against the Swagger request schema, CLI flags,
and the public model-routing scenario. Run either the REST tab or the CLI tab,
not both.
Prerequisites¶
- The VIP is assigned to the Gateway node.
198.51.100.11:8080is an HTTP backend that returns the markerbackend-llama.- The management authentication negative and positive checks passed.
- This is an isolated lab with no concurrent configuration writer or traffic generator; exact state and metric deltas depend on that isolation.
Exact command¶
create_status=$(curl --silent --show-error \
--request POST \
--header @control-plane.headers \
--header 'Content-Type: application/json' \
--data '{
"serviceArguments": {
"externalIP": "192.0.2.10",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "192.0.2.10",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "llama-70b",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "198.51.100.11", "targetPort": 8080, "weight": 1}
]
}' \
--output create.json \
--write-out '%{http_code}' \
"$CONTROL_API/config/loadbalancer")
Expected result¶
The REST form returns HTTP 200; the CLI exits 0. A duplicate rule or an
invalid body returns a non-success status and must not be counted as created.
Validate¶
test "${create_status:-200}" = 200
curl --fail-with-body --silent --show-error \
--header @control-plane.headers \
"$CONTROL_API/config/loadbalancer/all" > rules.json
jq -e '
.lbAttr[] |
select(.serviceArguments.externalIP == "192.0.2.10") |
select(.serviceArguments.port == 2020) |
.serviceArguments.model_name == "llama-70b" and
.serviceArguments.mode == 4
' rules.json
Also prove a rejected mutation leaves the complete readback unchanged:
# docs-example: expect-schema-error
jq -S . rules.json > rules-before-invalid.json
invalid_status=$(curl --silent --show-error \
--request POST \
--header @control-plane.headers \
--header 'Content-Type: application/json' \
--data '{
"serviceArguments": {
"externalIP": "192.0.2.10",
"port": 2020,
"protocol": "not-a-protocol",
"sel": 0,
"mode": 4
},
"endpoints": [
{"endpointIP": "198.51.100.11", "targetPort": 8080, "weight": 1}
]
}' \
--output invalid-create.json \
--write-out '%{http_code}' \
"$CONTROL_API/config/loadbalancer")
test "$invalid_status" = 400
curl --fail-with-body --silent --show-error \
--header @control-plane.headers \
"$CONTROL_API/config/loadbalancer/all" \
| jq -S . > rules-after-invalid.json
cmp --silent rules-before-invalid.json rules-after-invalid.json
Cleanup¶
If validation fails after creation, run the exact delete in step 7 before changing any key field.
Diagnose¶
| Symptom | Check |
|---|---|
HTTP 400 on the valid create |
Validate JSON field casing and required endpoint fields. |
HTTP 409 |
An equivalent rule already exists; inspect before deleting it. |
| Rule reads back without the model | Recheck exact model_name spelling and the complete L7 key. |
| Invalid create changes readback | Stop and restore the pre-change snapshot; rejection was not atomic. |
5. Send traffic¶
Example status: verified against the public model-routing scenario. The
backend marker is the independent delivery oracle; HTTP 200 without the
expected marker is not a pass.
Prerequisites¶
- The rule readback passed.
- The backend is healthy, returns
backend-llama, and appends each receivedX-Docs-Noncevalue to a line-oriented log available to the operator. - Set
BACKEND_RECEIPT_LOGto that backend-owned log. It must not be produced from Gateway or client output.
Exact command¶
export BACKEND_RECEIPT_LOG='/var/tmp/docs-backend-receipts.log'
test -r "$BACKEND_RECEIPT_LOG"
receipt_count() {
grep -cF -- "$1" "$BACKEND_RECEIPT_LOG" || true
}
curl --fail-with-body --silent --show-error \
"$CONTROL_API/metrics" > metrics-before.prom
positive_nonce="docs-positive-$(date +%s)-$$"
positive_receipts_before=$(receipt_count "$positive_nonce")
traffic_status=$(curl --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
--header "X-Docs-Nonce: $positive_nonce" \
--data '{"model":"llama-70b","messages":[{"role":"user","content":"hello"}]}' \
--output inference.json \
--write-out '%{http_code}' \
http://192.0.2.10:2020/v1/chat/completions)
Expected result¶
The request returns HTTP 200, and the backend-specific marker appears in the
response. The exact OpenAI response shape belongs to the backend.
Validate¶
test "$traffic_status" = 200
grep -q 'backend-llama' inference.json
positive_receipts_after=$(receipt_count "$positive_nonce")
test "$((positive_receipts_after - positive_receipts_before))" -eq 1
Also prove a wrong model does not reach this backend:
wrong_model_nonce="docs-negative-$(date +%s)-$$"
negative_receipts_before=$(receipt_count "$wrong_model_nonce")
wrong_model_status=$(curl --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
--header "X-Docs-Nonce: $wrong_model_nonce" \
--data '{"model":"unknown-model","messages":[]}' \
--output wrong-model.json \
--write-out '%{http_code}' \
http://192.0.2.10:2020/v1/chat/completions)
test "$wrong_model_status" = 503
! grep -q 'backend-llama' wrong-model.json
negative_receipts_after=$(receipt_count "$wrong_model_nonce")
test "$negative_receipts_after" -eq "$negative_receipts_before"
Cleanup¶
Keep the rule until after the metrics check. Remove response files when they are no longer needed.
Diagnose¶
| Symptom | Check |
|---|---|
HTTP 503 model_unavailable |
Model matching is case-sensitive; confirm llama-70b. |
HTTP 200 without the marker or receipt delta |
The request did not prove the expected active backend path. |
| Connection refused | Confirm the VIP, fullproxy listener, and backend reachability. |
| Wrong-model receipt count increases | Stop; the rejected request reached the backend. |
6. Inspect metrics¶
Example status: verified against the frozen release-scope metric
manifest and current writer mapping. The documentation check validates the
name and labels; it does not claim a live scrape occurred on your host.
Prerequisites¶
- Metrics export is enabled and
GET /metricsis reachable. metrics-before.promwas captured immediately before the single positive request on an otherwise idle, isolated Gateway.
Exact command¶
metrics_status=$(curl --silent --show-error \
--output metrics-after.prom \
--write-out '%{http_code}' \
"$CONTROL_API/metrics")
Expected result¶
The scrape returns HTTP 200. The sum of
loxilb_service_requests_total across its documented service label grows by
exactly one for the one positive request. The rejected wrong-model request must
not add to that service-delivery counter.
Validate¶
test "$metrics_status" = 200
grep -E '^loxilb_service_requests_total\{service="[^"]+"\} [0-9]+' metrics-after.prom
service_request_total() {
awk '$1 ~ /^loxilb_service_requests_total\{service=/ {sum += $2}
END {printf "%.0f\n", sum + 0}' "$1"
}
requests_before=$(service_request_total metrics-before.prom)
requests_after=$(service_request_total metrics-after.prom)
test "$((requests_after - requests_before))" -eq 1
Cleanup¶
Remove both metric snapshots after retaining only the sanitized evidence required by your deployment process.
Diagnose¶
| Symptom | Check |
|---|---|
HTTP 404 |
Confirm the API base path and the running Gateway build. |
HTTP 200 but no series |
Confirm export activation and that traffic crossed the active service path. |
| Delta is not exactly one | Stop other traffic, repeat from a clean isolated rule, and compare backend receipts. |
| Unexpected labels | Compare the query with the frozen metric reference. |
7. Clean up¶
Example status: verified against the exact model-keyed REST and CLI
delete contracts. Run the cleanup form that matches the create form you chose.
Prerequisites¶
- Use the same VIP, port, host, path, path mode, and model name used at create.
- Confirm that the matching rule belongs to this lab.
Exact command¶
cleanup_status=$(curl --silent --show-error \
--request DELETE \
--header @control-plane.headers \
--output cleanup.json \
--write-out '%{http_code}' \
"$CONTROL_API/config/loadbalancer/hosturl/192.0.2.10/externalipaddress/192.0.2.10/port/2020/protocol/tcp?path_prefix=%2F&path_match_mode=prefix&model_name=llama-70b")
Expected result¶
The REST form returns 200; the CLI exits 0.
Validate¶
test "${cleanup_status:-200}" = 200
curl --fail-with-body --silent --show-error \
--header @control-plane.headers \
"$CONTROL_API/config/loadbalancer/all" > rules-after-cleanup.json
! jq -e '
.lbAttr[] |
select(.serviceArguments.externalIP == "192.0.2.10") |
select(.serviceArguments.port == 2020) |
select(.serviceArguments.model_name == "llama-70b")
' rules-after-cleanup.json
curl --fail-with-body --silent --show-error \
--header @control-plane.headers \
"$CONTROL_API/status/ready" > ready-after-cleanup.json
jq -e '.ready == true and (.reasons | length == 0)' ready-after-cleanup.json
Cleanup¶
Remove local credential and response files:
rm -f ./gateway.token ./control-plane.headers ready.json \
unauthenticated.json authenticated.json create.json rules.json \
invalid-create.json rules-before-invalid.json rules-after-invalid.json \
inference.json wrong-model.json metrics-before.prom metrics-after.prom cleanup.json \
rules-after-cleanup.json ready-after-cleanup.json gateway-version.json gateway-image.id
unset CONTROL_PLANE_TOKEN CONTROL_API BACKEND_RECEIPT_LOG
Diagnose¶
| Symptom | Check |
|---|---|
HTTP 404 |
One rule-key component differs from create; inspect the exact readback. |
| Rule remains | Repeat the complete model-keyed delete; do not use delete-all on a shared Gateway. |
| Other rules disappeared | Stop and restore from the pre-change snapshot; cleanup scope was too broad. |
| Readiness fails after cleanup | Inspect reasons[]; cleanup is not complete until readiness recovers. |
REST-only surfaces¶
Do not invent CLI flags for surfaces the frozen CLI does not implement:
| Surface | Supported path |
|---|---|
JWT profile CRUD and jwt / apikey-or-jwt rule binding |
REST-only |
| Global/rule-default and user/user-model QoS CRUD | REST-only |
Model-profile discovery and kvexactstatus |
REST-only |
kvModelProfile and kvExactApiMode |
REST-only |
Capability readiness and sockmapreset |
REST-only |
The CLI reference carries the canonical compatibility matrix.
Next steps¶
- Model Load Balancing explains multi-pool and wildcard routing.
- Management API Authentication covers supported operator authentication modes.
- Data-Plane Authentication and JWT adds inference credentials without confusing the two trust planes.
- Monitoring and Metrics defines activation and evidence limits for every metric family.
- Verification Status explains why source/static, CI, Linux runtime, GPU, HA, and release evidence are separate.