Prepare a shared-service lab with ordinary commands¶
Build the isolated packet path, initialize the policy database, and start the Gateway yourself. The only executable fixture is a synthetic backend that records requests and can hold them until you release them.
Before you start¶
Start with rootful Docker and no existing share-* resources. Choose DOCS, GATEWAY_SOURCE, GATEWAY_IMAGE and a new private LAB first.
Focus of this page: Build the three veth paths and a separate store before installing any policy.
Topology and execution locations¶
flowchart TD
A["Support namespace<br/>192.0.2.10"] -->|"sa to ga"| G["share-gateway<br/>inference 8080 / API 11111"]
B["Research namespace<br/>198.51.100.10"] -->|"sb to gb"| G
G -->|"ge to se"| E["Receipt backend namespace<br/>203.0.113.1:8080"]
G -.->|"key and objective store"| P["share-pg<br/>Docker bridge / 5432"]
O["Host Bash<br/>private LAB files"] -.->|"management via share-a"| G
O -.->|"independent receipt via share-backend"| E
style G fill:#e1f5fe,stroke:#0288d1,color:#1f2937
style E fill:#e8f5e9,stroke:#43a047,color:#1f2937
style P fill:#fff3e0,stroke:#fb8c00,color:#1f2937
One disposable Linux host contains two Gateway/DB containers and four network namespaces. Support reaches the Gateway on 192.0.2.254; research uses its 198.51.100.254 interface. The backend is a host Python process in share-backend, not an inference engine. PostgreSQL uses its inspected Docker-bridge address.
| Where | What happens here |
|---|---|
| Host root Bash | Create namespaces, save LAB inputs, start/stop exact owned processes. |
| share-a / share-b | Send inference as support/research; management curls use share-a. |
| share-gateway | Apply policy and forward requests; management is port 11111, inference port 8080. |
| share-backend | Read /receipt/ and /order directly, bypassing Gateway routing. |
| share-pg | Keep keys/objectives; pause only in the explicit recovery experiment. |
Walkthrough¶
Prerequisites and values to choose¶
Use a dedicated Linux host with root access, Docker, Bash, Python 3, curl, jq, OpenSSL and iproute2, with VLAN/VXLAN support. Use one dedicated root Bash for the entire lab (for example, start sudo bash --noprofile --norc from your normal terminal). The commands enable set -euo pipefail; an unexpected failure exits this lab shell and leaves your original terminal available for diagnosis. The short blocks share variables and background child processes, so execute them in this same shell in order. Read a complete experiment before starting its timed requests; do not continue after a failure. Do not put a block inside if, && or ||, which can suppress Bash error handling. The private $LAB/env file carries chosen values and the inspected DB address between blocks; source only the file created by this lab. Docker bridge networking must work. Obtain this documentation checkout and clean pinned Gateway sources before starting. Choose an image built from the matching Gateway/eBPF/libbpf tuple with socket-map support; record its immutable ID. A downloaded source tree alone does not identify a running binary. No GPU or recent CLI installation is required for this REST walkthrough.
Set DOCS, GATEWAY_SOURCE, GATEWAY_IMAGE and LAB to your actual inputs. For example, LAB may be a new /srv/shared-inference/run-1 directory. The checks below stop if an input is missing; they do not choose an image or source revision for you. Keep the new LAB private and do not reuse an old run directory.
Choose these four values in the dedicated lab shell before the first check. The paths below are examples: replace them with your own checkouts and immutable image. Keep LAB different for each clean run.
| Variable | Example assignment to edit | Used for |
|---|---|---|
| DOCS | export DOCS=/srv/loxilbdocs-inference-gateway |
Find the public backend and JSON inputs |
| GATEWAY_SOURCE | export GATEWAY_SOURCE=/srv/loxilb-inference-gateway |
Read the matching database bootstrap SQL |
| GATEWAY_IMAGE | export GATEWAY_IMAGE='your-image@sha256:<digest>' |
Start the exact selected Gateway binary |
| LAB | export LAB=/srv/shared-inference/run-1 |
Save private inputs, credentials, PID records and results |
| Value | Default in this walkthrough | When customizing |
|---|---|---|
| VIP / PORT | 192.0.2.254 / 8080 | Change client URLs, base rule host/externalIP/port, binding services and cleanup full key together |
| MODEL | shared-model | Change base rule model_name, key allowed_models, request model and cleanup key together |
| Backend | 203.0.113.1:8080 | Change endpointIP/targetPort and the backend network/fixture listen port together |
| Source networks | support 192.0.2.0/24, research 198.51.100.0/24 | Change addresses, routes and CIDR bindings together |
| Interfaces | ga/gb/ge at Gateway | Change peer names and interface bindings together |
| Tenant/key names | support and research | Change tenant_id, key definitions and expected fairness identity together |
The documentation ranges and fixed resource names below are for this isolated host. Do not copy its management-auth choice into a shared deployment. Use UC-1 for JWT, TLS, quotas and authentication prerequisites.
Step 1 — Choose and check the lab inputs
Load the selected paths and immutable image, create a private run directory, and check the required tools. The directory must be new; leave existing deployment state untouched.
set -euo pipefail
umask 077
export DOCS="${DOCS:?path to documentation checkout required}"
export GATEWAY_SOURCE="${GATEWAY_SOURCE:?path to clean pinned Gateway sources required}"
export GATEWAY_IMAGE="${GATEWAY_IMAGE:?matching immutable image reference required}"
export LAB="${LAB:?new absolute private run directory required}"
export FIX="$DOCS/docs/assets/shared-inference/fixtures"
export VIP='192.0.2.254'
export PORT=8080
export MODEL='shared-model'
export API="http://$VIP:11111/netlox/v1"
export INFER="http://$VIP:$PORT/v1/chat/completions"
umask 077
export PYTHONDONTWRITEBYTECODE=1
test "$(id -u)" -eq 0
[[ "$LAB" == /* && "$LAB" != / ]]
test ! -e "$LAB"
mkdir -p "$LAB/cert" "$LAB/config" "$LAB/database" "$LAB/keys"
chmod 700 "$LAB"
# PostgreSQL drops privileges; its own mount parent must be searchable.
chmod 755 "$LAB/database"
python3 --version
curl --version
jq --version
Step 2 — Check tool access and resource-name conflicts
Inspect the selected image and database bootstrap SQL, then reject any existing lab container or namespace name. Save the selected inputs in the private LAB only after these checks pass.
docker version >/dev/null
ip link show docker0 >/dev/null
docker image inspect "$GATEWAY_IMAGE" --format '{{.Id}}' > "$LAB/image-id.txt"
test -f "$GATEWAY_SOURCE/scripts/aigw-db-bootstrap.sql"
docker ps -a --format '{{.Names}} {{.Status}}'
ip netns list
for name in share-pg share-gateway; do
if docker inspect "$name" >/dev/null 2>&1; then
printf 'Resource already exists: %s\n' "$name" >&2
exit 1
fi
done
for name in share-gateway share-a share-b share-backend; do
if ip netns list | awk '{print $1}' | grep -Fxq "$name"; then
printf 'Namespace already exists: %s\n' "$name" >&2
exit 1
fi
done
printf 'new isolated run\n' > "$LAB/started"
declare -p DOCS GATEWAY_SOURCE GATEWAY_IMAGE LAB FIX VIP PORT MODEL API INFER > "$LAB/env"
chmod 600 "$LAB/env"
printf 'X-Api-Key: invalid-lab-key\n' > "$LAB/invalid-key.headers"
Initialize a dedicated database¶
The database has no published host port. The bootstrap SQL creates separate management/data-plane schemas and roles. Replace the database host/roles/password files in the Gateway command if you use an existing supported store; do not run this fresh-lab database setup against your production DB.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
PG_OWNER_PASSWORD=$(openssl rand -hex 16)
DP_PASSWORD=$(openssl rand -hex 16)
MGMT_PASSWORD=$(openssl rand -hex 16)
printf 'POSTGRES_USER=labowner\nPOSTGRES_DB=loxilb\nPOSTGRES_PASSWORD=%s\n' \
"$PG_OWNER_PASSWORD" > "$LAB/postgres.env"
printf '%s' "$DP_PASSWORD" > "$LAB/cert/dp-password"
printf '%s' "$MGMT_PASSWORD" > "$LAB/cert/mgmt-password"
docker run -d --name share-pg --label "shared-lab=$LAB" \
--env-file "$LAB/postgres.env" \
-v "$LAB/database:/var/lib/postgresql" postgres:18.6
pg_ready=false
for attempt in $(seq 1 60); do
if docker exec share-pg pg_isready -h 127.0.0.1 -U labowner -d loxilb >/dev/null 2>&1; then
pg_ready=true
break
fi
sleep 1
done
test "$pg_ready" = true
docker cp "$GATEWAY_SOURCE/scripts/aigw-db-bootstrap.sql" share-pg:/tmp/bootstrap.sql
docker exec -e "AIGW_DB_PASSWORD=$DP_PASSWORD" -e "AIGW_MGMT_DB_PASSWORD=$MGMT_PASSWORD" \
share-pg psql -h 127.0.0.1 -q -U labowner -d loxilb -f /tmp/bootstrap.sql
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' share-pg)
test -n "$PG_IP"
declare -p PG_IP >> "$LAB/env"
Expected: pg_isready succeeds over TCP and psql exits successfully. The TCP check excludes the temporary socket-only PostgreSQL server used during first-run initialization. Do not print the password variables or enable shell tracing. The private files under LAB are recovery inputs, not public evidence.
Create the Gateway container and packet paths¶
Start a shell container from the selected image, then attach three explicit veth pairs. Each peer name and address below is visible so you can adapt the topology. These commands create namespaces; they do not depend on an upstream scenario bootstrap.
Step 1 — Create the Gateway container and support path
Start the owned Gateway shell container, attach its network namespace, and create the support client path. Begin the research veth pair here; the next block completes it before any traffic is sent.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
docker run -dt --name share-gateway --label "shared-lab=$LAB" \
--privileged --entrypoint /bin/bash -e LLB_FC_MODE=enforce -e LLB_FC_MAX_QUEUE_DEPTH=0 \
-v "$LAB/cert:/opt/loxilb/cert:ro" -v "$LAB/config:/etc/loxilb" "$GATEWAY_IMAGE"
GATEWAY_PID=$(docker inspect -f '{{.State.Pid}}' share-gateway)
mkdir -p /var/run/netns
ln -s "/proc/$GATEWAY_PID/ns/net" /var/run/netns/share-gateway
ip netns add share-a
ip netns add share-b
ip netns add share-backend
# Support client <-> Gateway.
ip link add sa type veth peer name ga
ip link set sa netns share-a
ip link set ga netns share-gateway
ip netns exec share-a ip addr add 192.0.2.10/24 dev sa
ip netns exec share-a ip link set sa up
ip netns exec share-a ip route add default via 192.0.2.254
ip netns exec share-gateway ip addr add 192.0.2.254/24 dev ga
ip netns exec share-gateway ip link set ga up
# Research client <-> Gateway.
ip link add sb type veth peer name gb
ip link set sb netns share-b
Step 2 — Complete the research and backend paths
Move the research peer into the Gateway namespace, assign its addresses and routes, then connect the backend namespace. Bring up the displayed loopback interfaces before starting services.
ip link set gb netns share-gateway
ip netns exec share-b ip addr add 198.51.100.10/24 dev sb
ip netns exec share-b ip link set sb up
ip netns exec share-b ip route add default via 198.51.100.254
ip netns exec share-gateway ip addr add 198.51.100.254/24 dev gb
ip netns exec share-gateway ip link set gb up
# Backend <-> Gateway.
ip link add se type veth peer name ge
ip link set se netns share-backend
ip link set ge netns share-gateway
ip netns exec share-backend ip addr add 203.0.113.1/24 dev se
ip netns exec share-backend ip link set se up
ip netns exec share-backend ip route add default via 203.0.113.254
ip netns exec share-gateway ip addr add 203.0.113.254/24 dev ge
ip netns exec share-gateway ip link set ge up
for ns in share-a share-b share-backend share-gateway; do
ip netns exec "$ns" ip link set lo up
ip netns exec "$ns" sysctl -q -w net.ipv4.conf.all.rp_filter=0
done
ip netns exec share-gateway sysctl -q -w net.ipv4.ip_forward=1
ip netns exec share-a ip route
ip netns exec share-b ip route
ip netns exec share-gateway ip -br addr
The example packet path is share-a→192.0.2.254:8080→203.0.113.1:8080. Management is 192.0.2.254:11111/netlox/v1. Gateway→PostgreSQL uses the inspected Docker-bridge address; do not hard-code a bridge IP.
Start the synthetic backend and Gateway process¶
The backend's hold/release and receipt endpoints make admission observable without a GPU. They are lab-only HTTP endpoints, never production policy controls. Save its PID and process start time so cleanup can stop exactly the process you started.
Step 1 — Start the independent receipt backend
Start the synthetic server in the backend namespace and record its exact host PID and start time. This server remains active across chapters; cleanup later uses that process identity.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
ip netns exec share-backend python3 "$FIX/backend.py" > "$LAB/backend.log" 2>&1 &
BACKEND_PID=$!
printf '%s\n' "$BACKEND_PID" > "$LAB/backend.pid"
awk '{print $22}' "/proc/$BACKEND_PID/stat" > "$LAB/backend.starttime"
backend_ready=false
for attempt in $(seq 1 30); do
if ip netns exec share-backend curl -fsS --max-time 1 http://127.0.0.1:8080/health \
> "$LAB/backend-health.json"; then
backend_ready=true
break
fi
sleep 0.1
done
test "$backend_ready" = true
Step 2 — Start the Gateway and record binary identity
Launch the Gateway with the displayed store credentials and socket-map support. Save the database image, Gateway binary and bootstrap hashes so the selected source can be compared with the actual runtime.
docker exec -d share-gateway sh -c '
exec /root/loxilb-io/loxilb/loxilb -p --sockmapsupport --loglevel debug \
--mgmt-db-host "$1" --mgmt-db-port 5432 --mgmt-db-user aigw_mgmt_user \
--mgmt-db-name loxilb --mgmt-db-password-file /opt/loxilb/cert/mgmt-password \
--aikey-db-host "$1" --aikey-db-port 5432 --aikey-db-user aigwuser \
--aikey-db-name loxilb --aikey-db-password-file /opt/loxilb/cert/dp-password \
> /tmp/gateway.out 2> /tmp/gateway.err
' sh "$PG_IP"
docker inspect -f '{{.Image}}' share-gateway > "$LAB/gateway-image.txt"
docker inspect -f '{{.Image}}' share-pg > "$LAB/database-image.txt"
docker exec share-gateway sha256sum /root/loxilb-io/loxilb/loxilb > "$LAB/binary.sha256"
sha256sum "$GATEWAY_SOURCE/scripts/aigw-db-bootstrap.sql" > "$LAB/db-bootstrap.sha256"
Wait for readiness and required stores¶
The management API in this isolated lab has userservice disabled, so its probes do not require a bearer. The displayed --mgmt-db-* flags are inactive without --userservice; they do not enable management authentication or establish management-store readiness. The --aikey-db-* flags configure the active data-plane key/objective store. Enabling userservice is a separate authenticated-deployment exercise. In an authenticated deployment provide the required management bearer and validate that probe contract. A version response alone is not readiness; the startup limitation remains open.
The loop below waits for all three necessary endpoints, preserves every probe status, and fails after a bounded number of attempts. HTTP000 means a transport failure, not a successful denial.
set -euo pipefail
umask 077
source "${LAB:?set LAB to your private run directory}/env"
probe_log="$LAB/readiness-startup.jsonl"
ready=false
for attempt in $(seq 1 90); do
ready=true
for path in /status/ready /config/ai/apikey /config/ai/objective; do
if status=$(ip netns exec share-a curl -sS --max-time 5 \
-o "$LAB/readiness-body.json" -w '%{http_code}' "$API$path"); then
:
else
status=000
fi
jq -nc --arg path "$path" --arg status "$status" \
'{time:now,path:$path,http:$status}' >> "$probe_log"
test "$status" = 200 || ready=false
done
test "$ready" = true && break
sleep 1
done
test "$ready" = true
tail -n 3 "$probe_log"
ip netns exec share-a curl -fsS --max-time 5 "$API/version" | jq .
ip netns exec share-a curl -fsS --max-time 5 "$API/status/capabilities" | jq .
Expected: the last probe set contains 200 for readiness, key list and objective list; backend health is ready. If setup fails, retain LAB and remove only the resources you created using cleanup. Continue with command conventions and tenant onboarding.
Check your result¶
Require readiness plus key/objective APIs and direct backend health; /version alone is insufficient.
Keep the configured policy, HTTP status, resolved identity, and independent backend receipt together. A rejected request must have receipt 0; a successful request must have receipt 1. A missing receipt response is a failed observation, never a zero.
Next steps and cleanup¶
After all readiness and direct backend checks pass, read Inputs & Conventions, then continue with Tenant & Priority Authority. If setup stops early, preserve LAB and use the ownership-aware cleanup steps only for resources actually created; do not rerun setup over a partial lab.