Phase 1.4 β Deploy the unified Go bridge to bridge.lux.network
This runbook is the operator-side handoff for closing REQUIREMENTS.md Β§7 row 1.4 + Β§13.6 step 5. The engineering work is done; everything below is cluster + DNS + secret material that has to live outside the repo.
What gets deployed
A single Go binary image (ghcr.io/luxfi/bridge:<tag>) that embeds the SPA via go:embed and serves both:
/β Lux Bridge SPA (canonical Vite build, baked into the image)/v1/bridge/*β native API (networks / tokens / quote / swaps / MPC dispatch)
Replaces the legacy split of ghcr.io/luxfi/bridge-server (Express) + ghcr.io/luxfi/bridge-ui (Vite). Same hostnames, same wire contract for the SPA picker β drop-in for downstream consumers.
K8s artifact: k8s/bridge-deployment.yaml (already in this repo).
Prerequisites
| Requirement | How to verify |
|---|---|
K8s cluster with kubectl access | kubectl cluster-info |
Namespace lux-bridge exists | kubectl get ns lux-bridge (create with kubectl apply -f k8s/mpc-deployment.yaml first β it creates the namespace + the StatefulSet the bridge talks to) |
cert-manager installed with letsencrypt-prod ClusterIssuer | kubectl get clusterissuer letsencrypt-prod |
Ingress controller with ingressClassName: ingress | kubectl get ingressclass ingress |
Image ghcr.io/luxfi/bridge:latest exists | docker manifest inspect ghcr.io/luxfi/bridge:latest (CI docker.yml builds + pushes on tag) |
DNS control for bridge.lux.network + bridge-api.lux.network | Confirm with the team that owns the Lux zone |
Step 1 β Populate bridge-secrets
The Deployment references a bridge-secrets Secret in the lux-bridge namespace. Keys required for the bridge container:
| Key | Purpose | Source |
|---|---|---|
mpc-api-token | Bearer token for mpcd /keygen + /sign endpoints (required) | Output of the mpcd token-derivation step in mpc-deployment.yaml |
Keys referenced by other manifests in mpc-deployment.yaml (create alongside even if not consumed by the bridge container itself, since the same Secret is shared with the MPC StatefulSet):
| Key | Purpose |
|---|---|
mpc-db-password | Postgres password for mpcd's DB |
mpc-wallet-id | MPC wallet tenant id |
database-url | Full Postgres URL for mpcd |
fee-collector-address | Where bridge fees flow |
postgres-password | Root pw for the in-cluster postgres |
Create from a secrets file (never commit secrets to git):
kubectl create secret generic bridge-secrets \
--namespace lux-bridge \
--from-literal=mpc-api-token="$(cat /path/to/mpc-token)" \
--from-literal=mpc-db-password="$(cat /path/to/mpc-db-pw)" \
--from-literal=mpc-wallet-id="bridge-mainnet" \
--from-literal=database-url="postgresql://...mpcd-db..." \
--from-literal=fee-collector-address="0xYourFeeCollectorAddr" \
--from-literal=postgres-password="$(cat /path/to/pg-pw)"Verify:
kubectl -n lux-bridge get secret bridge-secrets -o jsonpath='{.data}' | jq 'keys'
# Expect: ["database-url","fee-collector-address","mpc-api-token","mpc-db-password","mpc-wallet-id","postgres-password"]Step 2 β Apply the manifest
kubectl apply -f k8s/bridge-deployment.yamlThis creates / updates:
ConfigMap/lux-bridge-configβ thenetworks.yamlbaked into/etc/bridge/networks.yamlPersistentVolumeClaim/lux-bridge-dataβ 20Gi for zapdb (single-writer)Deployment/lux-bridgeβreplicas: 1withstrategy: Recreate(zapdb takes an exclusive directory lock; multi-replica deploys require migrating the swap store to a shared external store first)Service/lux-bridgeβ ClusterIP, port 80 β 8080Ingress/lux-bridgeβ terminates TLS atbridge.lux.network+bridge-api.lux.network, routes both to the Service
Watch the rollout:
kubectl -n lux-bridge rollout status deployment/lux-bridge
kubectl -n lux-bridge logs -f deployment/lux-bridgeExpected log lines on a healthy startup:
swap store opened(withdata_dir=/var/lib/lux-bridge)deposit watcher startedsigning driver startedMPC keygen client readylistening on :8080
Failure modes:
mpc keygen unreachableβbridge-secrets.mpc-api-tokenwrong, or thempc-api-svc.lux-bridge.svc:8081service isn't routable from the bridge pod. Checkkubectl -n lux-bridge get svc mpc-api-svc.swap store open: zapdb: cannot lockβ PVC stuck on an old pod's lock.kubectl -n lux-bridge delete pod -l app=lux-bridgeand letRecreatestrategy spin a fresh pod once the PV releases.- Image pull error β CI hasn't pushed
:latestyet, or the cluster'sghcr.iopull secret is missing. Switch to a known-good tag explicitly:kubectl -n lux-bridge set image deployment/lux-bridge bridge=ghcr.io/luxfi/bridge:v1.2.3.
Step 3 β Verify ingress + TLS
kubectl -n lux-bridge get ingress lux-bridge
kubectl -n lux-bridge describe certificate lux-bridge-tlsWait for cert-manager.io/cluster-issuer: letsencrypt-prod to issue the cert. Status should reach Ready: True. If issuance is slow:
kubectl -n lux-bridge describe order
kubectl -n lux-bridge describe challengeCommon cause: DNS A-record not pointing at the LB yet (Step 4) and Let's Encrypt's HTTP-01 challenge fails the reachability check.
Step 4 β DNS cutover
Repoint both hostnames at the cluster's ingress LB IP:
bridge.lux.network A <LB_IP>
bridge-api.lux.network A <LB_IP>If the existing bridge.lux.network already points at the legacy bridge-server + bridge-ui stack, this is the cutover moment β clients will start hitting the Go binary as soon as DNS propagates (TTL-dependent). Pre-stage by lowering the TTL on the old record 24h in advance.
For a cautious cutover, leave the legacy bridge-server and bridge-ui deployments running alongside for ~1 week of soak. After confirming the Go binary handles real traffic without regressions, delete the legacy Deployments + Services + Ingress and the legacy ghcr.io/luxfi/bridge-ui + ghcr.io/luxfi/bridge/server images.
Step 5 β Post-deploy verification
| Check | Command |
|---|---|
| Health | curl -s https://bridge-api.lux.network/health | jq .status β "ok" |
| Networks served | curl -s https://bridge-api.lux.network/v1/bridge/networks | jq '.data | length' β should match the ConfigMap |
| Tokens served | curl -s 'https://bridge-api.lux.network/v1/bridge/tokens?network=ETHEREUM_MAINNET' | jq β ETH + USDC + USDT entries |
| Quote works | curl -s 'https://bridge-api.lux.network/v1/bridge/quote?source_network=ETHEREUM_MAINNET&source_token=USDC&destination_network=LUX_MAINNET&destination_token=LUX&amount=10&refuel=0' | jq .data.quote.receive_amount β a positive number |
| SPA renders | Open https://bridge.lux.network/ in a browser β LUX Bridge chip, chain picker populated |
| Swap creation reaches MPC | curl -X POST -H 'Content-Type: application/json' -d '{...}' https://bridge-api.lux.network/v1/bridge/swaps β returns a deposit_address derived from a freshly-minted MPC wallet |
Step 6 β Release wallet funding (ongoing, per destination network)
After the first swap targeting each destination network, the bridge auto-mints a long-lived MPC release wallet and persists it to /var/lib/lux-bridge/release-wallets.json. The bridge does not auto-fund these. Until each is hand-funded with native gas of its destination chain, swaps to that destination will stall in bridge_transfer_pending_broadcasting with last_error: "Insufficient funds in release address".
Discover the wallets after first activity:
kubectl -n lux-bridge exec deployment/lux-bridge -- cat /var/lib/lux-bridge/release-wallets.json | jqFund each address with ~0.5 native units per chain (enough for many swaps' gas + a few payouts). Top up via the operator treasury when balances fall below an alerting threshold. There is no auto-refill today.
Rollback
If the Go binary deploy regresses production:
- Revert DNS to point
bridge.lux.network+bridge-api.lux.networkat the legacy LB (ifbridge-server+bridge-uiare still running, this restores them immediately). - If the legacy stack was already retired, scale the Go bridge to zero and bring it back manually:
kubectl -n lux-bridge scale deployment/lux-bridge --replicas=0. - The PVC retains all in-flight swap state β re-scaling to 1 picks up where the binary left off.
Sign-off checklist (for closing REQUIREMENTS.md Β§7 row 1.4)
- Secret
bridge-secretspopulated withmpc-api-token -
kubectl apply -f k8s/bridge-deployment.yamlsucceeded - Pod
lux-bridge-*isReady -
Certificate/lux-bridge-tlsisReady: True - DNS A-records cut over (
dig bridge.lux.network) -
/healthreturns 200 over public HTTPS - A real testnet bridge transaction (source-chain deposit β MPC sign β destination payout) completes against the deployed pod
- Release wallets discovered + funded for all initially-supported destination networks
- Update REQUIREMENTS.md Β§7 row 1.4: π‘ β β with the testnet tx hash + MPC session id as evidence; Β§13.6 step 5: β¬ β β
That last commit also closes Phase 1.3 if the same tx satisfies the Β§7 acceptance criteria.